From c73e05df14584eb66ed51ee3398f54d4536ca782 Mon Sep 17 00:00:00 2001 From: Kangdy Date: Tue, 3 Feb 2026 22:06:33 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EC=99=84=EB=A3=8C=20-=20=EC=8B=9C=EA=B0=84,=20?= =?UTF-8?q?=EC=9D=B8=EC=9B=90=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20UI=20=EA=B5=AC=EC=B6=95=20-=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/create/page.tsx | 10 +-- app/meeting/[id]/page.tsx | 85 ++++++++++------------- components/meeting/MeetingInfoSection.tsx | 84 ++++++++++++++++++++++ hooks/useCountdown.ts | 49 +++++++++++++ hooks/useIsLoggedIn.ts | 21 ++++++ 5 files changed, 196 insertions(+), 53 deletions(-) create mode 100644 components/meeting/MeetingInfoSection.tsx create mode 100644 hooks/useCountdown.ts create mode 100644 hooks/useIsLoggedIn.ts diff --git a/app/create/page.tsx b/app/create/page.tsx index 4a2cacf..5ca407e 100644 --- a/app/create/page.tsx +++ b/app/create/page.tsx @@ -41,7 +41,7 @@ export default function Page() { }; const handleIncreaseParticipants = () => { - if (!isParticipantUndecided) { + if (!isParticipantUndecided && participantCount < 10) { setParticipantCount((prev) => prev + 1); } }; @@ -53,7 +53,7 @@ export default function Page() { }; const handleIncreaseDeadline = () => { - if (!isDeadlineFlexible) { + if (!isDeadlineFlexible && deadlineDays < 180) { setDeadlineDays((prev) => prev + 1); } }; @@ -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, @@ -308,7 +308,7 @@ export default function Page() { - - + {meetingData?.data && ( + + )} {/* 모바일 전용 지도 영역 */} 참여현황 {/* 전체 인원 수 동적 반영 (API) */} - - {meetingData?.data.totalParticipantCount ?? 0}명 - - 이 참여 중 + {meetingData?.data.currentParticipantCount}명이 + 참여 중 diff --git a/components/meeting/MeetingInfoSection.tsx b/components/meeting/MeetingInfoSection.tsx new file mode 100644 index 0000000..1fad764 --- /dev/null +++ b/components/meeting/MeetingInfoSection.tsx @@ -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) => 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 ( +
+
+
+ {/* --- [타이틀 영역] --- */} +

+ {isTimeSet ? ( + // Case: 시간이 설정됨 (시간만 입력 or 둘 다 입력) + <> + 투표 마감 시간 +
+ {isExpired ? ( + 마감되었습니다 + ) : ( + <> + + {days > 0 && `${days}일 `} + {hours}시간 {minutes}분 + + {' 남았습니다'} + + )} + + ) : ( + // Case: 시간이 유연함 (참여자만 입력 or 둘 다 안 함) + '투표에 참여해주세요' + )} +

+ + {/* --- [인원 텍스트 영역] --- */} + {/* isCapacitySet이 false면 아예 렌더링되지 않음 */} + {isCapacitySet && ( +

+ 아직 입력 안 한 모임원 {pendingCount}명 +

+ )} +
+ + +
+
+ ); +} diff --git a/hooks/useCountdown.ts b/hooks/useCountdown.ts new file mode 100644 index 0000000..b82313c --- /dev/null +++ b/hooks/useCountdown.ts @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; + +export const useCountdown = (targetDateISO: string) => { + const [timeLeft, setTimeLeft] = useState({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isExpired: false, + }); + + useEffect(() => { + const calculateTimeLeft = () => { + 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, + }; + }; + + // 1. 먼저 한 번 계산해서 현재 상태를 업데이트합니다. + const initialTime = calculateTimeLeft(); + setTimeLeft(initialTime); + + // 2. 기한 없음 상태)면 타이머(setInterval)를 아예 시작하지 않고 여기서 끝냅니다. + if (initialTime.days >= 59) { + return; + } + + // 3. 59일 미만일 때만 1초마다 갱신합니다. + const timer = setInterval(() => { + setTimeLeft(calculateTimeLeft()); + }, 1000); + + return () => clearInterval(timer); + }, [targetDateISO]); + + return timeLeft; +}; diff --git a/hooks/useIsLoggedIn.ts b/hooks/useIsLoggedIn.ts new file mode 100644 index 0000000..7baf43e --- /dev/null +++ b/hooks/useIsLoggedIn.ts @@ -0,0 +1,21 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +export const useIsLoggedIn = () => { + const [isLogin, setIsLogin] = useState(false); + + useEffect(() => { + // 1. 쿠키에서 확인하는 경우 (가장 추천, 아까 쿠키 세팅 하셨으니까요!) + // 'accessToken'이나 'JSESSIONID' 등 실제 사용하는 쿠키 이름이 포함되어 있는지 확인 + const hasCookie = document.cookie.includes('accessToken'); + + // 2. 혹은 로컬스토리지/세션스토리지에서 확인하는 경우 + // const hasToken = !!localStorage.getItem('accessToken'); + // const hasSession = !!sessionStorage.getItem('accessToken'); + + setIsLogin(hasCookie); // 조건에 따라 true/false 설정 + }, []); + + return isLogin; +}; From f79490f5c2785a72b29627da67911753643afca3 Mon Sep 17 00:00:00 2001 From: Kangdy Date: Tue, 3 Feb 2026 22:21:47 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20lint=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks/useCountdown.ts | 66 ++++++++++++++++++++---------------------- hooks/useIsLoggedIn.ts | 28 +++++++++++++----- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/hooks/useCountdown.ts b/hooks/useCountdown.ts index b82313c..aa95d43 100644 --- a/hooks/useCountdown.ts +++ b/hooks/useCountdown.ts @@ -1,49 +1,45 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; + +// 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) => { - const [timeLeft, setTimeLeft] = useState({ - days: 0, - hours: 0, - minutes: 0, - seconds: 0, - isExpired: false, - }); + // 2. [수정] useState 안에서 함수를 호출하여 '초기값'을 바로 세팅합니다. + // 이렇게 하면 useEffect에서 setTimeLeft를 또 호출할 필요가 없습니다. + const [timeLeft, setTimeLeft] = useState(() => calculateTimeLeft(targetDateISO)); useEffect(() => { - const calculateTimeLeft = () => { - 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, - }; - }; - - // 1. 먼저 한 번 계산해서 현재 상태를 업데이트합니다. - const initialTime = calculateTimeLeft(); - setTimeLeft(initialTime); - - // 2. 기한 없음 상태)면 타이머(setInterval)를 아예 시작하지 않고 여기서 끝냅니다. - if (initialTime.days >= 59) { + // 3. 기한 없음(59일 이상) 체크를 여기서 수행 + // (초기값이 이미 계산되어 있으므로 timeLeft.days를 바로 확인 가능) + if (timeLeft.days >= 59) { return; } - // 3. 59일 미만일 때만 1초마다 갱신합니다. + // 만약 초기값 계산 시점과 mount 시점의 차이를 보정하고 싶다면 + // 여기서 한 번 더 계산할 수도 있지만, 위에서 초기화를 했으므로 바로 인터벌을 돌려도 됩니다. + const timer = setInterval(() => { - setTimeLeft(calculateTimeLeft()); + setTimeLeft(calculateTimeLeft(targetDateISO)); }, 1000); return () => clearInterval(timer); - }, [targetDateISO]); + }, [targetDateISO, timeLeft.days]); // timeLeft.days 의존성 추가 return timeLeft; }; diff --git a/hooks/useIsLoggedIn.ts b/hooks/useIsLoggedIn.ts index 7baf43e..bc5fe83 100644 --- a/hooks/useIsLoggedIn.ts +++ b/hooks/useIsLoggedIn.ts @@ -2,19 +2,31 @@ import { useState, useEffect } from 'react'; +// 커스텀 이벤트로 로그인 상태 변경 알림 +export const notifyLoginStateChange = () => { + window.dispatchEvent(new Event('loginStateChange')); +}; + export const useIsLoggedIn = () => { - const [isLogin, setIsLogin] = useState(false); + const [isLogin, setIsLogin] = useState(() => { + // 초기값을 lazy initialization으로 설정 + if (typeof window !== 'undefined') { + return document.cookie.includes('accessToken'); + } + return false; + }); useEffect(() => { - // 1. 쿠키에서 확인하는 경우 (가장 추천, 아까 쿠키 세팅 하셨으니까요!) - // 'accessToken'이나 'JSESSIONID' 등 실제 사용하는 쿠키 이름이 포함되어 있는지 확인 - const hasCookie = document.cookie.includes('accessToken'); + const checkLoginState = () => { + setIsLogin(document.cookie.includes('accessToken')); + }; - // 2. 혹은 로컬스토리지/세션스토리지에서 확인하는 경우 - // const hasToken = !!localStorage.getItem('accessToken'); - // const hasSession = !!sessionStorage.getItem('accessToken'); + // 커스텀 이벤트 리스너 등록 + window.addEventListener('loginStateChange', checkLoginState); - setIsLogin(hasCookie); // 조건에 따라 true/false 설정 + return () => { + window.removeEventListener('loginStateChange', checkLoginState); + }; }, []); return isLogin;