Skip to content

Conversation

@cl-o-lc
Copy link
Collaborator

@cl-o-lc cl-o-lc commented Aug 8, 2025

📌 작업 내용

Header 컴포넌트

  • onLoginClick prop 정의 및 로그인 버튼 클릭 이벤트 핸들러 추가
  • 스타일 클래스 적용 및 UI 수정

page.tsx

  • loginOpen / signupOpen 상태 관리 로직 추가
  • Header에 onLoginClick 이벤트 연결

Auth 모달 컴포넌트

  • LoginModal, SignupModal UI 구현 및 기본 동작 추가
  • 로그인 성공 시 모달 닫기 및 사용자 상태 갱신
  • 로그아웃 버튼 및 확인 모달(ConfirmModal) 추가

기타

  • .env.local에 프론트엔드 API base URL 설정 추가
  • 전역 배경색(bg-gray-50) 적용 시 스크롤 하단 검정색 영역 문제 해결

📸 스크린샷

Login_test.mp4

📝 기타

Summary by CodeRabbit

  • 신규 기능

    • 로그인, 회원가입, 로그아웃 확인을 위한 모달 다이얼로그가 추가되었습니다.
    • 회원가입 시 비밀번호 강도 표시 및 실시간 유효성 검사가 제공됩니다.
    • 로그아웃 시 확인 모달이 표시됩니다.
  • 기능 개선

    • 로그인 상태가 페이지 새로고침 후에도 유지됩니다.
    • 헤더에 로그인/로그아웃 및 사용자명 표시 기능이 추가되었습니다.
    • 인증 상태에 따라 헤더 버튼이 동적으로 변경됩니다.
  • 버그 수정

    • 일부 컴포넌트의 경로 오타가 수정되었습니다.
    • 서버 시간 비교 및 현재 시간 조회 API 주소가 환경 변수 기반으로 변경되어, 다양한 환경에서 정상 동작합니다.
  • 스타일

    • 전체 배경색과 텍스트 색상이 더 보기 좋게 개선되었습니다.

cl-o-lc added 6 commits August 8, 2025 16:18
- Header 컴포넌트에 onLoginClick prop 정의
- 로그인 버튼에 onClick 속성 연결
- 스타일 클래스 적용 및 UI 수정
- LoginModal, SignupModal UI 및 기본 동작 추가
- page.tsx에서 loginOpen 상태 추가 및 이벤트 연결
- home에 로그인 상태(isAuthed)와 사용자명(userName) 관리 로직 추가
- LoginModal에서 로그인 성공 시에만 모달 닫히도록 수정
- Header에 로그인 상태에 따른 사용자명 표시 및 로그아웃 버튼 추가
- 로그아웃 재확인 모달(ConfirmModal ) 추가
@cl-o-lc cl-o-lc added feat🛠️ 기능 구현 design✨ UI 구현 및 개선 labels Aug 8, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 8, 2025

Walkthrough

이번 변경에서는 사용자 인증 기능이 대폭 추가되었습니다. 홈 페이지는 로그인, 회원가입, 로그아웃 확인 모달과 인증 상태 관리가 도입되었고, 관련 모달 컴포넌트가 새로 생성되었습니다. API 호출 URL이 환경 변수 기반으로 변경되었으며, 헤더 컴포넌트는 인증 상태에 따라 동적으로 동작하도록 변경되었습니다. 또한, 레이아웃의 기본 배경과 텍스트 색상이 Tailwind CSS 클래스로 지정되었습니다.

Changes

