Skip to content
Snippets Groups Projects
Select Git revision
  • 06766f7532d2a4e2287a752ba4d3902c0b09a737
  • master default protected
2 results

led_dev.mod.c

Blame
  • AddPlace.js 20.60 KiB
    import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
    import '../styles/AddPlace.css';
    import { FaPhoneAlt, FaStar, FaPlus, FaCalendarAlt, FaUser, FaTicketAlt } from 'react-icons/fa';
    import axios from 'axios';
    import { toast } from 'react-toastify';
    import { AREA_CODE } from '../constants/areaCode';
    import { useAuth } from '../App';
    
    const API_URL = process.env.REACT_APP_SERVER_URL;
    
    const AddPlace = ({ tripId, day, onBack, onPlaceSelect, tripStartDate, tripEndDate, location, onSuccess }) => {
      const { user } = useAuth();
      const [activeTab, setActiveTab] = useState('attractions');
      const [searchKeyword, setSearchKeyword] = useState('');
      const [favorites, setFavorites] = useState(new Set());
      const [favoriteItems, setFavoriteItems] = useState([]);
      const [debouncedKeyword, setDebouncedKeyword] = useState(searchKeyword);
      const [lastApiCall, setLastApiCall] = useState(0);
      const API_CALL_INTERVAL = 1000;
    
      const [tabStates, setTabStates] = useState({
        attractions: {
          places: [],
          loading: false,
          pageNo: 1,
          hasMore: true,
          totalCount: 0
        },
        festivals: {
          places: [],
          loading: false,
          pageNo: 1,
          hasMore: true,
          totalCount: 0
        }
      });
    
      const updateTabState = (tab, newState) => {
        setTabStates(prev => ({
          ...prev,
          [tab]: {
            ...prev[tab],
            ...newState
          }
        }));
      };
    
      const getAreaCode = (location) => {
        const sido = location.split(/[\s,]/)[0];
        const areaCode = AREA_CODE[sido];
    
        console.log('추출된 지역명:', sido);
        console.log('지역 코드:', areaCode);
    
        if (!areaCode) {
          console.warn(`지역 코드를 찾을 수 없습니다: ${sido}`);
          return null;
        }
    
        return areaCode;
      };
    
      useEffect(() => {
        const currentState = tabStates[activeTab];
        if (currentState.pageNo > 1 && currentState.hasMore) {
          searchPlaces(currentState.pageNo, false);
        }
      }, [tabStates[activeTab].pageNo]);
    
      const handleTabChange = (newTab) => {
        setActiveTab(newTab);
        if (tabStates[newTab].places.length === 0) {
          searchPlaces(1, true);
        }
      };
    
      useEffect(() => {
        const handler = setTimeout(() => {
          setDebouncedKeyword(searchKeyword);
        }, 300);
        return () => clearTimeout(handler);
      }, [searchKeyword]);
    
      useEffect(() => {
        const currentState = tabStates[activeTab];
        console.log('페이지 번호:', currentState.pageNo);
        console.log('로딩 상태:', currentState.loading);
        console.log('더 불러올 데이터 여부:', currentState.hasMore);
        console.log('현재 탭:', activeTab);
        console.log('현재 탭의 데이터 개수:', currentState.places.length);
      }, [tabStates, activeTab]);
    
      useEffect(() => {
        updateTabState(activeTab, {
          pageNo: 1,
          hasMore: true
        });
    
        if (tabStates[activeTab].places.length === 0) {
          searchPlaces(1, true);
        }
      }, [activeTab]);
    
      useEffect(() => {
        const fetchFavorites = async () => {
          if (!user) return;
    
          try {
            const response = await axios.get(`${API_URL}/favorites?userId=${user._id}`);
            const favoriteItems = response.data.data;
    
            // 중복된 placeId 제거 (Set 사용)
            const uniqueFavorites = favoriteItems.reduce((acc, curr) => {
              if (!acc.some(item => item.placeId === curr.placeId)) {
                acc.push(curr);
              }
              return acc;
            }, []);
    
            // 즐겨찾기 Set 업데이트 (placeId 기준)
            const favoriteIds = new Set(uniqueFavorites.map(item => item.placeId));
            setFavorites(favoriteIds);
    
            // 즐겨찾기 장소 데이터 저장
            const favorites = uniqueFavorites.map(item => ({
              id: item.placeId,
              name: item.placeName,
              location: item.placeAddress,
              type: item.typeId,
              areaCode: item.areaCode,
              coordinates: item.coordinates,
              isFavorite: true
            }));
    
            setFavoriteItems(favorites);
          } catch (error) {
            console.error('즐겨찾기 불러오기 실패:', error);
            console.error('에러 상세:', error.response?.data);
          }
        };
    
        fetchFavorites();
      }, [user]);
    
      const observerRef = useRef();
      const loadingRef = useRef(null);
    
      const TOUR_API_KEY = process.env.REACT_APP_OPEN_API_KEY;
      const TOUR_API_BASE_URL = 'https://apis.data.go.kr/B551011/KorService1';
    
      const CONTENT_TYPE = {
        attractions: '12',
        festivals: '15'
      };
    
      const filteredPlaces = useMemo(() => {
        const currentState = tabStates[activeTab];
        if (!currentState.places) return [];
    
        const currentTypeId = CONTENT_TYPE[activeTab];
        const currentAreaCode = getAreaCode(location);
    
        // 현재 탭입과 지역에 맞는 즐겨찾기 항목 필터링
        const matchingFavorites = favoriteItems.filter(place =>
          String(place.type) === currentTypeId &&
          place.areaCode === currentAreaCode
        );
    
        // 즐겨찾기 항목을 먼저, 그 다음에 일반 항목들
        return [...matchingFavorites, ...currentState.places];
      }, [tabStates[activeTab].places, favoriteItems, activeTab, location]);
    
      const toggleFavorite = async (place) => {
        if (!user) {
          toast.error('로그인이 필요한 서비스입니다.');
          return;
        }
    
        try {
          if (favorites.has(place.id)) {
            // 즐겨찾기 삭제 로직
            const response = await axios.delete(`${API_URL}/favorites/${place.id}`, {
              data: {
                userId: user._id
              }
            });
    
            if (response.status === 200) {
              setFavorites(prev => {
                const newFavorites = new Set(prev);
                newFavorites.delete(place.id);
                return newFavorites;
              });
    
              setFavoriteItems(prev => prev.filter(item => item.id !== place.id));
              toast.success('즐겨찾기가 삭제되었습니다.');
            }
          } else {
            // 즐겨찾기 추가 로직
            const areaCode = getAreaCode(place.location);
    
            const favoriteData = {
              userId: user._id,
              placeId: String(place.id),
              placeName: place.name,
              placeAddress: place.location,
              areaCode: areaCode,
              typeId: Number(place.type || 12),
              coordinates: place.coordinates
            };
    
            const response = await axios.post(`${API_URL}/favorites`, favoriteData);
    
            if (response.status === 201) {
              setFavorites(prev => {
                const newFavorites = new Set(prev);
                newFavorites.add(place.id);
                return newFavorites;
              });
    
              setFavoriteItems(prev => [...prev, {
                id: place.id,
                name: place.name,
                location: place.location,
                type: place.type || 12,
                areaCode: areaCode,
                coordinates: place.coordinates,
                isFavorite: true
              }]);
    
              toast.success('즐겨찾기에 추가되었습니다.');
            }
          }
        } catch (error) {
          console.error('즐겨찾기 처리 실패:', error);
          console.error('에러 상세:', error.response?.data);
          toast.error('즐겨찾기 처리에 실패했습니다.');
        }
      };
    
      const observerCallback = useCallback(entries => {
        const target = entries[0];
        const currentState = tabStates[activeTab];
        
        if (target.isIntersecting && currentState.hasMore && !currentState.loading) {
          const now = Date.now();
          const timeToWait = API_CALL_INTERVAL - (now - lastApiCall);
    
          if (timeToWait <= 0) {
            updateTabState(activeTab, {
              pageNo: currentState.pageNo + 1
            });
            setLastApiCall(now);
            console.log('API 호출 준비 완료');
          } else {
            console.log(`API 호출 간격이 너무 짧습니다. ${timeToWait}ms 후 재시도...`);
            setTimeout(() => {
              updateTabState(activeTab, {
                pageNo: currentState.pageNo + 1
              });
              setLastApiCall(Date.now());
              console.log('대기 후 API 호출 준비 완료');
            }, timeToWait);
          }
        }
      }, [tabStates, activeTab, lastApiCall]);
    
      useEffect(() => {
        const observer = new IntersectionObserver(observerCallback, {
          threshold: 0.5,
          rootMargin: '100px'
        });
        observerRef.current = observer;
    
        const loadingElement = loadingRef.current;
        if (loadingElement) {
          observer.observe(loadingElement);
        }
    
        return () => observer.disconnect();
      }, [observerCallback, tabStates[activeTab].loading]);
    
      useEffect(() => {
        const loadingElement = loadingRef.current;
        const currentState = tabStates[activeTab];
    
        if (loadingElement && observerRef.current) {
          observerRef.current.observe(loadingElement);
        }
    
        return () => {
          if (loadingElement && observerRef.current) {
            observerRef.current.unobserve(loadingElement);
          }
        };
      }, [activeTab, tabStates[activeTab].loading]);
    
      useEffect(() => {
        const currentState = tabStates[activeTab];
        if (currentState.pageNo > 1 && currentState.hasMore) {
          searchPlaces(currentState.pageNo, false);
        }
      }, [tabStates[activeTab].pageNo]);
    
      const formatISODate = (isoDateString) => {
        const date = new Date(isoDateString);
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        return `${year}-${month}-${day}`;
      };
    
      const searchPlaces = async (page, isNewSearch, keyword = '') => {
        const currentState = tabStates[activeTab];
        if (currentState.loading || (!currentState.hasMore && !isNewSearch)) return;
        
        const areaCode = getAreaCode(location);
        if (!areaCode) {
          toast.error('올바른 지역 정보가 아닙니다.');
          return;
        }
    
        updateTabState(activeTab, { loading: true });
        try {
          // 키워드가 있으면 키워드 검색 API를, 없으면 기존 지역 기반 API를 사용
          const apiEndpoint = keyword 
            ? 'searchKeyword1'
            : 'areaBasedList1';
    
          const response = await fetch(
            `${TOUR_API_BASE_URL}/${apiEndpoint}` +
            `?ServiceKey=${TOUR_API_KEY}` +
            `&contentTypeId=${CONTENT_TYPE[activeTab]}` +
            `&areaCode=${areaCode}` +
            `&listYN=Y` +
            `&MobileOS=ETC` +
            `&MobileApp=AppTest` +
            `&arrange=E` +
            `&numOfRows=` + (CONTENT_TYPE[activeTab] === '15' ? 50 : 20) +
            `&pageNo=${page}` +
            (keyword ? `&keyword=${encodeURIComponent(keyword)}` : '') +
            `&_type=json`
          );
    
          const text = await response.text();
    
          if (text.includes('<OpenAPI_ServiceResponse>')) {
            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(text, 'text/xml');
            const errorMsg = xmlDoc.querySelector('returnAuthMsg')?.textContent;
            const reasonCode = xmlDoc.querySelector('returnReasonCode')?.textContent;
    
            let userErrorMsg = '서비스 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
            if (errorMsg === 'LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR') {
              userErrorMsg = '일일 API 호출 한도를 초과습니다. 잠시 후 다시 시도해주세요.';
            }
    
            console.log(userErrorMsg);
    
            updateTabState(activeTab, {
              loading: false,
              hasMore: false,
              error: userErrorMsg
            });
            return;
          }
          const data = JSON.parse(text);
    
          const items = data.response.body.items.item || [];
    
          console.log(items);
    
          if (activeTab === 'festivals') {  // 지역 축제인 경우, 여행 기간에 맞게 필터링
            const formattedTripStartDate = formatISODate(tripStartDate);
            const formattedTripEndDate = formatISODate(tripEndDate);
    
            console.log(`Formatted Trip Start Date: ${formattedTripStartDate}`);
            console.log(`Formatted Trip End Date: ${formattedTripEndDate}`);
    
            const filteredItems = await Promise.all(
              items.map(async (item) => {
                try {
                  const detailResponse = await fetch(
                    `${TOUR_API_BASE_URL}/detailIntro1` +
                    `?ServiceKey=${TOUR_API_KEY}` +
                    `&contentId=${item.contentid}` +
                    `&contentTypeId=15` +
                    `&MobileOS=ETC` +
                    `&MobileApp=AppTest` +
                    `&_type=json`
                  );
    
                  const detailData = await detailResponse.json();
                  const festivalInfo = detailData.response.body.items.item[0];
                  const formatDate = (dateString) => {
                    const year = dateString.substring(0, 4);
                    const month = dateString.substring(4, 6);
                    const day = dateString.substring(6, 8);
                    return `${year}-${month}-${day}`;
                  };
    
                  const festivalStart = formatDate(festivalInfo.eventstartdate);
                  const festivalEnd = formatDate(festivalInfo.eventenddate);
    
                  if (festivalEnd >= formattedTripStartDate && festivalStart <= formattedTripEndDate) {
                    console.log(festivalInfo.sponsor1);
                    console.log(festivalInfo.usetimefestival);
                    console.log(festivalInfo);
                    return {
                      ...item,
                      eventStartDate: festivalStart,
                      eventEndDate: festivalEnd,
                      sponsor: festivalInfo.sponsor1,
                      usetimefestival: festivalInfo.usetimefestival,
                    };
                  }
                  return null;
                } catch (error) {
                  console.error('Failed to fetch festival details:', error);
                  return null;
                }
              })
            );
    
            const validItems = filteredItems.filter(item => item !== null);
            const newPlaces = validItems.map(item => ({
              id: item.contentid,
              type: item.contenttypeid,
              name: item.title,
              location: `${item.addr1} ${item.addr2 || ''}`.trim(),
              coordinates: {
                lat: parseFloat(item.mapy),
                lng: parseFloat(item.mapx)
              },
              image: item.firstimage,
              tel: item.tel,
              dist: item.dist,
              eventStartDate: item.eventStartDate,
              eventEndDate: item.eventEndDate,
              sponsor: item.sponsor,
              usetimefestival: item.usetimefestival
            }));
    
            updateTabState('festivals', {
              places: isNewSearch ? newPlaces : [...currentState.places, ...newPlaces],
              loading: false,
              hasMore: newPlaces.length > 0,
              totalCount: validItems.length
            });
          } else { // 그냥 관광지인 경우
            const total = parseInt(data.response.body.totalCount);
            const newPlaces = items.map(item => ({
              id: item.contentid,
              type: item.contenttypeid,
              name: item.title,
              location: `${item.addr1} ${item.addr2 || ''}`.trim(),
              coordinates: {
                lat: parseFloat(item.mapy),
                lng: parseFloat(item.mapx)
              },
              image: item.firstimage,
              tel: item.tel,
              dist: item.dist
            }));
    
            updateTabState('attractions', {
              places: isNewSearch ? newPlaces : [...currentState.places, ...newPlaces],
              loading: false,
              hasMore: newPlaces.length > 0,
              totalCount: total
            });
          }
        } catch (error) {
          console.error('Failed to fetch places:', error);
        } finally {
          updateTabState(activeTab, { loading: false });
        }
      };
    
      const handleAddPlace = async (place, e) => {
        e.stopPropagation();
    
        try {
          // 현재 day의 plans 정보 가져오기
          const tripResponse = await axios.get(`${API_URL}/api/trips/trips/${tripId}`);
          const currentTrip = tripResponse.data;
          const currentDayPlans = currentTrip.plans[day] || { places: [], route: [] };
    
          // 새로운 place 객체 생성
          const newPlace = {
            id: place.id,
            type: place.type || 12,
            name: place.name,
            address: place.location,
            coordinates: place.coordinates
          };
    
          // 기존 places와 route에 새로운 장소 추가
          const updatedPlaces = [...currentDayPlans.places, newPlace];
          const updatedRoute = [...currentDayPlans.route, place.id];
          const requestData = {
            dayKey: day,
            places: updatedPlaces,
            route: updatedRoute
          };
    
          console.log(requestData);
    
          const response = await axios.put(
            `${API_URL}/api/trips/trips/${tripId}/plans/day`,
            requestData
          );
    
          if (response.status === 200 || response.status === 201) {
            toast.success('장소가 성공으로 추가되었습니다.');
            onSuccess?.();
            onBack();
          }
        } catch (error) {
          console.error('장소 추가 실패:', error);
          toast.error(error.response?.data?.message || '장소 추가에 실패했습니다.');
        }
      };
    
      useEffect(() => {
        if (debouncedKeyword) {
          // 검색어가 있을 때는 첫 페이지부터 새로 검색
          updateTabState(activeTab, {
            places: [],
            pageNo: 1,
            hasMore: true
          });
          searchPlaces(1, true, debouncedKeyword);
        } else if (tabStates[activeTab].places.length === 0) {
          // 검색어가 없고 장소 목록이 비어있을 때는 기본 지역 검색
          searchPlaces(1, true);
        }
      }, [debouncedKeyword, activeTab]);
    
      return (
        <div className="add-place">
          <div className="search-container">
            <input
              type="text"
              placeholder="장소를 검색하세요"
              value={searchKeyword}
              onChange={(e) => setSearchKeyword(e.target.value)}
              className="search-input"
            />
          </div>
    
          <div className="tabs">
            <button
              className={`tab ${activeTab === 'attractions' ? 'active' : ''}`}
              onClick={() => handleTabChange('attractions')}
            >
              관광지
            </button>
            <button
              className={`tab ${activeTab === 'festivals' ? 'active' : ''}`}
              onClick={() => handleTabChange('festivals')}
            >
              지역축제
            </button>
          </div>
    
          <div className="add-places-list">
            {filteredPlaces.map((place) => (
              <div
                key={place.id}
                className="place-item"
                onClick={() => {
                  if (place.coordinates) {
                    onPlaceSelect(place);
                  } else {
                    toast.warning('해당 장소의 좌표 정보가 없습니다.');
                  }
                }}
              >
                <div className="place-info">
                  <h3>{place.name}</h3>
                  <p className="location">{place.location}</p>
                  {place.tel && <p className="tel"><FaPhoneAlt className="tel-icon" />{place.tel}</p>}
                  {activeTab === 'festivals' && (
                    <>
                      {place.eventStartDate && place.eventEndDate && (
                        <p className="festival-period">
                          <FaCalendarAlt className="calendar-icon" />
                          {place.eventStartDate} ~ {place.eventEndDate}
                        </p>
                      )}
                      {place.sponsor && (
                        <p className="festival-sponsor">
                          <FaUser className="sponsor-icon" />
                          {place.sponsor}
                        </p>
                      )}
                      {place.usetimefestival && (
                        <p className="festival-fee">
                          <FaTicketAlt className="fee-icon" />
                          <span dangerouslySetInnerHTML={{ __html: place.usetimefestival }} />
                        </p>
                      )}
                    </>
                  )}
                </div>
                <div className="button-group">
                  <button
                    className={`favorite-button ${favorites.has(place.id) ? 'active' : ''}`}
                    onClick={(e) => {
                      e.stopPropagation();
                      toggleFavorite(place);
                    }}
                  >
                    <FaStar />
                  </button>
                  <button
                    className="add-button"
                    onClick={(e) => handleAddPlace(place, e)}
                  >
                    <FaPlus />
                  </button>
                </div>
              </div>
            ))}
            <div ref={loadingRef} className="loading-indicator">
              {tabStates[activeTab].loading && (
                <div>
                  <div className="spinner" />
                  <div className="loading-text">로딩 중...</div>
                </div>
              )}
              {!tabStates[activeTab].loading && tabStates[activeTab].hasMore && (
                <div className="loading-text">
                  스크롤하여 더 보기
                </div>
              )}
              {!tabStates[activeTab].hasMore && (
                <div className="loading-text">
                  더 이상 결과가 없습니다
                </div>
              )}
            </div>
          </div>
        </div>
      );
    };
    
    export default AddPlace;