diff --git a/app/globals.css b/app/globals.css index 4424318..09468ea 100644 --- a/app/globals.css +++ b/app/globals.css @@ -79,16 +79,6 @@ --ring: oklch(0.708 0 0); } -@layer base { -} - -/* 부드러운 무한 스크롤 애니메이션 */ -@layer utilities { - .animate-slide-smooth { - animation: slide-smooth 30s linear infinite; - } -} - @keyframes slide-smooth { 0% { transform: translateX(0); @@ -97,3 +87,12 @@ transform: translateX(-50%); } } + +.animate-slide-smooth { + animation: slide-smooth 30s linear infinite; + will-change: transform; +} + +.animate-slide-smooth:hover { + animation-play-state: paused; +} diff --git a/app/join/[id]/page.tsx b/app/join/[id]/page.tsx index 41dbf3a..bbda034 100644 --- a/app/join/[id]/page.tsx +++ b/app/join/[id]/page.tsx @@ -2,7 +2,7 @@ import { useRouter, useParams } from 'next/navigation'; import { useState } from 'react'; -import { useParticipantEnter } from '@/hooks/api/useParticipant'; +import { useEnterParticipant } from '@/hooks/api/mutation/useEnterParticipant'; import { useToast } from '@/hooks/useToast'; import Toast from '@/components/ui/toast'; @@ -14,7 +14,7 @@ export default function Page() { const [isRemembered, setIsRemembered] = useState(true); const [errorMessage, setErrorMessage] = useState(''); const router = useRouter(); - const participantEnter = useParticipantEnter(); + const participantEnter = useEnterParticipant(); const { isVisible, show } = useToast(); // 이름/비번 유효성 검사 (입력값이 있을 때만 버튼 활성화) diff --git a/app/layout.tsx b/app/layout.tsx index 69d12e3..f4273a8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -24,8 +24,9 @@ const pretendard = localFont({ }); export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', + title: '밍글링 - 어디서 만날지, 고민 시간을 줄여드려요', + description: + '퇴근 후 모임, 주말 약속까지. 서울 어디서든 모두가 비슷하게 도착하는 마법의 장소를 찾아드려요.', }; export default function RootLayout({ diff --git a/app/meeting/[id]/page.tsx b/app/meeting/[id]/page.tsx index feb698d..c282d22 100644 --- a/app/meeting/[id]/page.tsx +++ b/app/meeting/[id]/page.tsx @@ -1,19 +1,30 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import Image from 'next/image'; import KakaoMap from '@/components/map/kakaoMap'; import StationSearch from '@/components/meeting/stationSearch'; import { useOpenModal } from '@/hooks/useOpenModal'; -import { MOCK_PARTICIPANTS } from '@/mock/mockData'; -import StationData from '@/database/stations_info.json'; import { useParams, useRouter } from 'next/navigation'; +import { useSetDeparture } from '@/hooks/api/mutation/useSetDeparture'; +import { useCheckMeeting } from '@/hooks/api/query/useCheckMeeting'; // [추가] 조회 훅 +import StationDataRaw from '@/database/stations_info.json'; +import { getRandomHexColor } from '@/lib/color'; + +// 로컬 데이터 타입 정의 +interface StationInfo { + line: string; + name: string; + latitude: number; + longitude: number; +} -const STATION_DATA = StationData; +const STATION_DATA = StationDataRaw as StationInfo[]; export default function Page() { // 선택된 역 이름 상태 관리 const [selectedStation, setSelectedStation] = useState(null); + // 내 이름 관리 (로컬 스토리지에서 가져옴) const params = useParams(); const id = params?.id as string; @@ -21,35 +32,86 @@ export default function Page() { const openModal = useOpenModal(); const router = useRouter(); - // 1. [좌표 찾기] 선택된 역 이름으로 MOCK 데이터에서 정보 찾기 - const selectedStationInfo = STATION_DATA.find((station) => station.name === selectedStation); - - // 2. [내 참가자 객체 생성] 선택된 역이 있을 때만 생성 - const myParticipant = selectedStation - ? { - id: 'me', // 고유 ID (문자열) - name: '나', - station: selectedStation, - line: '2호선', - // 정보가 없으면 서울시청 좌표를 기본값으로 사용 (예외 처리) - latitude: selectedStationInfo?.latitude || 37.5665, - longitude: selectedStationInfo?.longitude || 126.978, - status: 'done', - hexColor: '#000000', - } - : null; - - // 3. [최종 리스트 병합] 내가 있으면 맨 앞에 추가, 없으면 기존 리스트만 사용 - const allParticipants = myParticipant ? [myParticipant, ...MOCK_PARTICIPANTS] : MOCK_PARTICIPANTS; + // [API Hook] 모임 정보 조회 & 출발지 등록 + const { data: meetingData } = useCheckMeeting(id); + const { mutate: setDeparture } = useSetDeparture(id); + const [myName] = useState(() => { + if (typeof window === 'undefined') return ''; + return localStorage.getItem('userId') || sessionStorage.getItem('userId') || ''; + }); + + // 2. 역 선택 시 실행될 로직 (상태 변경 + API 전송) + const handleSelectStation = (stationName: string | null) => { + // 화면 UI 즉시 업데이트 + setSelectedStation(stationName); + + if (!stationName) return; + + const stationInfo = STATION_DATA.find((s) => s.name === stationName); + + if (stationInfo) { + // API 전송 (백엔드 스펙: { departure: "역이름" }) + setDeparture({ + departure: stationInfo.name, + }); + } else { + console.error('역 정보를 찾을 수 없습니다.'); + } + }; + + // 3. '나' 객체 생성 (로컬 데이터 기반 즉시 반영) + const myParticipant = useMemo(() => { + if (!selectedStation) return null; + + // 로컬 JSON에서 좌표 즉시 조회 (API 응답 대기 X -> 속도 UP) + const info = STATION_DATA.find((s) => s.name === selectedStation); + if (!info) return null; + + return { + id: 'me', + name: myName || '나', // 로컬 스토리지 이름 사용 + station: info.name, + line: info.line, + latitude: info.latitude, + longitude: info.longitude, + status: 'done', + hexColor: '#000000', // 나는 검정색 고정 + }; + }, [selectedStation, myName]); + + // 4. [최종 리스트 병합] 서버 데이터(남) + 로컬 데이터(나) + const allParticipants = useMemo(() => { + // 서버에서 받은 '이미 등록한 참가자들' + const serverParticipants = meetingData?.data.participants || []; + + // 지도 표시용 포맷으로 변환 + const others = serverParticipants + .filter((p) => p.userName !== myName) // 혹시 모를 중복 방지 (내 이름 제외) + .map((p, index) => { + const stationInfo = STATION_DATA.find((s) => s.name === p.stationName); + return { + id: `other-${index}`, + name: p.userName, + station: p.stationName, + line: stationInfo?.line ?? '미확인', + latitude: p.latitude, + longitude: p.longitude, + status: 'done', + hexColor: getRandomHexColor(p.userName || p.stationName || `user-${index}`), + }; + }); + + // 내가 선택했으면 [나, ...다른사람들], 아니면 [...다른사람들] + return myParticipant ? [myParticipant, ...others] : others; + }, [meetingData, myParticipant, myName]); const handleSubmit = () => { if (!selectedStation) { - alert('출발지를 선택해주세요!'); + alert('출발지를 먼저 선택해주세요!'); return; } - - console.log('결과 요청:', allParticipants); - router.push('/result'); + // 결과 페이지로 이동 + router.push(`/result/${id}`); }; return ( @@ -64,10 +126,12 @@ export default function Page() {

투표 마감 시간
+ {/* 마감 시간은 API 데이터가 있으면 그것을 활용하거나 기존대로 유지 */} 03: 45 남았습니다

- 아직 입력 안 한 모임원 2명 + {/* API 데이터로 '안 한 사람' 수 계산 */} + 아직 입력 안 한 모임원 {meetingData?.data.pendingParticipantCount ?? 0}명

{/* 출발지 컴포넌트: 리스트 렌더링 */} @@ -130,7 +202,7 @@ export default function Page() {
@@ -142,12 +214,13 @@ export default function Page() { > {user.name}
- 안가연 + {user.name} ))} +