fromis_9/frontend/src/components/pc/admin/schedule/LocationSearchDialog.jsx

180 lines
6.3 KiB
React
Raw Normal View History

/**
* 장소 검색 다이얼로그 컴포넌트
* - 카카오 장소 검색 API를 사용
*/
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Search, MapPin } from 'lucide-react';
import useAuthStore from '@/stores/useAuthStore';
/**
* @param {Object} props
* @param {boolean} props.isOpen - 다이얼로그 열림 여부
* @param {Function} props.onClose - 닫기 핸들러
* @param {Function} props.onSelect - 장소 선택 핸들러 (place 객체 전달)
*/
function LocationSearchDialog({ isOpen, onClose, onSelect }) {
const [searchQuery, setSearchQuery] = useState('');
const [results, setResults] = useState([]);
const [searching, setSearching] = useState(false);
// 다이얼로그 닫기 시 상태 초기화
const handleClose = () => {
setSearchQuery('');
setResults([]);
onClose();
};
// 카카오 장소 검색 API 호출
const handleSearch = async () => {
if (!searchQuery.trim()) {
setResults([]);
return;
}
setSearching(true);
try {
const token = useAuthStore.getState().token;
const response = await fetch(`/api/admin/kakao/places?query=${encodeURIComponent(searchQuery)}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setResults(data.documents || []);
}
} catch (error) {
console.error('장소 검색 오류:', error);
} finally {
setSearching(false);
}
};
// 장소 선택
const handleSelectPlace = (place) => {
onSelect({
name: place.place_name,
address: place.road_address_name || place.address_name,
lat: parseFloat(place.y),
lng: parseFloat(place.x),
});
handleClose();
};
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={handleClose}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-white rounded-2xl p-6 max-w-lg w-full mx-4 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-gray-900">장소 검색</h3>
<button
type="button"
onClick={handleClose}
className="text-gray-400 hover:text-gray-600"
>
<X size={20} />
</button>
</div>
{/* 검색 입력 */}
<div className="flex gap-2 mb-4">
<div className="flex-1 relative">
<Search size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSearch();
}
}}
placeholder="장소명을 입력하세요"
className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
autoFocus
/>
</div>
<button
type="button"
onClick={handleSearch}
disabled={searching}
className="px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-colors disabled:opacity-50"
>
{searching ? (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: 'linear',
}}
>
<Search size={18} />
</motion.div>
) : (
'검색'
)}
</button>
</div>
{/* 검색 결과 */}
<div className="max-h-80 overflow-y-auto pr-2">
{results.length > 0 ? (
<div className="space-y-2">
{results.map((place, index) => (
<button
key={index}
type="button"
onClick={() => handleSelectPlace(place)}
className="w-full p-3 text-left hover:bg-gray-50 rounded-xl flex items-start gap-3 border border-gray-100"
>
<MapPin size={18} className="text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="font-medium text-gray-900">{place.place_name}</p>
<p className="text-sm text-gray-500 truncate">
{place.road_address_name || place.address_name}
</p>
{place.category_name && (
<p className="text-xs text-gray-400 mt-1">{place.category_name}</p>
)}
</div>
</button>
))}
</div>
) : searchQuery && !searching ? (
<div className="text-center py-8 text-gray-500">
<MapPin size={32} className="mx-auto mb-2 text-gray-300" />
<p>검색어를 입력하고 검색 버튼을 눌러주세요</p>
</div>
) : (
<div className="text-center py-8 text-gray-500">
<MapPin size={32} className="mx-auto mb-2 text-gray-300" />
<p>장소명을 입력하고 검색해주세요</p>
</div>
)}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
export default LocationSearchDialog;