Cohort / File(s) Change Summary
홈 인증 및 상태 관리
src/app/page.tsx
홈 컴포넌트에 인증 상태 관리, 로그인/회원가입/로그아웃 모달, localStorage 연동, 인증 상태에 따른 UI/로직 분기 추가.
레이아웃 스타일 개선
src/app/layout.tsx
<body>에 Tailwind CSS의 bg-gray-50text-gray-900 클래스 추가로 배경색과 텍스트 색상 지정.
API URL 환경변수화 및 import 경로 수정
src/app/result/page.tsx, src/components/KoreanStandardTime.tsx
API 호출 시 기존 하드코딩된 localhost 주소를 환경 변수(NEXT_PUBLIC_API_BASE) 기반 동적 URL로 변경. KoreanStandardTime 컴포넌트 import 경로 오타 수정. fetch 요청에 캐시 비활성화 및 에러 핸들링 추가.
로그인 모달 추가
src/components/auth/LoginModal.tsx
로그인 모달 컴포넌트 신규 추가. 이메일/비밀번호 입력, 비밀번호 보기 토글, 로딩 상태, 회원가입 모달 전환 지원 등 구현.
회원가입 모달 추가
src/components/auth/SignupModal.tsx
회원가입 모달 컴포넌트 신규 추가. 사용자명, 이메일, 비밀번호, 비밀번호 확인 입력 및 검증, 비밀번호 강도 표시, 로딩 처리, 로그인 모달 전환 지원 등 구현.
확인 모달 추가
src/components/ui/ConfirmModal.tsx
확인(Confirm) 모달 컴포넌트 신규 추가. 타이틀/메시지/확인 및 취소 버튼, ESC/배경 클릭으로 닫기, 포커스 관리 등 구현.
헤더 인증 상태 연동 및 UI 변경
src/components/ui/Header.tsx
헤더 컴포넌트가 인증 상태와 사용자명을 props로 받아 로그인/로그아웃 버튼 및 인사말 동적으로 표시. 기존 "시작하기" 버튼 제거. 스크롤 이벤트 핸들러 간소화.

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 결과 전달
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~18 minutes

Poem

🐇
로그인 창이 반짝, 회원가입도 쏙!
모달이 춤추며 인증을 톡톡,
헤더엔 인사말, 버튼이 척척,
환경변수 따라 API도 척척!
오늘도 토끼는 코드를 토닥,
새 기능 품고 깡충깡충 hop hop!

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/auth

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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: 입력값 공백 제거 후 전송 권장

email, password 값을 그대로 전달하면 사용자가 앞뒤에 공백을 넣었을 때 인증이 실패할 수 있습니다. trim() 후 전달하도록 개선해 주세요.

src/app/page.tsx (2)

90-117: 토큰을 localStorage에 저장하면 XSS에 취약합니다

accessToken·refreshTokenlocalStorage 에 보관하면 스크립트 삽입 공격으로 탈취될 위험이 있습니다.
HTTP-only 쿠키로 전환하거나 최소한 sessionStorage + CSP 강화 등을 검토해 주세요.


128-151: 중복된 fetch 로직은 헬퍼 함수로 분리하세요

로그인/회원가입 모두 동일한 패턴의 fetch → res.json → 오류 처리 코드를 갖고 있습니다.
공통 util 함수로 추출해 중복을 제거하면 유지보수가 쉬워집니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5945318 and 1cd3680.

📒 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 기반 리팩토링 방향 좋습니다

부모에서 인증 상태/액션을 주입받는 구조가 명확하고 재사용성도 좋아졌습니다.

Copy link

@coderabbitai coderabbitai bot left a 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: 포커스 트랩 및 접근성 속성 보강 제안

