-
Notifications
You must be signed in to change notification settings - Fork 0
[feature] 로그인/회원가입 모달 및 Header 이벤트 로직 구현 #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Header 컴포넌트에 onLoginClick prop 정의 - 로그인 버튼에 onClick 속성 연결 - 스타일 클래스 적용 및 UI 수정
- LoginModal, SignupModal UI 및 기본 동작 추가
- page.tsx에서 loginOpen 상태 추가 및 이벤트 연결
- home에 로그인 상태(isAuthed)와 사용자명(userName) 관리 로직 추가 - LoginModal에서 로그인 성공 시에만 모달 닫히도록 수정 - Header에 로그인 상태에 따른 사용자명 표시 및 로그아웃 버튼 추가 - 로그아웃 재확인 모달(ConfirmModal ) 추가
Walkthrough이번 변경에서는 사용자 인증 기능이 대폭 추가되었습니다. 홈 페이지는 로그인, 회원가입, 로그아웃 확인 모달과 인증 상태 관리가 도입되었고, 관련 모달 컴포넌트가 새로 생성되었습니다. API 호출 URL이 환경 변수 기반으로 변경되었으며, 헤더 컴포넌트는 인증 상태에 따라 동적으로 동작하도록 변경되었습니다. 또한, 레이아웃의 기본 배경과 텍스트 색상이 Tailwind CSS 클래스로 지정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant HomePage
participant Header
participant LoginModal
participant SignupModal
participant ConfirmModal
participant API
User->>Header: "로그인" 클릭
Header->>HomePage: onLoginClick()
HomePage->>LoginModal: open = true
User->>LoginModal: 로그인 정보 입력 후 제출
LoginModal->>API: POST /api/auth/login
API-->>LoginModal: 성공/실패 응답
LoginModal->>HomePage: onSubmit 결과 전달
HomePage->>Header: 인증 상태 및 사용자명 갱신
User->>Header: "로그아웃" 클릭
Header->>HomePage: onLogoutClick()
HomePage->>ConfirmModal: open = true
User->>ConfirmModal: "확인" 클릭
ConfirmModal->>HomePage: onConfirm()
HomePage->>Header: 인증 상태 초기화
User->>LoginModal: "회원가입" 클릭
LoginModal->>SignupModal: open = true
User->>SignupModal: 회원정보 입력 후 제출
SignupModal->>API: POST /api/auth/signup
API-->>SignupModal: 성공/실패 응답
SignupModal->>HomePage: onSubmit 결과 전달
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~18 minutes Poem
Note 🔌 MCP (Model Context Protocol) integration is now available in Early Access!Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🔭 Outside diff range comments (1)
src/app/result/page.tsx (1)
351-358: 환경변수 미설정/URL 조합 이슈 대비한 안전한 호출로 교체 권장NEXT_PUBLIC_API_BASE가 비어도 동작하도록 폴백(상대 경로)과 new URL을 사용한 안전한 조합을 권장합니다.
- const compareResponse = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE}/api/time/compare`, - { + const base = process.env.NEXT_PUBLIC_API_BASE; + const compareUrl = base + ? new URL('/api/time/compare', base).toString() + : '/api/time/compare'; + const compareResponse = await fetch(compareUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targetUrl: url }), - }, - ); + });
🧹 Nitpick comments (4)
src/components/ui/ConfirmModal.tsx (1)
15-23: ESC 키로 모달 닫기(선택), 포커스 트랩(선택) 제안키보드 사용성을 높이려면 open=true일 때 ESC로 닫기, 최초 포커스 이동/포커스 트랩을 고려하세요.
아래 훅을 컴포넌트 내부에 추가해보세요(의사코드):
useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [open, onClose]); // 포커스 트랩은 별도 라이브러리(focus-trap, @radix-ui/react-dialog 등) 사용을 고려.src/components/auth/LoginModal.tsx (1)
33-38: 입력값 공백 제거 후 전송 권장
password값을 그대로 전달하면 사용자가 앞뒤에 공백을 넣었을 때 인증이 실패할 수 있습니다.trim()후 전달하도록 개선해 주세요.src/app/page.tsx (2)
90-117: 토큰을 localStorage에 저장하면 XSS에 취약합니다
accessToken·refreshToken을localStorage에 보관하면 스크립트 삽입 공격으로 탈취될 위험이 있습니다.
HTTP-only 쿠키로 전환하거나 최소한sessionStorage+ CSP 강화 등을 검토해 주세요.
128-151: 중복된 fetch 로직은 헬퍼 함수로 분리하세요로그인/회원가입 모두 동일한 패턴의
fetch → res.json → 오류 처리코드를 갖고 있습니다.
공통 util 함수로 추출해 중복을 제거하면 유지보수가 쉬워집니다.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/app/layout.tsx(1 hunks)src/app/page.tsx(2 hunks)src/app/result/page.tsx(1 hunks)src/components/KoreanStandaradTime.tsx(1 hunks)src/components/auth/LoginModal.tsx(1 hunks)src/components/auth/SignupModal.tsx(1 hunks)src/components/ui/ConfirmModal.tsx(1 hunks)src/components/ui/Header.tsx(2 hunks)
🔇 Additional comments (2)
src/app/layout.tsx (1)
28-28: 전역 배경/텍스트 색상 적용 LGTM레이아웃 전역 톤 설정이 모달/헤더 UI 변경과 잘 맞습니다. 별도 이슈 없어 보입니다.
src/components/ui/Header.tsx (1)
14-19: Props 기반 리팩토링 방향 좋습니다부모에서 인증 상태/액션을 주입받는 구조가 명확하고 재사용성도 좋아졌습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (7)
src/app/layout.tsx (1)
28-28: 다크 모드 고려가 누락되어 있습니다
text-gray-900만 지정하면 다크 모드 전환 시 글자가 보이지 않을 수 있습니다.dark:text-gray-50등 조건부 클래스를 함께 두는 것을 권장드립니다.src/components/ui/ConfirmModal.tsx (1)
26-56: 포커스 트랩 및 접근성 속성 보강 제안모달이 열렸을 때
- 최초 포커스를 모달 내부 요소로 이동하고
- 포커스가 모달 밖으로 빠져나가지 않도록(trap) 해야 WCAG 기준을 충족합니다.
또한role="alertdialog"와aria-labelledby/aria-describedby를 지정하면 스크린리더 호환성이 개선됩니다.src/components/auth/LoginModal.tsx (2)
48-70: 모달 접근성 개선 필요
role="dialog"는 선언되었지만 포커스 트랩이 없고, 제목·본문에 대한aria-labelledby/aria-describedby연결이 없어 스크린리더 사용자가 맥락을 파악하기 어렵습니다.focus-trap-react같은 유틸을 도입하거나 직접 포커스 관리 로직을 추가해주세요.
118-124: 이모지 토글 아이콘의 스크린리더 라벨 부족‘👁️/🙈’ 이모지는 의미 전달이 불명확합니다.
aria-label을 버튼에 이미 지정하셨지만, 텍스트 대신 SVG 아이콘 또는 시각적 대체 텍스트 제공을 고려해주세요.src/app/page.tsx (3)
24-33:localStorage접근 시 예외 대비가 필요합니다일부 브라우저 설정(프라이빗 모드 등)에서는
localStorage접근이SecurityError를 일으킬 수 있습니다. 짧은try { … } catch {}로 감싸 두면 예기치 않은 초기 렌더 크래시를 예방할 수 있습니다.
90-117: 토큰을localStorage에 저장하면 XSS 위험에 노출됩니다
accessToken / refreshToken을localStorage에 저장할 경우, 스크립트 인젝션이 발생했을 때 탈취가 매우 쉽습니다. 가능하다면
• 서버에서HttpOnly, Secure, SameSite쿠키로 발급하거나
• 최소한sessionStorage+ CSP 강화 등 대안을 고려해주세요.
144-147: 디버깅용console.log삭제 권장운영 환경에 불필요한 콘솔 출력은 노이즈가 될 수 있습니다. 배포 전에 제거해주세요.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/app/layout.tsx(1 hunks)src/app/page.tsx(2 hunks)src/app/result/page.tsx(1 hunks)src/components/KoreanStandaradTime.tsx(1 hunks)src/components/auth/LoginModal.tsx(1 hunks)src/components/auth/SignupModal.tsx(1 hunks)src/components/ui/ConfirmModal.tsx(1 hunks)src/components/ui/Header.tsx(2 hunks)
🔇 Additional comments (1)
src/app/result/page.tsx (1)
352-353: API 베이스 URL 검증 필요
NEXT_PUBLIC_API_BASE값이 없을 때 동일한 문제가 발생합니다. 위에서 제안한 방식과 동일하게 기본값 검증 로직을 추가하시기 바랍니다.
| let score = 0; | ||
| if (password.length >= 8) score++; | ||
| if (/[a-zA-Z]/.test(password)) score++; | ||
| if (/[0-9]/.test(password)) score++; | ||
| if (/[^a-zA-Z0-9]/.test(password)) score++; | ||
| return score; | ||
| })(); | ||
|
|
||
| const strengthBarClass = | ||
| passwordStrength <= 1 | ||
| ? 'bg-red-500 w-1/4' | ||
| : passwordStrength === 2 | ||
| ? 'bg-yellow-500 w-1/2' | ||
| : passwordStrength === 3 | ||
| ? 'bg-blue-500 w-3/4' | ||
| : 'bg-green-500 w-full'; | ||
|
|
||
| const canSubmit = | ||
| username.trim().length >= 2 && | ||
| /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && | ||
| passwordStrength >= 3 && | ||
| password === confirmPassword && | ||
| !loading; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
비밀번호 최소 길이(8자) 검증이 실제로는 누락되어 있습니다
passwordStrength 점수에 길이 조건이 포함돼 있지만 canSubmit은 passwordStrength >= 3 만 확인합니다.
예: Abc1!(5자) → 길이점수 0 + 영문 + 숫자 + 특수문자 = 3 → 제출 가능.
UI 에서 “8자 이상”을 안내하고 있으므로 다음 중 한 가지로 수정이 필요합니다.
- passwordStrength >= 3 &&
+ password.length >= 8 &&
+ passwordStrength >= 3 &&또는 passwordStrength 계산 방식을 변경해 길이 만족이 필수로 반영되도록 해주세요.
📝 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.
| let score = 0; | |
| if (password.length >= 8) score++; | |
| if (/[a-zA-Z]/.test(password)) score++; | |
| if (/[0-9]/.test(password)) score++; | |
| if (/[^a-zA-Z0-9]/.test(password)) score++; | |
| return score; | |
| })(); | |
| const strengthBarClass = | |
| passwordStrength <= 1 | |
| ? 'bg-red-500 w-1/4' | |
| : passwordStrength === 2 | |
| ? 'bg-yellow-500 w-1/2' | |
| : passwordStrength === 3 | |
| ? 'bg-blue-500 w-3/4' | |
| : 'bg-green-500 w-full'; | |
| const canSubmit = | |
| username.trim().length >= 2 && | |
| /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && | |
| passwordStrength >= 3 && | |
| password === confirmPassword && | |
| !loading; | |
| const canSubmit = | |
| username.trim().length >= 2 && | |
| /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && | |
| password.length >= 8 && | |
| passwordStrength >= 3 && | |
| password === confirmPassword && | |
| !loading; |
🤖 Prompt for AI Agents
In src/components/auth/SignupModal.tsx around lines 40 to 63, the password
minimum length check (8 characters) is missing from the canSubmit condition,
allowing short passwords to pass if other criteria are met. To fix this,
explicitly add a check for password length >= 8 in the canSubmit condition or
modify the passwordStrength calculation so that the length requirement is
mandatory for achieving a passing score.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
선택사항으로 판단되어 보류
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cl-o-lc
알겠습니다. 선택사항으로 간주된 것으로 이해했고, 본 PR에서는 변경 없이 진행하겠습니다. 후속으로 추적할 이슈를 열어둘까요? 원하시면 생성해 드리겠습니다. 또한 UI 문구가 “8자 이상”처럼 강제 안내 중이라면 “권장 8자 이상” 등으로의 카피 조정도 가능해요.
(˘•︵•˘)🐰
✏️ Learnings added
Learnt from: cl-o-lc
PR: The-CheckMate/CheckTime-FE#6
File: src/components/auth/SignupModal.tsx:40-63
Timestamp: 2025-08-08T14:00:26.546Z
Learning: Repo preference: In src/components/auth/SignupModal.tsx, enforcing a strict 8+ character password check in canSubmit is considered optional by the maintainer (cl-o-lc) and may be deferred; avoid blocking PRs on this point and consider aligning UI copy if necessary.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
- dialog 메타데이터(role, aria-*) 추가 - 모든 버튼에 type="button" 명시 - 열릴 때 포커스 이동 및 ESC 키로 닫기 지원
- 옵션 prop 호출 시 ?.()로 런타임 오류 방지 - 파일명 오타 수정 (KoreanStandaradTime → KoreanStandardTime)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🔭 Outside diff range comments (3)
src/components/KoreanStandardTime.tsx (3)
24-28: data.data 접근 시 런타임 오류 가능성API가 성공 플래그만 true이고 data가 비어있는 경우
data.data.timestamp접근에서 TypeError가 발생할 수 있습니다. 옵셔널 체이닝과 타입 가드를 추가하세요.- if (data.success && data.data.timestamp) { - const serverTimestamp = data.data.timestamp; + if (data?.success && typeof data?.data?.timestamp === 'number') { + const serverTimestamp = data.data.timestamp as number; offset = serverTimestamp - Date.now(); setTime(new Date(Date.now() + offset)); }
15-16: 초기 로딩 중 로컬 시간이 먼저 노출됨fetch가 완료되기 전에 interval이 동작하면서 offset=0 상태로 time이 설정되어, “KST” 라벨임에도 잠시 로컬 시간이 렌더링됩니다. offset이 준비되기 전에는 갱신을 막아주세요.
useEffect(() => { - let offset = 0; + let offset = 0; + let hasOffset = false; const fetchTime = async () => { try { @@ - if (data.success && data.data.timestamp) { + if (data?.success && typeof data?.data?.timestamp === 'number') { const serverTimestamp = data.data.timestamp; offset = serverTimestamp - Date.now(); setTime(new Date(Date.now() + offset)); + hasOffset = true; } } catch (error) { console.error('시간 정보를 불러오는 데 실패했습니다', error); } }; @@ - const interval = setInterval(() => { - setTime(new Date(Date.now() + offset)); - }, 33); // 30FPS 정도로 실시간 업데이트 + const interval = setInterval(() => { + if (!hasOffset) return; // 서버 시계 동기화 전에는 렌더링하지 않음 + setTime(new Date(Date.now() + offset)); + }, 33); // 30FPS 정도로 실시간 업데이트Also applies to: 24-28, 36-39
53-59: KST(Asia/Seoul) 타임존으로 고정되지 않음getHours()/getMinutes()는 사용자 로컬 타임존을 사용합니다. “한국 표준시”를 정확히 표기하려면 Asia/Seoul 타임존으로 포맷팅하세요.
- const formatTime = (date: Date) => { - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - const millis = String(date.getMilliseconds()).padStart(3, '0'); - return `${hours}:${minutes}:${seconds}.${millis}`; - }; + const formatTime = (date: Date) => { + const options = { + timeZone: 'Asia/Seoul', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + } as const; + const [hours, minutes, seconds] = new Intl.DateTimeFormat('ko-KR', options) + .format(date) + .split(':'); + const millis = String(date.getMilliseconds()).padStart(3, '0'); + return `${hours}:${minutes}:${seconds}.${millis}`; + };
🧹 Nitpick comments (3)
src/components/KoreanStandardTime.tsx (1)
36-39: 업데이트 주기 최적화밀리초 표시가 꺼져 있을 때는 1초 간격으로만 갱신해도 충분합니다. 렌더 비용을 줄이세요.
- }, 33); // 30FPS 정도로 실시간 업데이트 + }, showMilliseconds ? 33 : 1000); // ms 미표시 시 1s 간격src/app/page.tsx (2)
101-104: 에러 핸들링 순서 개선 제안
res.ok여부를 확인하기 전에res.json()을 호출하면, 서버가 500 응답과 함께 body 를 비워둘 때 JSON 파싱 예외가 먼저 발생합니다.-const data = await res.json(); -if (!res.ok) throw new Error(data.error || '로그인 실패'); +let data: any = null; +if (res.headers.get('content-type')?.includes('application/json')) { + data = await res.json(); +} +if (!res.ok) { + throw new Error(data?.error ?? '로그인 실패'); +}예외 원인을 더 명확히 구분할 수 있습니다.
144-144: 개인 정보가 콘솔에 노출됩니다
console.log('회원가입 성공', data.data.user);는 프로덕션 번들에 포함되어 브라우저 콘솔에서 사용자 데이터가 노출됩니다.
배포 전 제거하거나process.env.NODE_ENV !== 'production'조건부로 감싸 주세요.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/app/page.tsx(2 hunks)src/app/result/page.tsx(2 hunks)src/components/KoreanStandardTime.tsx(1 hunks)src/components/auth/LoginModal.tsx(1 hunks)src/components/auth/SignupModal.tsx(1 hunks)src/components/ui/ConfirmModal.tsx(1 hunks)src/components/ui/Header.tsx(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/components/auth/LoginModal.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- src/app/result/page.tsx
- src/components/ui/Header.tsx
- src/components/ui/ConfirmModal.tsx
- src/components/auth/SignupModal.tsx
🔇 Additional comments (2)
src/components/KoreanStandardTime.tsx (2)
19-21: 환경 변수 기반 API URL 전환, 방향성 좋습니다하드코딩된 URL 제거하고 구성 가능하도록 바꾼 점 좋습니다. 배포/환경별로 유연해졌습니다.
86-103: toggleMilliseconds CustomEvent 리스너 확인 및 콜백 prop 제안토글 이벤트 리스너는
src/app/result/page.tsx의useEffect내에서document.addEventListener('toggleMilliseconds', listener);로 등록되어 있으므로,
onChange시 UI가 정상 업데이트됩니다.
하지만 글로벌 이벤트에 의존하는 현재 방식은 컴포넌트 간 결합도를 높이고 유지보수를 어렵게 할 수 있습니다. React 방식에 더 부합하도록 아래와 같이 수정해 보시길 권장드립니다.– src/components/KoreanStandardTime.tsx
•onToggleMilliseconds?: (checked: boolean) => voidprop 추가
•onChange핸들러에서document.dispatchEvent대신onToggleMilliseconds(e.target.checked)호출– src/app/result/page.tsx
•<KoreanStandardTime … onToggleMilliseconds={setShowMilliseconds} />와 같이 상태 변경 콜백 전달이렇게 하면 글로벌 이벤트 의존도를 제거하고 재사용성, 테스트 용이성을 개선할 수 있습니다.
| // 새로고침 시에도 로그인 유지 | ||
| useEffect(() => { | ||
| const at = localStorage.getItem('accessToken'); | ||
| const name = localStorage.getItem('userName') || undefined; | ||
| if (at) { | ||
| setIsAuthed(true); | ||
| setUserName(name); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
토큰 만료‧무결성 검증 로직이 없습니다
로컬 스토리지에 토큰이 존재한다는 이유만으로 isAuthed를 true로 설정하면
만료·위조 토큰을 가진 사용자가 인증된 상태로 오인될 수 있습니다.
JWT 만료 확인 혹은 /auth/validate 같은 엔드포인트 호출을 통해 최초 마운트 시 토큰 유효성을 검증해 주세요.
🏁 Script executed:
#!/bin/bash
set -e
echo "----- src/app/page.tsx (lines 1-200) -----"
sed -n '1,200p' src/app/page.tsxLength of output: 5395
토큰 유효성 검증 로직 추가 필요
로컬 스토리지에 토큰이 존재한다고 곧바로 isAuthed를 true로 설정하면, 만료되었거나 위조된 토큰도 인증된 상태로 오인될 수 있습니다.
• 검증 대상: src/app/page.tsx useEffect 내부 (24–32라인)
• 제안 사항:
- JWT 만료 시간(
exp) 확인 - 또는
/api/auth/validate같은 백엔드 엔드포인트에 토큰 검증 요청 - 검증 실패 시 로컬 스토리지에서 토큰 제거 및 인증 상태(false) 처리
예시 코드 스니펫:
useEffect(() => {
const at = localStorage.getItem('accessToken');
const name = localStorage.getItem('userName') || undefined;
if (at) {
- setIsAuthed(true);
- setUserName(name);
+ // 1) 만료·무결성 검증
+ fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/validate`, {
+ headers: { Authorization: `Bearer ${at}` },
+ })
+ .then(res => {
+ if (res.ok) {
+ setIsAuthed(true);
+ setUserName(name);
+ } else {
+ localStorage.removeItem('accessToken');
+ setIsAuthed(false);
+ }
+ });
}
}, []);📝 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.
| // 새로고침 시에도 로그인 유지 | |
| useEffect(() => { | |
| const at = localStorage.getItem('accessToken'); | |
| const name = localStorage.getItem('userName') || undefined; | |
| if (at) { | |
| setIsAuthed(true); | |
| setUserName(name); | |
| } | |
| }, []); | |
| // 새로고침 시에도 로그인 유지 | |
| useEffect(() => { | |
| const at = localStorage.getItem('accessToken'); | |
| const name = localStorage.getItem('userName') || undefined; | |
| if (at) { | |
| // 1) 만료·무결성 검증 | |
| fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/validate`, { | |
| headers: { Authorization: `Bearer ${at}` }, | |
| }) | |
| .then(res => { | |
| if (res.ok) { | |
| setIsAuthed(true); | |
| setUserName(name); | |
| } else { | |
| localStorage.removeItem('accessToken'); | |
| setIsAuthed(false); | |
| } | |
| }); | |
| } | |
| }, []); |
🤖 Prompt for AI Agents
In src/app/page.tsx around lines 24 to 32, the current useEffect sets isAuthed
to true if an accessToken exists in localStorage without validating it, which
risks accepting expired or forged tokens. To fix this, add logic to verify the
token's validity by either decoding the JWT to check its expiration time or
sending the token to a backend validation endpoint like /api/auth/validate. If
the token is invalid or expired, remove it from localStorage and set isAuthed to
false to prevent unauthorized access.
| localStorage.setItem('accessToken', data.data.accessToken); | ||
| localStorage.setItem('refreshToken', data.data.refreshToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refresh Token을 localStorage 에 저장하면 XSS에 취약합니다
accessToken 뿐 아니라 장기 수명의 refreshToken 도 동일한 DOM 영역에 저장되고 있어,
스크립트 인젝션 하나로 영구 재발급 권한까지 탈취될 수 있습니다.
refreshToken 은 httpOnly‧Secure‧SameSite 쿠키로 내려받도록 백엔드와 협의하시길 권장합니다.
🤖 Prompt for AI Agents
In src/app/page.tsx around lines 105 to 106, the refreshToken is being stored in
localStorage, which exposes it to XSS attacks. To fix this, remove the line that
stores refreshToken in localStorage and instead coordinate with the backend to
have the refreshToken set as an httpOnly, Secure, and SameSite cookie to protect
it from client-side script access.
- Authed/Guest 분기 타입 정의 - 로그인 상태일 때 onLogoutClick 필수, 게스트일 때 onLoginClick 필수 - 상태-프롭 불일치를 컴파일 타임에 검출
- new URL로 슬래시 중복/누락 방지 - 환경 변수 미설정 시 상대 경로로 fallback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
src/components/ui/Header.tsx (4)
7-22: 분기 타입(discriminated union) 강화 제안: isAuthed를 게스트 쪽에서도 필수 false로 지정게스트 타입에서
isAuthed?: false대신isAuthed: false로 고정하면 분기 축이 양쪽에 모두 존재해 타입 내로잉이 더 정확하게 동작합니다. 그러면 아래 액션 영역에서도 옵셔널 체이닝 없이 핸들러를 직접 전달할 수 있습니다.적용 예시:
type GuestProps = { - isAuthed?: false; + isAuthed: false; userName?: never; onLoginClick: () => void; // 로그인 모달 열기 onLogoutClick?: never; };
27-29: 스크롤 초기 상태 동기화 + passive 리스너 권장첫 렌더 시 스크롤이 이미 100px 초과인 경우 초기 상태가 맞지 않을 수 있습니다. 또한 스크롤 리스너는
passive: true로 거는 것이 성능에 유리합니다.useEffect(() => { const handleScroll = () => setScrolled(window.scrollY > 100); - window.addEventListener('scroll', handleScroll); + window.addEventListener('scroll', handleScroll, { passive: true }); + handleScroll(); // 초기 상태 동기화 return () => window.removeEventListener('scroll', handleScroll); }, []);
79-81: 불필요한 옵셔널 체이닝/화살표 래핑 제거Authed 분기에서는
onLogoutClick가 존재함이 타입으로 보장됩니다. 핸들러를 직접 전달하면 불필요한 재생성을 줄일 수 있습니다.- <button - type="button" - onClick={() => props.onLogoutClick?.()} + <button + type="button" + onClick={props.onLogoutClick} className="px-4 py-2 rounded-md text-sm border border-gray-300 hover:bg-gray-50"
87-91: 게스트 분기에서도 핸들러 직접 전달게스트 분기에서
onLoginClick은 존재함이 타입으로 보장되도록 위 제안(GuestProps에isAuthed: false)을 적용하면 옵셔널 체이닝과 래핑을 제거할 수 있습니다.- <button - type="button" - onClick={() => props.onLoginClick?.()} // 게스트에서만 허용 + <button + type="button" + onClick={props.onLoginClick} // 게스트에서만 허용 className="bg-black text-white hover:bg-black/80 px-4 py-2 rounded-md text-sm transition"
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/app/page.tsx(2 hunks)src/components/KoreanStandardTime.tsx(1 hunks)src/components/ui/Header.tsx(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/components/KoreanStandardTime.tsx
- src/app/page.tsx
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: cl-o-lc
PR: The-CheckMate/CheckTime-FE#6
File: src/components/auth/SignupModal.tsx:40-63
Timestamp: 2025-08-08T14:00:26.546Z
Learning: Repo preference: In src/components/auth/SignupModal.tsx, enforcing a strict 8+ character password check in canSubmit is considered optional by the maintainer (cl-o-lc) and may be deferred; avoid blocking PRs on this point and consider aligning UI copy if necessary.
Learnt from: cl-o-lc
PR: The-CheckMate/CheckTime-FE#6
File: src/components/KoreanStandaradTime.tsx:19-21
Timestamp: 2025-08-08T14:05:10.132Z
Learning: Repo preference: In PR The-CheckMate/CheckTime-FE#6, maintainer (cl-o-lc) chose to defer adding a runtime guard for process.env.NEXT_PUBLIC_API_BASE in src/components/KoreanStandaradTime.tsx; do not block the PR on this point and handle as a follow-up if needed.
Learnt from: cl-o-lc
PR: The-CheckMate/CheckTime-FE#6
File: src/components/KoreanStandaradTime.tsx:19-21
Timestamp: 2025-08-08T14:05:10.132Z
Learning: The-CheckMate/CheckTime-FE PR #6: Maintainer (cl-o-lc) deferred adding runtime guards for process.env.NEXT_PUBLIC_API_BASE; treat as non-blocking and propose a follow-up issue instead of pushing changes in this PR.
Learnt from: cl-o-lc
PR: The-CheckMate/CheckTime-FE#6
File: src/components/KoreanStandaradTime.tsx:19-21
Timestamp: 2025-08-08T14:05:10.132Z
Learning: Repo preference: In The-CheckMate/CheckTime-FE PR #6, maintainer (cl-o-lc) decided to defer adding a runtime guard for process.env.NEXT_PUBLIC_API_BASE across components (e.g., KoreanStandardTime), so treat this as non-blocking and handle via a follow-up issue if needed.
📚 Learning: 2025-08-08T14:00:26.546Z
Learnt from: cl-o-lc
PR: The-CheckMate/CheckTime-FE#6
File: src/components/auth/SignupModal.tsx:40-63
Timestamp: 2025-08-08T14:00:26.546Z
Learning: Repo preference: In src/components/auth/SignupModal.tsx, enforcing a strict 8+ character password check in canSubmit is considered optional by the maintainer (cl-o-lc) and may be deferred; avoid blocking PRs on this point and consider aligning UI copy if necessary.
Applied to files:
src/components/ui/Header.tsx
📚 Learning: 2025-08-08T14:05:10.132Z
Learnt from: cl-o-lc
PR: The-CheckMate/CheckTime-FE#6
File: src/components/KoreanStandaradTime.tsx:19-21
Timestamp: 2025-08-08T14:05:10.132Z
Learning: Repo preference: In PR The-CheckMate/CheckTime-FE#6, maintainer (cl-o-lc) chose to defer adding a runtime guard for process.env.NEXT_PUBLIC_API_BASE in src/components/KoreanStandaradTime.tsx; do not block the PR on this point and handle as a follow-up if needed.
Applied to files:
src/components/ui/Header.tsx
🔇 Additional comments (2)
src/components/ui/Header.tsx (2)
23-23: 컴포넌트 시그니처에 분기 타입 적용한 점 좋습니다Props 모델링이 명확해져 오용 가능성이 낮아졌습니다.
71-86: 조건부 렌더링과 버튼 전환 깔끔합니다로그인/로그아웃 상태별 UI 분기, 시맨틱 버튼 적용, 이전 런타임 오류 가능성(옵셔널 호출) 대응 모두 적절합니다.
📌 작업 내용
Header 컴포넌트
page.tsx
Auth 모달 컴포넌트
기타
📸 스크린샷
Login_test.mp4
📝 기타
Summary by CodeRabbit
신규 기능
기능 개선
버그 수정
스타일