Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions app/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function Page() {
};

const handleIncreaseParticipants = () => {
if (!isParticipantUndecided) {
if (!isParticipantUndecided && participantCount < 10) {
setParticipantCount((prev) => prev + 1);
}
};
Expand All @@ -53,7 +53,7 @@ export default function Page() {
};

const handleIncreaseDeadline = () => {
if (!isDeadlineFlexible) {
if (!isDeadlineFlexible && deadlineDays < 180) {
setDeadlineDays((prev) => prev + 1);
}
};
Expand Down Expand Up @@ -124,7 +124,7 @@ export default function Page() {
const purposes = getPurposes();

// capacity 처리: "아직 안정해졌어요" 체크 시 30으로 설정
const capacity = isParticipantUndecided ? 30 : participantCount || 1;
const capacity = isParticipantUndecided ? 10 : participantCount || 1;

const requestData: MeetingCreateRequest = {
meetingName,
Expand Down Expand Up @@ -308,7 +308,7 @@ export default function Page() {
<button
type="button"
onClick={handleIncreaseParticipants}
disabled={isParticipantUndecided}
disabled={isParticipantUndecided || participantCount >= 10}
className="bg-gray-1 absolute -top-px -right-px flex h-11 w-11 items-center justify-center rounded-tr-lg rounded-br-lg disabled:opacity-50 sm:h-11 sm:w-11"
>
<Image src="/icon/plus.svg" alt="plus" width={20} height={20} />
Expand Down Expand Up @@ -357,7 +357,7 @@ export default function Page() {
<button
type="button"
onClick={handleIncreaseDeadline}
disabled={isDeadlineFlexible}
disabled={isDeadlineFlexible || deadlineDays >= 180}
className="bg-gray-1 absolute -top-px -right-px flex h-15 w-11 items-center justify-center rounded-tr-lg rounded-br-lg disabled:opacity-50 sm:h-15 sm:w-11"
>
<Image src="/icon/plus.svg" alt="plus" width={20} height={20} />
Expand Down
85 changes: 37 additions & 48 deletions app/meeting/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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';
import MeetingInfoSection from '@/components/meeting/MeetingInfoSection';

// 로컬 데이터 타입 정의
interface StationInfo {
Expand All @@ -36,9 +37,14 @@ export default function Page() {
const { data: meetingData } = useCheckMeeting(id);
const { mutate: setDeparture } = useSetDeparture(id);
const [myName] = useState<string>(() => {
if (typeof window === 'undefined') return '';
return localStorage.getItem('userId') || sessionStorage.getItem('userId') || '';
});
if (typeof window === 'undefined') return '';
return localStorage.getItem('userId') || sessionStorage.getItem('userId') || '';
});

const handleShareClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// 'SHARE' 타입의 모달을 열면서 meetingId를 전달
openModal('SHARE', { meetingId: id }, e);
};

// 2. 역 선택 시 실행될 로직 (상태 변경 + API 전송)
const handleSelectStation = (stationName: string | null) => {
Expand Down Expand Up @@ -85,25 +91,25 @@ export default function Page() {
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}`),
};
});
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]);
}, [meetingData, myParticipant, myName]);

const handleSubmit = () => {
if (!selectedStation) {
Expand All @@ -119,31 +125,16 @@ export default function Page() {
<div className="flex h-full w-full flex-col bg-white md:h-175 md:w-174 md:flex-row md:gap-4 md:rounded-xl lg:w-215">
{/* [LEFT PANEL] 데스크탑 전용 정보 영역 */}
<section className="border-gray-1 flex w-full flex-col gap-5 bg-white md:w-77.5 md:gap-10">
{/* 타이머 섹션 */}
<div className="px-5 pt-10 md:p-0">
<div className="flex items-start justify-between">
<div className="text-[22px] leading-[1.364] font-semibold tracking-[-1.948%]">
<h2 className="text-gray-9">
투표 마감 시간
<br />
{/* 마감 시간은 API 데이터가 있으면 그것을 활용하거나 기존대로 유지 */}
<span className="text-blue-5">03: 45</span> 남았습니다
</h2>
<p className="text-gray-5 mt-2 text-[15px] font-normal">
{/* API 데이터로 '안 한 사람' 수 계산 */}
아직 입력 안 한 모임원 {meetingData?.data.pendingParticipantCount ?? 0}명
</p>
</div>
<button
className="text-blue-5 bg-blue-1 hover:bg-blue-2 flex h-6 w-30 cursor-pointer items-center gap-0.5 rounded px-3 py-1.5 text-[11px] font-semibold transition-colors"
type="button"
onClick={(e) => openModal('SHARE', { meetingId: id }, e)}
>
<Image src="/icon/share.svg" alt="공유 아이콘" width={12} height={12} />
참여 링크 공유하기
</button>
</div>
</div>
{meetingData?.data && (
<MeetingInfoSection
deadline={meetingData.data.deadlineAt} // "2026-01-23T18:00:00"
totalCapacity={meetingData.data.totalParticipantCount} // 10
currentParticipants={meetingData.data.currentParticipantCount} // 2
isDeadlineFlexible={false}
isParticipantUndecided={false}
onShare={handleShareClick}
/>
Comment on lines +128 to +136
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for MeetingStatusData type and related fields
rg -n "MeetingStatusData|deadlineFlexible|participantUndecided" --type=ts

Repository: SWYP-mingling/Frontend

Length of output: 202


🏁 Script executed:

# Also explore the file structure to understand the context better
fd --type f -e "tsx" -e "ts" | grep -E "(type|api|meeting)" | head -20

Repository: SWYP-mingling/Frontend

Length of output: 477


🏁 Script executed:

# Check the specific file mentioned in the review
cat -n app/meeting/[id]/page.tsx | sed -n '120,145p'

Repository: SWYP-mingling/Frontend

Length of output: 1337


🏁 Script executed:

# View the MeetingStatusData interface definition
cat -n types/api.ts | sed -n '55,75p'

Repository: SWYP-mingling/Frontend

Length of output: 498


🏁 Script executed:

# Search more broadly for any flexible or undecided related fields
rg -n "flexible|Flexible|undecided|Undecided" --type=ts

Repository: SWYP-mingling/Frontend

Length of output: 2668


🏁 Script executed:

# Check the MeetingInfoSection component to understand what props it expects
cat -n components/meeting/MeetingInfoSection.tsx | head -50

Repository: SWYP-mingling/Frontend

Length of output: 2007


API 응답에서 isDeadlineFlexibleisParticipantUndecided 필드 누락

현재 MeetingStatusData 타입에는 isDeadlineFlexibleisParticipantUndecided 필드가 포함되어 있지 않습니다. 이 값들은 모임 생성 시(app/create/page.tsx)에는 상태로 관리되지만, API 응답에는 반영되지 않고 있습니다.

모임의 의미 있는 속성이므로 API에서 이 필드들을 반환하도록 업데이트하고, 그 후 여기서 동적으로 전달하는 것이 좋습니다.

🤖 Prompt for AI Agents
In `@app/meeting/`[id]/page.tsx around lines 128 - 136, The API response and
MeetingStatusData type are missing the boolean fields isDeadlineFlexible and
isParticipantUndecided, so update the backend API to return these two fields and
extend the MeetingStatusData type/interface to include them, then pass those
properties dynamically into the MeetingInfoSection (currently called with
hardcoded false values) — locate the MeetingStatusData type definition and the
API response mapping used to build meetingData, add isDeadlineFlexible and
isParticipantUndecided there, and replace the hardcoded props in the page
component (where MeetingInfoSection is rendered) to use
meetingData.data.isDeadlineFlexible and meetingData.data.isParticipantUndecided;
also ensure app/create/page.tsx uses the same field names so creation and read
models align.

)}

{/* 모바일 전용 지도 영역 */}
<KakaoMap
Expand All @@ -167,10 +158,8 @@ export default function Page() {
<h3 className="text-gray-9 text-xl font-semibold">참여현황</h3>
<span className="text-gray-6 text-normal text-xs">
{/* 전체 인원 수 동적 반영 (API) */}
<span className="text-blue-5">
{meetingData?.data.totalParticipantCount ?? 0}명
</span>
이 참여 중
<span className="text-blue-5">{meetingData?.data.currentParticipantCount}명</span>이
참여 중
</span>
</div>

Expand Down
84 changes: 84 additions & 0 deletions components/meeting/MeetingInfoSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use client';

import Image from 'next/image';
import { useCountdown } from '@/hooks/useCountdown';

interface MeetingInfoProps {
deadline: string;
isDeadlineFlexible?: boolean;
totalCapacity: number;
currentParticipants: number;
isParticipantUndecided?: boolean;
onShare: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

export default function MeetingInfoSection({
deadline,
isDeadlineFlexible = false,
totalCapacity,
currentParticipants,
isParticipantUndecided = false,
onShare,
}: MeetingInfoProps) {
const { days, hours, minutes, isExpired } = useCountdown(deadline);

// 남은 인원 (음수 방지)
const pendingCount = Math.max(0, totalCapacity - currentParticipants);

// 1. 시간 렌더링 여부: (기한 유연 아님) AND (59일 미만)
const isTimeSet = !isDeadlineFlexible && days < 59;

// 2. 인원 렌더링 여부: (인원 미정 아님) AND (남은 사람이 있음)
// 👉 !isParticipantUndecided 덕분에 "인원 선택 안 함" 상태면 false가 되어 숨겨집니다.
const isCapacitySet = !isParticipantUndecided && pendingCount > 0;

return (
<div className="px-5 pt-10 md:p-0">
<div className="flex items-start justify-between">
<div className="text-[22px] leading-[1.364] font-semibold tracking-[-1.948%] break-keep">
{/* --- [타이틀 영역] --- */}
<h2 className="text-gray-9">
{isTimeSet ? (
// Case: 시간이 설정됨 (시간만 입력 or 둘 다 입력)
<>
투표 마감 시간
<br />
{isExpired ? (
<span className="text-gray-400">마감되었습니다</span>
) : (
<>
<span className="text-blue-5">
{days > 0 && `${days}일 `}
{hours}시간 {minutes}분
</span>
{' 남았습니다'}
</>
)}
</>
) : (
// Case: 시간이 유연함 (참여자만 입력 or 둘 다 안 함)
'투표에 참여해주세요'
)}
</h2>

{/* --- [인원 텍스트 영역] --- */}
{/* isCapacitySet이 false면 아예 렌더링되지 않음 */}
{isCapacitySet && (
<p className="text-gray-5 mt-2 text-[15px] font-normal">
<span>아직 입력 안 한 모임원 {pendingCount}명</span>
</p>
)}
</div>

<button
className="text-blue-5 bg-blue-1 hover:bg-blue-2 flex h-6 w-fit shrink-0 cursor-pointer items-center gap-0.5 rounded px-3 py-1.5 text-[11px] font-semibold whitespace-nowrap transition-colors"
type="button"
onClick={onShare}
>
<Image src="/icon/share.svg" alt="공유 아이콘" width={12} height={12} />
참여 링크 공유하기
</button>
</div>
</div>
);
}
45 changes: 45 additions & 0 deletions hooks/useCountdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState, useEffect, useCallback } from 'react';

Check warning on line 1 in hooks/useCountdown.ts

View workflow job for this annotation

GitHub Actions / build

'useCallback' is defined but never used

Check warning on line 1 in hooks/useCountdown.ts

View workflow job for this annotation

GitHub Actions / build

'useCallback' is defined but never used
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

사용하지 않는 useCallback import 제거 필요

useCallback이 import되었지만 실제로 사용되지 않습니다.

🧹 수정 제안
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect } from 'react';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect } from 'react';
🧰 Tools
🪛 GitHub Check: build

[warning] 1-1:
'useCallback' is defined but never used

🤖 Prompt for AI Agents
In `@hooks/useCountdown.ts` at line 1, The import list in hooks/useCountdown.ts
includes an unused symbol useCallback; remove useCallback from the import
statement (leave useState and useEffect) and run lint/type-check to ensure no
other references to useCallback remain (look for any functions named
useCountdown or internal callbacks if present).


// 1. 계산 로직을 훅 내부(useEffect 밖) 또는 파일 최상단으로 분리
const calculateTimeLeft = (targetDateISO: string) => {
const now = new Date().getTime();
const target = new Date(targetDateISO).getTime();
const difference = target - now;

if (difference <= 0) {
return { days: 0, hours: 0, minutes: 0, seconds: 0, isExpired: true };
}

return {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
minutes: Math.floor((difference / 1000 / 60) % 60),
seconds: Math.floor((difference / 1000) % 60),
isExpired: false,
};
};

export const useCountdown = (targetDateISO: string) => {
// 2. [수정] useState 안에서 함수를 호출하여 '초기값'을 바로 세팅합니다.
// 이렇게 하면 useEffect에서 setTimeLeft를 또 호출할 필요가 없습니다.
const [timeLeft, setTimeLeft] = useState(() => calculateTimeLeft(targetDateISO));

useEffect(() => {
// 3. 기한 없음(59일 이상) 체크를 여기서 수행
// (초기값이 이미 계산되어 있으므로 timeLeft.days를 바로 확인 가능)
if (timeLeft.days >= 59) {
return;
}

// 만약 초기값 계산 시점과 mount 시점의 차이를 보정하고 싶다면
// 여기서 한 번 더 계산할 수도 있지만, 위에서 초기화를 했으므로 바로 인터벌을 돌려도 됩니다.

const timer = setInterval(() => {
setTimeLeft(calculateTimeLeft(targetDateISO));
}, 1000);

return () => clearInterval(timer);
}, [targetDateISO, timeLeft.days]); // timeLeft.days 의존성 추가

return timeLeft;
};
33 changes: 33 additions & 0 deletions hooks/useIsLoggedIn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import { useState, useEffect } from 'react';

// 커스텀 이벤트로 로그인 상태 변경 알림
export const notifyLoginStateChange = () => {
window.dispatchEvent(new Event('loginStateChange'));
};

export const useIsLoggedIn = () => {
const [isLogin, setIsLogin] = useState(() => {
// 초기값을 lazy initialization으로 설정
if (typeof window !== 'undefined') {
return document.cookie.includes('accessToken');
}
return false;
});

useEffect(() => {
const checkLoginState = () => {
setIsLogin(document.cookie.includes('accessToken'));
};

// 커스텀 이벤트 리스너 등록
window.addEventListener('loginStateChange', checkLoginState);

return () => {
window.removeEventListener('loginStateChange', checkLoginState);
};
}, []);

return isLogin;
};
Loading