모달이 열렸을 때

  1. 최초 포커스를 모달 내부 요소로 이동하고
  2. 포커스가 모달 밖으로 빠져나가지 않도록(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 / refreshTokenlocalStorage에 저장할 경우, 스크립트 인젝션이 발생했을 때 탈취가 매우 쉽습니다. 가능하다면
• 서버에서 HttpOnly, Secure, SameSite 쿠키로 발급하거나
• 최소한 sessionStorage + CSP 강화 등 대안을 고려해주세요.


144-147: 디버깅용 console.log 삭제 권장

운영 환경에 불필요한 콘솔 출력은 노이즈가 될 수 있습니다. 배포 전에 제거해주세요.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5945318 and 1cd3680.

📒 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 값이 없을 때 동일한 문제가 발생합니다. 위에서 제안한 방식과 동일하게 기본값 검증 로직을 추가하시기 바랍니다.

Comment on lines +40 to +63
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;

Copy link

@coderabbitai coderabbitai bot Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

비밀번호 최소 길이(8자) 검증이 실제로는 누락되어 있습니다

passwordStrength 점수에 길이 조건이 포함돼 있지만 canSubmitpasswordStrength >= 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.

Suggested change
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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선택사항으로 판단되어 보류

Copy link

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.

cl-o-lc added 3 commits August 8, 2025 22:13
- dialog 메타데이터(role, aria-*) 추가
- 모든 버튼에 type="button" 명시
- 열릴 때 포커스 이동 및 ESC 키로 닫기 지원
- 옵션 prop 호출 시 ?.()로 런타임 오류 방지
- 파일명 오타 수정 (KoreanStandaradTime → KoreanStandardTime)
Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1cd3680 and bb0b647.

📒 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.tsxuseEffect 내에서

document.addEventListener('toggleMilliseconds', listener);

로 등록되어 있으므로, onChange 시 UI가 정상 업데이트됩니다.
하지만 글로벌 이벤트에 의존하는 현재 방식은 컴포넌트 간 결합도를 높이고 유지보수를 어렵게 할 수 있습니다. React 방식에 더 부합하도록 아래와 같이 수정해 보시길 권장드립니다.

– src/components/KoreanStandardTime.tsx
onToggleMilliseconds?: (checked: boolean) => void prop 추가
onChange 핸들러에서 document.dispatchEvent 대신 onToggleMilliseconds(e.target.checked) 호출

– src/app/result/page.tsx
<KoreanStandardTime … onToggleMilliseconds={setShowMilliseconds} /> 와 같이 상태 변경 콜백 전달

이렇게 하면 글로벌 이벤트 의존도를 제거하고 재사용성, 테스트 용이성을 개선할 수 있습니다.

Comment on lines +24 to +32
// 새로고침 시에도 로그인 유지
useEffect(() => {
const at = localStorage.getItem('accessToken');
const name = localStorage.getItem('userName') || undefined;
if (at) {
setIsAuthed(true);
setUserName(name);
}
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

토큰 만료‧무결성 검증 로직이 없습니다

로컬 스토리지에 토큰이 존재한다는 이유만으로 isAuthedtrue로 설정하면
만료·위조 토큰을 가진 사용자가 인증된 상태로 오인될 수 있습니다.
JWT 만료 확인 혹은 /auth/validate 같은 엔드포인트 호출을 통해 최초 마운트 시 토큰 유효성을 검증해 주세요.


🏁 Script executed:

#!/bin/bash
set -e
echo "----- src/app/page.tsx (lines 1-200) -----"
sed -n '1,200p' src/app/page.tsx

Length of output: 5395


토큰 유효성 검증 로직 추가 필요
로컬 스토리지에 토큰이 존재한다고 곧바로 isAuthedtrue로 설정하면, 만료되었거나 위조된 토큰도 인증된 상태로 오인될 수 있습니다.

• 검증 대상: 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.

Suggested change
// 새로고침 시에도 로그인 유지
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.

Comment on lines +105 to +106
localStorage.setItem('accessToken', data.data.accessToken);
localStorage.setItem('refreshToken', data.data.refreshToken);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Refresh Token을 localStorage 에 저장하면 XSS에 취약합니다

accessToken 뿐 아니라 장기 수명의 refreshToken 도 동일한 DOM 영역에 저장되고 있어,
스크립트 인젝션 하나로 영구 재발급 권한까지 탈취될 수 있습니다.
refreshTokenhttpOnly‧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.

cl-o-lc added 3 commits August 8, 2025 23:27
- Authed/Guest 분기 타입 정의
- 로그인 상태일 때 onLogoutClick 필수, 게스트일 때 onLoginClick 필수
- 상태-프롭 불일치를 컴파일 타임에 검출
- new URL로 슬래시 중복/누락 방지
- 환경 변수 미설정 시 상대 경로로 fallback
Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between bb0b647 and c1dfb2e.

📒 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 분기, 시맨틱 버튼 적용, 이전 런타임 오류 가능성(옵셔널 호출) 대응 모두 적절합니다.

@cl-o-lc cl-o-lc merged commit a8115f7 into main Aug 9, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

design✨ UI 구현 및 개선 feat🛠️ 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants