From b83d4c9fcff6df47d208e90c4440b09a8e6a9fae Mon Sep 17 00:00:00 2001 From: nabbang6 Date: Wed, 29 Apr 2026 20:47:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=EC=B6=9C=EC=84=9D=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=99=95=EC=9D=B8=20=EB=B2=84=ED=8A=BC=EC=9D=B4=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=EC=A7=84=EC=97=90=EA=B2=8C=EB=A7=8C=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C=EB=90=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/attendance/AttendanceTodayCard.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/attendance/AttendanceTodayCard.tsx b/src/components/attendance/AttendanceTodayCard.tsx index e8cbe606..28f2b03a 100644 --- a/src/components/attendance/AttendanceTodayCard.tsx +++ b/src/components/attendance/AttendanceTodayCard.tsx @@ -7,7 +7,7 @@ import { useState } from 'react'; import { CompleteIcon, RushIcon } from '@/assets/icons'; import { Card } from '@/components/ui'; import { AttendanceCodeModal } from '@/components/attendance/AttendanceCodeModal'; -import { toastError } from '@/stores/useToastStore'; +import { useIsAdmin } from '@/hooks/shared'; interface AttendanceTodayCardProps { overline: string; @@ -55,13 +55,10 @@ function AttendanceTodayCard({ }: AttendanceTodayCardProps) { const router = useRouter(); const { clubId } = useParams<{ clubId: string }>(); + const { isAdmin: isAdminUser } = useIsAdmin(); const [codeModalOpen, setCodeModalOpen] = useState(false); function handleSecondaryClick() { - if (!isAdmin) { - toastError('운영진만 사용할 수 있는 기능입니다.'); - return; - } if (sessionId == null) return; router.push(`/${clubId}/attendance/qr?sessionId=${sessionId}`); } @@ -77,9 +74,11 @@ function AttendanceTodayCard({ onPrimaryClick={() => setCodeModalOpen(true)} primaryButtonText={isChecked ? '출석 완료' : '출석하기'} primaryButtonDisabled={disabled || isChecked} - onSecondaryClick={handleSecondaryClick} - secondaryButtonText="출석코드 확인" - secondaryButtonDisabled={disabled || sessionId == null} + {...(isAdminUser && { + onSecondaryClick: handleSecondaryClick, + secondaryButtonText: '출석코드 확인', + secondaryButtonDisabled: disabled || sessionId == null, + })} > {isChecked ? ( Date: Wed, 29 Apr 2026 20:49:03 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EC=B6=9C=EC=84=9D=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=EC=8B=9C=EA=B0=84=20SSE=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A3=BC=EC=84=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attendance/AttendanceCodeModal.tsx | 63 ++++++++++--------- .../attendance/AttendanceQRContent.tsx | 15 +++-- src/hooks/attendance/useCheckIn.ts | 26 ++++---- 3 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/components/attendance/AttendanceCodeModal.tsx b/src/components/attendance/AttendanceCodeModal.tsx index 3c2229b2..ea9ea04f 100644 --- a/src/components/attendance/AttendanceCodeModal.tsx +++ b/src/components/attendance/AttendanceCodeModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; +import { useState } from 'react'; import { CheckRoundIcon } from '@/assets/icons'; import { @@ -13,10 +13,12 @@ import { Icon, } from '@/components/ui'; import { InputOTP } from '@/components/attendance/InputOTP'; -import { useAttendanceSSE } from '@/hooks/attendance'; -import { useRemainingTime } from '@/hooks'; +// TODO: SSE 연결 안정화 후 복원 +// import { useEffect, useRef } from 'react'; +// import { useAttendanceSSE } from '@/hooks/attendance'; +// import { useRemainingTime } from '@/hooks'; +// import { toastError } from '@/stores/useToastStore'; import { formatModalDescription } from '@/lib/formatTime'; -import { toastError } from '@/stores/useToastStore'; interface AttendanceCodeModalProps { open: boolean; @@ -36,36 +38,39 @@ function AttendanceCodeModal({ location, }: AttendanceCodeModalProps) { const [code, setCode] = useState(''); - const { status, expiredAt: sseExpiredAt } = useAttendanceSSE(); - const isLoading = status === null; - const { minutes, seconds, isExpired } = useRemainingTime(sseExpiredAt ?? ''); + // TODO: SSE 연결 안정화 후 복원 + // const { status, expiredAt: sseExpiredAt } = useAttendanceSSE(); + // const isLoading = status === null; + // const { minutes, seconds, isExpired } = useRemainingTime(sseExpiredAt ?? ''); const isComplete = code.length === 6; const description = formatModalDescription(start, location); - const hasShownRef = useRef(false); + // TODO: SSE 연결 안정화 후 복원 + // const hasShownRef = useRef(false); function handleOpenChange(nextOpen: boolean) { if (!nextOpen) setCode(''); onOpenChange(nextOpen); } - useEffect(() => { - if (!open) return; - if (status === null) return; - - if ((status === 'qr-none' || status === 'qr-close') && !hasShownRef.current) { - hasShownRef.current = true; - toastError('현재 출석이 진행 중이 아닙니다.'); - - onOpenChange(false); - } - }, [open, status, onOpenChange]); - - useEffect(() => { - if (!open) { - hasShownRef.current = false; - } - }, [open]); + // TODO: SSE 연결 안정화 후 복원 + // useEffect(() => { + // if (!open) return; + // if (status === null) return; + // + // if ((status === 'qr-none' || status === 'qr-close') && !hasShownRef.current) { + // hasShownRef.current = true; + // toastError('현재 출석이 진행 중이 아닙니다.'); + // + // onOpenChange(false); + // } + // }, [open, status, onOpenChange]); + + // useEffect(() => { + // if (!open) { + // hasShownRef.current = false; + // } + // }, [open]); return ( @@ -96,7 +101,7 @@ function AttendanceCodeModal({ - {isLoading ? ( + {/* {isLoading ? (

출석 정보를 불러오는 중...

@@ -111,7 +116,9 @@ function AttendanceCodeModal({

출석 가능 시간이 만료되었습니다

- )} + )} */} + + {/* TODO: SSE 연결 안정화 후 출석 가능 시간 표시 복원 */} { onConfirm?.(code); handleOpenChange(false); diff --git a/src/components/attendance/AttendanceQRContent.tsx b/src/components/attendance/AttendanceQRContent.tsx index d322a6bf..5a9c5682 100644 --- a/src/components/attendance/AttendanceQRContent.tsx +++ b/src/components/attendance/AttendanceQRContent.tsx @@ -11,7 +11,9 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui'; -import { useAttendanceSSE, useAttendanceQR } from '@/hooks/attendance'; +// TODO: SSE 연결 안정화 후 복원 +// import { useAttendanceSSE } from '@/hooks/attendance'; +import { useAttendanceQR } from '@/hooks/attendance'; import { useRemainingTime } from '@/hooks/useRemainingTime'; import { useClubId } from '@/stores/useClubStore'; @@ -23,8 +25,9 @@ function AttendanceQRContent({ sessionId }: AttendanceQRContentProps) { const { clubId: clubIdParam } = useParams<{ clubId: string }>(); const clubId = useClubId(); const { qrRef, qrData, isLoading } = useAttendanceQR(clubId, sessionId); - const { expiredAt: sseExpiredAt } = useAttendanceSSE(); - const { minutes, seconds, isExpired } = useRemainingTime(sseExpiredAt ?? ''); + // TODO: SSE 연결 안정화 후 복원 + // const { expiredAt: sseExpiredAt } = useAttendanceSSE(); + const { minutes, seconds, isExpired } = useRemainingTime(qrData?.expiredAt ?? ''); return (
@@ -73,7 +76,11 @@ function AttendanceQRContent({ sessionId }: AttendanceQRContentProps) {
출석 가능 시간 - {!sseExpiredAt ? '연결 중...' : isExpired ? '마감' : `${minutes}:${seconds}`} + {!qrData?.expiredAt + ? '로딩 중...' + : isExpired + ? '마감' + : `${minutes}:${seconds}`}

QR코드는 모바일만 제공하고 있어요.

diff --git a/src/hooks/attendance/useCheckIn.ts b/src/hooks/attendance/useCheckIn.ts index 6c38a5ed..04b8fb85 100644 --- a/src/hooks/attendance/useCheckIn.ts +++ b/src/hooks/attendance/useCheckIn.ts @@ -6,7 +6,8 @@ import { ATTENDANCE_ERROR_MESSAGE } from '@/constants/attendance'; import { useClubId } from '@/stores/useClubStore'; import { toastError } from '@/stores/useToastStore'; import { useAttendanceQuery } from '@/hooks/attendance/useAttendanceQuery'; -import { useAttendanceSSE } from '@/hooks/attendance/useAttendanceSSE'; +// TODO: SSE 연결 안정화 후 복원 +// import { useAttendanceSSE } from '@/hooks/attendance/useAttendanceSSE'; interface UseCheckInOptions { sessionId?: number | null; @@ -18,21 +19,23 @@ export function useCheckIn(options?: UseCheckInOptions) { const clubId = useClubId(); const { data } = useAttendanceQuery(); const { data: profileStatus, isLoading: isProfileLoading } = useProfileStatusQuery(); - const { status: qrStatus } = useAttendanceSSE(); + // TODO: SSE 연결 안정화 후 복원 + // const { status: qrStatus } = useAttendanceSSE(); const [codeModalOpen, setCodeModalOpen] = useState(false); const [cardinalModalOpen, setCardinalModalOpen] = useState(false); const [checkedSessionId, setCheckedSessionId] = useState(null); const [checkInError, setCheckInError] = useState(false); function openCodeModal() { - if (qrStatus === 'qr-none') { - toastError('아직 출석 코드가 생성되지 않았습니다.'); - return; - } - if (qrStatus === 'qr-close') { - toastError('QR 코드가 만료되었거나 존재하지 않습니다.'); - return; - } + // TODO: SSE 연결 안정화 후 복원 + // if (qrStatus === 'qr-none') { + // toastError('아직 출석 코드가 생성되지 않았습니다.'); + // return; + // } + // if (qrStatus === 'qr-close') { + // toastError('QR 코드가 만료되었거나 존재하지 않습니다.'); + // return; + // } setCodeModalOpen(true); } @@ -70,7 +73,8 @@ export function useCheckIn(options?: UseCheckInOptions) { return { isChecked, checkInError, - qrStatus, + // TODO: SSE 연결 안정화 후 복원 + // qrStatus, codeModalOpen, setCodeModalOpen, openCodeModal, From 03e21b03c8a119414ea5cfe90f9cb6e4ddb59ddc Mon Sep 17 00:00:00 2001 From: nabbang6 Date: Wed, 29 Apr 2026 21:12:36 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20location=EC=9D=B4=20null?= =?UTF-8?q?=EC=9D=BC=20=EB=95=8C=20=EC=9E=A5=EC=86=8C=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20=EC=83=9D=EB=9E=B5=ED=95=98=EB=8F=84=EB=A1=9D=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 --- src/components/attendance/AttendanceContent.tsx | 2 +- src/lib/formatTime.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/attendance/AttendanceContent.tsx b/src/components/attendance/AttendanceContent.tsx index 5c0afa3a..2e653653 100644 --- a/src/components/attendance/AttendanceContent.tsx +++ b/src/components/attendance/AttendanceContent.tsx @@ -77,7 +77,7 @@ function AttendanceContent({ location = null, } = attendance ?? {}; const attendanceRate = attendanceData?.attendanceRate ?? attendance?.attendanceRate ?? 0; - const description = formatAttendanceDescription(start ?? '', end ?? '', location ?? ''); + const description = formatAttendanceDescription(start ?? '', end ?? '', location); return ( <> diff --git a/src/lib/formatTime.ts b/src/lib/formatTime.ts index f64b8642..664c401c 100644 --- a/src/lib/formatTime.ts +++ b/src/lib/formatTime.ts @@ -16,11 +16,12 @@ function formatKoreanDate(date: Date) { * 출석 카드용 설명 문자열 생성 * "날짜 : 2026년 3월 20일 (7:00 PM~9:00 PM)\n장소 : 동아리방" */ -function formatAttendanceDescription(start: string, end: string, location: string) { +function formatAttendanceDescription(start: string, end: string, location: string | null) { const startDate = new Date(start); const endDate = new Date(end); - return `날짜 : ${formatKoreanDate(startDate)} (${formatTime(startDate)}~${formatTime(endDate)})\n장소 : ${location}`; + const datePart = `날짜 : ${formatKoreanDate(startDate)} (${formatTime(startDate)}~${formatTime(endDate)})`; + return location ? `${datePart}\n장소 : ${location}` : datePart; } /**