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
63 changes: 35 additions & 28 deletions src/components/attendance/AttendanceCodeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState, useEffect, useRef } from 'react';
import { useState } from 'react';

import { CheckRoundIcon } from '@/assets/icons';
import {
Expand All @@ -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;
Expand All @@ -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 (
<Dialog open={open} onOpenChange={handleOpenChange}>
Expand Down Expand Up @@ -96,7 +101,7 @@ function AttendanceCodeModal({

<InputOTP value={code} onChange={setCode} />

{isLoading ? (
{/* {isLoading ? (
<p className="typo-caption2 text-text-alternative text-center">
출석 정보를 불러오는 중...
</p>
Expand All @@ -111,7 +116,9 @@ function AttendanceCodeModal({
<p className="typo-caption2 text-state-error text-center">
출석 가능 시간이 만료되었습니다
</p>
)}
)} */}

{/* TODO: SSE 연결 안정화 후 출석 가능 시간 표시 복원 */}
</DialogBody>

<DialogFooter
Expand All @@ -123,7 +130,7 @@ function AttendanceCodeModal({
variant="primary"
size="lg"
className="w-full"
disabled={!isComplete || (!isLoading && isExpired)}
disabled={!isComplete}
onClick={() => {
onConfirm?.(code);
handleOpenChange(false);
Expand Down
2 changes: 1 addition & 1 deletion src/components/attendance/AttendanceContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand Down
15 changes: 11 additions & 4 deletions src/components/attendance/AttendanceQRContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 (
<div className="mx-auto flex w-full max-w-[1025px] flex-col gap-700 pt-600">
Expand Down Expand Up @@ -73,7 +76,11 @@ function AttendanceQRContent({ sessionId }: AttendanceQRContentProps) {
<div className="flex items-center gap-200">
<span className="typo-sub3 text-text-strong">출석 가능 시간</span>
<span className="typo-sub3 text-state-error tabular-nums">
{!sseExpiredAt ? '연결 중...' : isExpired ? '마감' : `${minutes}:${seconds}`}
{!qrData?.expiredAt
? '로딩 중...'
: isExpired
? '마감'
: `${minutes}:${seconds}`}
</span>
</div>
<p className="typo-body2 text-text-strong">QR코드는 모바일만 제공하고 있어요.</p>
Expand Down
15 changes: 7 additions & 8 deletions src/components/attendance/AttendanceTodayCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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;
Expand Down Expand Up @@ -48,20 +48,17 @@
start,
location,
sessionId,
isAdmin = false,

Check warning on line 51 in src/components/attendance/AttendanceTodayCard.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'isAdmin' is assigned a value but never used
isChecked = false,
disabled = false,
onAttendanceComplete,
}: 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}`);
}
Expand All @@ -77,9 +74,11 @@
onPrimaryClick={() => setCodeModalOpen(true)}
primaryButtonText={isChecked ? '출석 완료' : '출석하기'}
primaryButtonDisabled={disabled || isChecked}
onSecondaryClick={handleSecondaryClick}
secondaryButtonText="출석코드 확인"
secondaryButtonDisabled={disabled || sessionId == null}
{...(isAdminUser && {
onSecondaryClick: handleSecondaryClick,
secondaryButtonText: '출석코드 확인',
secondaryButtonDisabled: disabled || sessionId == null,
})}
>
{isChecked ? (
<AttendanceBanner
Expand Down
26 changes: 15 additions & 11 deletions src/hooks/attendance/useCheckIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<number | null>(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);
}

Expand Down Expand Up @@ -70,7 +73,8 @@ export function useCheckIn(options?: UseCheckInOptions) {
return {
isChecked,
checkInError,
qrStatus,
// TODO: SSE 연결 안정화 후 복원
// qrStatus,
codeModalOpen,
setCodeModalOpen,
openCodeModal,
Expand Down
5 changes: 3 additions & 2 deletions src/lib/formatTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
Loading