import React, { useState, useCallback, useRef } from 'react'; import useTravelData from './useTravelData'; import MiniMap, { getRegionAccent } from './MiniMap'; import AlbumCard from './AlbumCard'; import AlbumDetail from './AlbumDetail'; import './Travel.css'; /* ───────────────────────────────────────────── Travel — main page component ───────────────────────────────────────────── */ const Travel = () => { const { regions, albums, loadingAlbums, selectedRegion, setSelectedRegion, photos, photoSummary, loading, loadingMore, error, hasNext, loadAlbumPhotos, loadMorePhotos, reloadAlbumPhotos, getFilteredAlbums, refreshAlbums, } = useTravelData(); /* ── Local state ──────────────────────────── */ const [selectedAlbum, setSelectedAlbum] = useState(null); const [albumSourceRect, setAlbumSourceRect] = useState(null); const [syncing, setSyncing] = useState(false); const [syncResult, setSyncResult] = useState(null); const syncTimerRef = useRef(null); /* ── Computed ──────────────────────────────── */ const regionAccent = getRegionAccent(selectedRegion || ''); const filteredAlbums = getFilteredAlbums(selectedRegion); /* ── Handlers ─────────────────────────────── */ const handleSelectRegion = useCallback( (regionId) => { setSelectedRegion(regionId); }, [setSelectedRegion], ); const handleClearRegion = useCallback(() => { setSelectedRegion(null); }, [setSelectedRegion]); const handleOpenAlbum = useCallback( (album, rect) => { setSelectedAlbum(album); setAlbumSourceRect(rect || null); loadAlbumPhotos(album.region, album.name); }, [loadAlbumPhotos], ); const handleCloseAlbum = useCallback(() => { setSelectedAlbum(null); setAlbumSourceRect(null); }, []); const handleLoadMore = useCallback(() => { if (!selectedAlbum) return; loadMorePhotos(selectedAlbum.region, selectedAlbum.name); }, [selectedAlbum, loadMorePhotos]); const handleReload = useCallback(() => { if (!selectedAlbum) return; return reloadAlbumPhotos(selectedAlbum.region, selectedAlbum.name); }, [selectedAlbum, reloadAlbumPhotos]); const handleSync = useCallback(async () => { if (syncing) return; setSyncing(true); setSyncResult(null); try { const res = await fetch('/api/travel/sync', { method: 'POST' }); if (!res.ok) throw new Error(`${res.status}`); const data = await res.json(); setSyncResult(data); if (syncTimerRef.current) clearTimeout(syncTimerRef.current); syncTimerRef.current = setTimeout(() => setSyncResult(null), 8000); } catch (e) { setSyncResult({ error: e.message }); if (syncTimerRef.current) clearTimeout(syncTimerRef.current); syncTimerRef.current = setTimeout(() => setSyncResult(null), 5000); } finally { setSyncing(false); } }, [syncing]); /* ── Render ────────────────────────────────── */ return (
{/* ═══════════════════════════════════════ HEADER — editorial masthead ═══════════════════════════════════════ */}
Visual Diary · 여행 포토 아카이브

Travel Archive

여행에서 포착한 색, 빛, 장면들을 필름처럼 현상합니다. 지도에서 지역을 선택하면 해당 앨범이 펼쳐집니다.

{syncResult && (
{syncResult.error ? `동기화 실패: ${syncResult.error}` : `+${syncResult.added} 추가 / ${syncResult.removed} 삭제 / 썸네일 ${syncResult.thumbs_generated}개${syncResult.discovered ? ` / 신규 폴더 ${syncResult.discovered}개 발견` : ''} (${syncResult.duration_sec}s)` }
)}
{selectedRegion ? (

Currently viewing

{selectedRegion}

) : (

지도에서 지역을 선택하세요

)}
{/* ═══════════════════════════════════════ MAP — collapsible mini-map ═══════════════════════════════════════ */} {/* ═══════════════════════════════════════ ALBUM CARDS ═══════════════════════════════════════ */}
{loadingAlbums && filteredAlbums.length === 0 && (

Loading albums…

)} {!loadingAlbums && filteredAlbums.length === 0 && (

{selectedRegion ? '이 지역에는 아직 앨범이 없습니다.' : '지도에서 지역을 선택하거나 전체 앨범을 둘러보세요.'}

)} {filteredAlbums.length > 0 && (
{filteredAlbums.map((album) => ( ))}
)}
{/* ═══════════════════════════════════════ ALBUM DETAIL OVERLAY ═══════════════════════════════════════ */} {selectedAlbum && ( )}
); }; export default Travel;