diff --git a/src/pages/travel/AlbumDetail.css b/src/pages/travel/AlbumDetail.css index d552bbd..7fafbeb 100644 --- a/src/pages/travel/AlbumDetail.css +++ b/src/pages/travel/AlbumDetail.css @@ -85,6 +85,111 @@ width: fit-content; } +.album-detail__region--editable { + border: 1px solid transparent; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 4px; + transition: border-color 0.2s, background 0.2s; +} +.album-detail__region--editable:hover { + border-color: var(--tv-line-bright, rgba(232, 221, 208, 0.22)); + background: var(--tv-surface-2, #221c14); +} + +.album-detail__region-edit-icon { + opacity: 0; + transition: opacity 0.2s; +} +.album-detail__region--editable:hover .album-detail__region-edit-icon { + opacity: 0.6; +} + +.album-detail__region-ok { + color: #c8905e; +} +.album-detail__region-err { + color: #dc5050; +} + +/* ── Region editor ── */ +.album-detail__region-editor { + position: relative; + display: flex; + align-items: center; + gap: 4px; +} + +.album-detail__region-input { + font-family: var(--tv-mono, 'SF Mono', 'Fira Code', monospace); + font-size: 11px; + letter-spacing: 0.04em; + color: var(--tv-text, #e8ddd0); + background: var(--tv-surface-2, #221c14); + border: 1px solid var(--tv-line-bright, rgba(232, 221, 208, 0.22)); + border-radius: 4px; + padding: 4px 8px; + width: 160px; + outline: none; +} +.album-detail__region-input:focus { + border-color: var(--tv-accent, #c8905e); +} + +.album-detail__region-suggestions { + position: absolute; + top: 100%; + left: 0; + z-index: 10; + margin: 4px 0 0; + padding: 4px 0; + list-style: none; + background: var(--tv-surface, #1a1510); + border: 1px solid var(--tv-line-bright, rgba(232, 221, 208, 0.22)); + border-radius: 6px; + max-height: 180px; + overflow-y: auto; + min-width: 160px; + scrollbar-width: thin; +} + +.album-detail__region-suggestion { + display: block; + width: 100%; + padding: 6px 12px; + font-family: var(--tv-mono, 'SF Mono', 'Fira Code', monospace); + font-size: 11px; + color: var(--tv-text, #e8ddd0); + background: none; + border: none; + cursor: pointer; + text-align: left; + transition: background 0.15s; +} +.album-detail__region-suggestion:hover { + background: var(--tv-line, rgba(232, 221, 208, 0.1)); +} + +.album-detail__region-cancel { + width: 22px; + height: 22px; + border-radius: 50%; + border: none; + background: var(--tv-line, rgba(232, 221, 208, 0.1)); + color: var(--tv-muted, rgba(232, 221, 208, 0.45)); + font-size: 10px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background 0.15s; +} +.album-detail__region-cancel:hover { + background: var(--tv-line-bright, rgba(232, 221, 208, 0.22)); +} + .album-detail__count { font-family: var(--tv-mono, 'SF Mono', 'Fira Code', monospace); font-size: 11px; diff --git a/src/pages/travel/AlbumDetail.jsx b/src/pages/travel/AlbumDetail.jsx index cd5cac5..1fd1244 100644 --- a/src/pages/travel/AlbumDetail.jsx +++ b/src/pages/travel/AlbumDetail.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import SwipeableView from '../../components/SwipeableView'; import PullToRefresh from '../../components/PullToRefresh'; import MasonryGrid from './MasonryGrid'; @@ -30,6 +30,7 @@ export default function AlbumDetail({ onLoadMore, onReload, onCoverChange, + regions, }) { const isMobile = useIsMobile(); @@ -39,6 +40,79 @@ export default function AlbumDetail({ const [lightboxRect, setLightboxRect] = useState(null); const closingRef = useRef(false); + /* ── Region editing ── */ + const [editingRegion, setEditingRegion] = useState(false); + const [regionInput, setRegionInput] = useState(''); + const [regionSaving, setRegionSaving] = useState(false); + const [regionMsg, setRegionMsg] = useState(null); // { type: 'ok'|'err', text } + const regionInputRef = useRef(null); + const regionMsgTimer = useRef(null); + + const regionNames = useMemo(() => { + if (!regions?.features) return []; + return regions.features + .map((f) => f.properties?.name || f.properties?.id || '') + .filter(Boolean); + }, [regions]); + + const filteredSuggestions = useMemo(() => { + if (!regionInput.trim()) return regionNames; + const q = regionInput.toLowerCase(); + return regionNames.filter((n) => n.toLowerCase().includes(q)); + }, [regionInput, regionNames]); + + const handleRegionEditStart = useCallback(() => { + setEditingRegion(true); + setRegionInput(album?.regionName || ''); + setRegionMsg(null); + setTimeout(() => regionInputRef.current?.focus(), 50); + }, [album]); + + const handleRegionEditCancel = useCallback(() => { + setEditingRegion(false); + setRegionInput(''); + setRegionMsg(null); + }, []); + + const handleRegionSave = useCallback(async (value) => { + const name = (value ?? regionInput).trim(); + if (!name || !album?.name) return; + if (name === album.regionName) { + setEditingRegion(false); + return; + } + setRegionSaving(true); + try { + const res = await fetch( + `/api/travel/albums/${encodeURIComponent(album.name)}/region`, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ region: name }), + }, + ); + if (!res.ok) throw new Error(`${res.status}`); + setRegionMsg({ type: 'ok', text: `→ ${name}` }); + setEditingRegion(false); + onCoverChange?.(); // refresh album list + } catch { + setRegionMsg({ type: 'err', text: '변경 실패' }); + } finally { + setRegionSaving(false); + } + if (regionMsgTimer.current) clearTimeout(regionMsgTimer.current); + regionMsgTimer.current = setTimeout(() => setRegionMsg(null), 3000); + }, [regionInput, album, onCoverChange]); + + const handleRegionKeyDown = useCallback((e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleRegionSave(); + } else if (e.key === 'Escape') { + handleRegionEditCancel(); + } + }, [handleRegionSave, handleRegionEditCancel]); + // Enter → open useEffect(() => { if (prefersReduced()) { @@ -62,13 +136,13 @@ export default function AlbumDetail({ /* ── ESC key (close album when lightbox not open) ── */ useEffect(() => { const handler = (e) => { - if (e.key === 'Escape' && selectedPhotoIndex == null) { + if (e.key === 'Escape' && selectedPhotoIndex == null && !editingRegion) { handleClose(); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [selectedPhotoIndex]); // eslint-disable-line react-hooks/exhaustive-deps + }, [selectedPhotoIndex, editingRegion]); // eslint-disable-line react-hooks/exhaustive-deps /* ── Close with exit animation ── */ const handleClose = useCallback(() => { @@ -183,8 +257,57 @@ export default function AlbumDetail({
{album?.name || ''} - {album?.regionName && ( - {album.regionName} + + {/* Region label / editor */} + {editingRegion ? ( +
+ setRegionInput(e.target.value)} + onKeyDown={handleRegionKeyDown} + placeholder="지역명 입력…" + disabled={regionSaving} + /> + {filteredSuggestions.length > 0 && ( + + )} + +
+ ) : ( + )}
diff --git a/src/pages/travel/Travel.jsx b/src/pages/travel/Travel.jsx index 91d72bb..8136826 100644 --- a/src/pages/travel/Travel.jsx +++ b/src/pages/travel/Travel.jsx @@ -222,6 +222,7 @@ const Travel = () => { onLoadMore={handleLoadMore} onReload={handleReload} onCoverChange={refreshAlbums} + regions={regions} /> )}