[Feat] WTH-324 : 출석 SSE 구독을 통한 출석 가능 시간 실시간 동기화#71
Conversation
여러 컴포넌트에서 useAttendanceSSE를 호출해도 EventSource 연결이 1개만 유지되도록 모듈 레벨 싱글턴 패턴 적용. 에러 시 지수 백오프 재연결 로직 추가.
- AttendanceCodeModal, AttendanceQRContent에서 SSE 구독으로 실시간 expiredAt 수신 - AttendanceData 타입에 expiredAt 필드 추가 - useCheckIn에 sessionId/onSuccess 옵션 추가하여 AttendanceContent의 중복 체크인 로직 제거 - useQRCode 호출을 관리자 전용으로 제한 (비관리자 403 방지)
useAttendanceQR, useQRCode를 hooks/ 루트에서 hooks/attendance/로 이동하고 barrel export 추가.
- strict equality 대신 includes로 Accept 헤더 검사 - response.ok 조건 추가로 업스트림 에러 시 즉시 에러 응답 반환 - 불필요한 connection: keep-alive 헤더 제거
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthrough출석 기능에서 Changes
Sequence Diagram(s)sequenceDiagram
participant Client as 클라이언트
participant Component as 컴포넌트
participant Hook as useAttendanceSSE
participant EventSource as EventSource
participant Server as 서버
Client->>Component: 컴포넌트 마운트 (clubId 전달)
Component->>Hook: useAttendanceSSE(clubId) 호출
Hook->>EventSource: EventSource 연결<br/>('/api/proxy/clubs/{clubId}/attendances/stream')
EventSource->>Server: SSE 연결 요청
Server-->>EventSource: 스트림 시작 (text/event-stream)
loop 메시지 수신
Server->>EventSource: 메시지 전송 (expiredAt 포함)
EventSource->>Hook: 메시지 파싱 및 처리
Hook->>Hook: expiredAt 상태 업데이트<br/>& 리스너에 알림
Hook-->>Component: 새로운 expiredAt 반환
Component->>Component: UI 리렌더링
end
Note over EventSource: 재연결 로직<br/>(지수 백오프)
Component->>Hook: 언마운트 또는 구독 해제
Hook->>EventSource: 연결 종료
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
|
구현한 기능 Preview: https://weeth-gstot5pur-weethsite-4975s-projects.vercel.app |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/app/api/proxy/[...path]/route.ts (1)
45-54: 역방향 프록시(Nginx 등)/배포 환경에서 SSE 응답이 버퍼링되지 않도록X-Accel-Buffering: no헤더 추가를 검토해 주세요.일부 프록시(특히 Nginx)나 호스팅 플랫폼은 응답을 기본적으로 버퍼링하여 SSE 이벤트가 클라이언트에 즉시 전달되지 않을 수 있습니다. 개발 환경(스크린샷의
localhost:8080)에서는 확인이 어려울 수 있으니, 운영 환경에서 이벤트가 실시간으로 도착하는지 확인하시고 필요 시 다음 헤더를 추가해 주세요.♻️ 제안
if (isSSE) { responseHeaders.set('content-type', 'text/event-stream'); responseHeaders.set('cache-control', 'no-cache'); + responseHeaders.set('x-accel-buffering', 'no'); + responseHeaders.set('connection', 'keep-alive'); return new NextResponse(response.body, { status: response.status, statusText: response.statusText, headers: responseHeaders, }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/api/proxy/`[...path]/route.ts around lines 45 - 54, When handling SSE in the isSSE branch, add the reverse-proxy buffering header so responses are not buffered by Nginx/hosting layers: set responseHeaders.set('X-Accel-Buffering', 'no') before returning the NextResponse in the isSSE block (where responseHeaders, isSSE, and NextResponse are used) so the SSE stream is forwarded immediately to clients in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/api/proxy/`[...path]/route.ts:
- Around line 42-43: The SSE retry logic can loop forever on permanent errors
(401/403); update useAttendanceSSE to enforce a max retry limit (e.g.,
MAX_RETRY_ATTEMPTS constant) and stop retrying when exceeded, and add explicit
handling to treat 401 and 403 as terminal errors (set connection state to failed
and do not schedule further reconnection). Locate and change the code that
manipulates conn.retryCount (resetting only on success) to increment and compare
against MAX_RETRY_ATTEMPTS, and add a branch that checks response.status (or the
error code path) to immediately abort retries for 401/403 instead of backing
off; keep exponential backoff/reset behavior for transient errors only.
In `@src/components/attendance/AttendanceCodeModal.tsx`:
- Around line 38-39: AttendanceCodeModal currently treats sseExpiredAt === null
as an empty string and lets useRemainingTime mark isExpired true before the
first SSE arrives, disabling the submit button; update the component to
distinguish "not-yet-received" from real expiry by checking sseExpiredAt ===
null and, while null, render a placeholder/skeleton for the timer and force
isExpired to false (or introduce a new flag like isExpiredLoading) so the "출석 가능
시간이 만료되었습니다" branch is suppressed and disabled={!isComplete || isExpired} only
uses the real expiry value from useRemainingTime once sseExpiredAt is populated;
alternatively adapt useRemainingTime to accept null/undefined and return an
explicit loading boolean along with minutes, seconds, and isExpired, then wire
that loading flag into the UI and button disabled logic.
In `@src/components/attendance/AttendanceQRContent.tsx`:
- Around line 26-27: The timer shows "expired" briefly because
useAttendanceSSE's expiredAt is null initially and an empty string passed to
useRemainingTime yields isExpired=true; update AttendanceQRContent to avoid
rendering or calling useRemainingTime until sseExpiredAt is explicitly set: gate
the timer rendering (the block that uses minutes/seconds/isExpired and the call
to useRemainingTime) behind a check for sseExpiredAt (or pass undefined/null
through) and preserve existing isLoading behavior so that useRemainingTime is
only invoked with a valid timestamp (reference useAttendanceSSE, sseExpiredAt,
useRemainingTime, isLoading, and isExpired).
In `@src/hooks/attendance/useCheckIn.ts`:
- Around line 22-26: The isChecked computation incorrectly treats null === null
as true when both checkedSessionId and sessionId are null; update the isChecked
logic in useCheckIn to only compare checkedSessionId to sessionId when sessionId
is non-null (i.e., require sessionId != null/undefined before evaluating
checkedSessionId === sessionId) so that isChecked is false when there is no
active session, preserving the existing data?.status === 'ATTEND' check; modify
the expression that sets isChecked (referencing checkedSessionId, sessionId, and
isChecked) accordingly.
---
Nitpick comments:
In `@src/app/api/proxy/`[...path]/route.ts:
- Around line 45-54: When handling SSE in the isSSE branch, add the
reverse-proxy buffering header so responses are not buffered by Nginx/hosting
layers: set responseHeaders.set('X-Accel-Buffering', 'no') before returning the
NextResponse in the isSSE block (where responseHeaders, isSSE, and NextResponse
are used) so the SSE stream is forwarded immediately to clients in production.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f20cb3c0-00a5-459a-a8ab-76cf58f8c445
📒 Files selected for processing (12)
src/app/api/proxy/[...path]/route.tssrc/components/attendance/AttendanceCodeModal.tsxsrc/components/attendance/AttendanceContent.tsxsrc/components/attendance/AttendanceQRContent.tsxsrc/components/attendance/AttendanceTodayCard.tsxsrc/components/home/TodayScheduleBox.tsxsrc/hooks/attendance/index.tssrc/hooks/attendance/useAttendanceQR.tssrc/hooks/attendance/useAttendanceSSE.tssrc/hooks/attendance/useCheckIn.tssrc/hooks/attendance/useQRCode.tssrc/types/attendance.ts
💤 Files with no reviewable changes (2)
- src/components/home/TodayScheduleBox.tsx
- src/components/attendance/AttendanceTodayCard.tsx
| const isSSE = | ||
| request.headers.get('accept')?.includes('text/event-stream') && response.ok && response.body; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -e ts useAttendanceSSE --exec cat {}Repository: Team-Weeth/weeth-client
Length of output: 2832
🏁 Script executed:
cat -n src/app/api/proxy/[...path]/route.ts | head -80Repository: Team-Weeth/weeth-client
Length of output: 2663
비-2xx SSE 요청에서 응답 본문이 arrayBuffer로 소비되며, 재시도 횟수 제한이 없습니다.
upstream이 401/5xx로 응답하면 response.ok가 false가 되어 line 56의 arrayBuffer() 분기로 진입하고, 에러 응답 본문이 그대로 반환됩니다. 브라우저 EventSource는 비-2xx 응답을 받으면 onerror를 호출하고 내장 재연결을 시도하며, useAttendanceSSE의 지수 백오프 로직이 이에 응답합니다.
문제는 useAttendanceSSE에 최대 재시도 횟수 제한이 없다는 점입니다. retryCount는 성공한 메시지 수신 시에만 리셋되며(conn.retryCount = 0), 지연시간은 MAX_RETRY_DELAY(30초)로 상한이 있지만 재시도 자체는 계속됩니다. 인증 만료(401) 같은 영구적 에러에서 무한 재시도가 발생할 수 있으므로, 재시도 횟수 상한 추가 또는 특정 상태 코드(예: 401, 403)에 대한 별도 처리 로직이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/proxy/`[...path]/route.ts around lines 42 - 43, The SSE retry
logic can loop forever on permanent errors (401/403); update useAttendanceSSE to
enforce a max retry limit (e.g., MAX_RETRY_ATTEMPTS constant) and stop retrying
when exceeded, and add explicit handling to treat 401 and 403 as terminal errors
(set connection state to failed and do not schedule further reconnection).
Locate and change the code that manipulates conn.retryCount (resetting only on
success) to increment and compare against MAX_RETRY_ATTEMPTS, and add a branch
that checks response.status (or the error code path) to immediately abort
retries for 401/403 instead of backing off; keep exponential backoff/reset
behavior for transient errors only.
JIN921
left a comment
There was a problem hiding this comment.
오 SSE는 처음 보는데 이렇게 하나 배워갑니당!! 깔끔하게 구현하셔서 수정할 부분은 없어 보여용 수고하셧어요!
| const [code, setCode] = useState(''); | ||
| const { minutes, seconds, isExpired } = useRemainingTime(endTime); | ||
| const { expiredAt: sseExpiredAt } = useAttendanceSSE(); | ||
| const { minutes, seconds, isExpired } = useRemainingTime(sseExpiredAt ?? ''); |
There was a problem hiding this comment.
sseExpiredAt이 null 상태일 때 처리가 useRemainingTime에 잘 되어 잇나용? 아니라면 null일 때 카운트 다운 렌더가 안 되게 처리해도 좋을 거 같습니다!
dalzzy
left a comment
There was a problem hiding this comment.
수고하셨습니다~!! sse는 해본적이 없어서 신기하구만요,, 크게 수정할 부분 없는 것 같습니당 👍🏻
woneeeee
left a comment
There was a problem hiding this comment.
우왓 확인했습니당!! SSE도 잘 됐으면 좋겠네용...👍🏻
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
|
구현한 기능 Preview: https://weeth-f1k7br4sw-weethsite-4975s-projects.vercel.app |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
|
구현한 기능 Preview: https://weeth-ducpdcob1-weethsite-4975s-projects.vercel.app |
✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
SSE 구독 기반 실시간
expiredAt반영useAttendanceSSE훅을 useSyncExternalStore + 모듈 레벨 싱글턴으로 리팩터링하여,여러 컴포넌트에서 호출해도 EventSource 연결이 1개만 유지되도록 개선
AttendanceQRContent,AttendanceCodeModal에서 SSE 구독을 통해 출석 가능 시간(expiredAt)을 실시간으로수신
출석 체크인 로직 통합
useCheckIn훅에 sessionId, onSuccess 옵션을 추가하여AttendanceContent의 중복 체크인 로직 제거관리자 전용 QR API 호출 제한
isAdmin조건을 추가하여 일반 사용자의 불필요한 admin API 호출(403) 방지QR 훅 위치 정리
SSE 프록시 개선
📸 스크린샷 or 실행영상
테스트할 때는 잘 됏엇는데 오늘 출석을 이미 해버려서......
내일 영상 추가해둘게용!
🎸 기타 사항 or 추가 코멘트
timeout만 표시되어 잇는데, 실제 응답은 아래 이미지처럼{ data: { expiredAt: "..." } }형태로 내려오고 있어 해당 구조 기준으로 파싱하게 해두었습니다!Summary by CodeRabbit
릴리스 노트
새로운 기능
개선사항