Skip to content

Comments

Feat: 개발 QA 1차(정민 | 레벨페이지 수정)#110

Merged
jjangminii merged 8 commits intodevelopfrom
refactor/#101/arcons-reflect-refresh
Sep 17, 2025
Merged

Feat: 개발 QA 1차(정민 | 레벨페이지 수정)#110
jjangminii merged 8 commits intodevelopfrom
refactor/#101/arcons-reflect-refresh

Conversation

@jjangminii
Copy link
Collaborator

@jjangminii jjangminii commented Sep 15, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

  • 도토리 레벨 새로고침 안해도 반영
  • 인포카드, 사이드바 레벨카드 치삐 이미지 반영
  • 레벨5(도토리 7개 이상)일 때 다음 도토리 수집 시간 띄우기

⭐ PR Point (To Reviewer)

  • 현재 시간이 새벽 3시인데 다음 리마인드가 5시간뒤가 아닌 29시간 뒤인 것에 대해서 서버와 혹은 기획측과 상의해보도록 하겠습니다 -> 기획측에서 맞는 경우라고 답변 받았습니당
image

📷 Screenshot

image
2025-09-17.3.26.40.mov

Summary by CodeRabbit

  • 개선
    • 북마크·리마인드에서 ‘읽음’ 처리 시 새로고침 대상이 조정되어 기존 읽음/안읽음/리마인드 목록 유지 및 관련 피드(‘arcons’)가 함께 갱신됩니다.
  • 신규 기능
    • 레벨 화면에 다음 획득 시간 표시(NextAcornTime) UI 추가 — 특정 레벨일 때 하단에 시간 정보가 오버레이로 표시됩니다.
  • 버그 수정
    • 일부 화면에서 상태 변경 후 목록 반영 지연이 개선되었습니다.

@jjangminii jjangminii self-assigned this Sep 15, 2025
@jjangminii jjangminii linked an issue Sep 15, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Sep 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
pinback-client-client Ready Ready Preview Comment Sep 17, 2025 5:10am

@coderabbitai
Copy link

coderabbitai bot commented Sep 15, 2025

Walkthrough

React Query 캐시 무효화 대상 일부가 조정되고, 레벨 페이지에 NextAcornTime 컴포넌트와 useCountdown 훅이 추가되었으며, AcornsResponse 타입 필드명 변경 및 사소한 포맷 정리가 포함되었습니다.

Changes

Cohort / File(s) Change Summary
MyBookmark 캐시 무효화 조정
apps/client/src/pages/myBookmark/MyBookmark.tsx
updateToReadStatus의 onSuccess에서 ['categoryBookmarkArticles'] 무효화 제거 및 ['arcons'] 무효화 추가. handleDeleteArticle onSuccess에 공백 줄 추가(동작 없음).
Remind 캐시 무효화 확장
apps/client/src/pages/remind/Remind.tsx
updateToReadStatus onSuccess에 기존 ['remindArticles'] 무효화 외에 ['arcons'] 무효화 추가.
Level UI/훅 추가
apps/client/src/pages/level/Level.tsx, apps/client/src/pages/level/components/NextAcornTime.tsx, apps/client/src/pages/level/hooks/useCountdown.ts
신규 NextAcornTime 컴포넌트 추가(기본 export), Level.tsx에 isLevel5 조건 추가 및 조건부 오버레이 렌더링, useCountdown 훅 추가(타겟 시간까지 HH:MM:SS 타이머).
타입 변경
apps/client/src/shared/types/api.ts
AcornsResponse 타입 필드명 remindDateTimenextRemind로 변경.
사소한 포맷 변경
apps/client/src/shared/apis/queries.ts
sendTokenToExtension 내부 window.postMessage 호출 들여쓰기 조정(동작 변경 없음).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant MB as MyBookmark 페이지
  participant RM as Remind 페이지
  participant M as updateToReadStatus 뮤테이션
  participant S as 서버 API
  participant C as React Query 캐시

  rect rgba(230,245,255,0.6)
    note right of MB: MyBookmark에서 읽음 처리
    U->>MB: "읽음으로 표시" 클릭
    MB->>M: mutate(id, toRead=true)
    M->>S: PATCH /bookmarks/{id}/read
    S-->>M: 200 OK
    M->>C: invalidate(['bookmarkReadArticles'])
    M->>C: invalidate(['bookmarkUnreadArticles'])
    M->>C: invalidate(['arcons'])
  end

  rect rgba(240,230,255,0.6)
    note right of RM: Remind에서 읽음 처리
    U->>RM: "읽음으로 표시" 클릭
    RM->>M: mutate(id, toRead=true)
    M->>S: PATCH /bookmarks/{id}/read
    S-->>M: 200 OK
    M->>C: invalidate(['remindArticles'])
    M->>C: invalidate(['arcons'])
  end

  rect rgba(245,255,240,0.6)
    note right of Level: 레벨 페이지 — 다음 도토리 시간 표시
    participant L as Level 페이지
    participant V as NextAcornTime 컴포넌트
    participant H as useCountdown 훅
    L->>V: nextAcornTime (data.nextRemind)
    V->>H: useCountdown(nextAcornTime)
    H-->>V: "HH:MM:SS" 업데이트(매초)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

frontend, 정민, feat

Suggested reviewers

  • jllee000
  • constantly-dev

Poem

봄밭에 토끼가 깡충, 코드에 풀밭을 심네 🐰
무효화 툭! 도토리 표식 반짝, 캐시가 새로워져요 ✨
다음 도토리 시간은 탁! 카운트가 톡톡,
컴포넌트가 빛나고 훅은 초 단위로 졸졸 ⏳
깡충 — 배포되면 모두가 폴짝 웃겠지!

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#101/arcons-reflect-refresh

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

Pre-merge checks

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning PR은 이슈 #101(도토리 새로고침)의 주요 목표를 충족합니다 — 레벨 페이지에 NextAcornTime 및 useCountdown 추가, AcornsResponse 타입의 필드(nextRemind) 업데이트, 그리고 북마크/리마인드 후 ['arcons'] 캐시 무효화로 즉시 반영을 시도하는 변경이 포함되어 있습니다; 반면 이 PR은 이슈 #25(Progress 컴포넌트 구현)의 핵심 요구사항(Progress 컴포넌트와 스토리북·의존성 추가 등)을 포함하고 있지 않습니다. 해결책: linked_issues에서 #25를 제거하거나 이번 PR에 #25의 요구사항을 포함하도록 추가 커밋을 올려 주세요(예: Progress 컴포넌트, 스토리북 스토리, @radix 의존성 등); 어느 쪽을 선택했는지 PR 본문에 명시해 주시기 바랍니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 'Feat: 개발 QA 1차(정민
Out of Scope Changes Check ✅ Passed 변경된 파일들(레벨 관련 새 컴포넌트 및 훅, AcornsResponse 타입 변경, 북마크/리마인드의 ['arcons'] 캐시 무효화 추가 등)은 이슈 #101의 목적(도토리 레벨 즉시 반영)을 지원하는 범위로 보이며, 기능과 무관한 대규모 외부 기능 추가는 발견되지 않았습니다; 일부 파일의 포맷팅 변경은 무해합니다.
Description Check ✅ Passed PR 설명은 저장소의 템플릿 구조를 전반적으로 잘 따르고 있습니다. "📌 Related Issues"에 close #101이 명시되어 있고 "📄 Tasks", "⭐ PR Point", "📷 Screenshot" 섹션이 모두 채워져 있어 리뷰어가 필요한 맥락과 스크린샷을 확인할 수 있습니다. 필수 항목들이 포함되어 있어 설명은 대부분 완전하다고 판단됩니다.

@github-actions github-actions bot added the refactor 코드 리팩토링 label Sep 15, 2025
@github-actions
Copy link

github-actions bot commented Sep 15, 2025

✅ Storybook chromatic 배포 확인:
🐿️ storybook

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

58-72: 버그: close()는 window.close를 호출합니다 — 의도대로 closeMenu()로 교체 필요

close()는 전역 window.close()로 해석되어 창 닫기 시도를 합니다. 메뉴 닫힘 의도라면 closeMenu()여야 합니다.

-        close();
+        closeMenu();
🧹 Nitpick comments (1)
apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

41-43: 불필요한 콘솔 로그 제거 권장

배포 빌드에 잔류하지 않도록 정리해 주세요. 필요 시 console.debug로 대체하거나 환경 플래그로 가드하세요.

-  console.log('categoryArticles', categoryArticles);
+  // console.debug('categoryArticles', categoryArticles);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c8069e and f703a26.

📒 Files selected for processing (2)
  • apps/client/src/pages/level/Level.tsx (1 hunks)
  • apps/client/src/pages/myBookmark/MyBookmark.tsx (2 hunks)
🔇 Additional comments (2)
apps/client/src/pages/level/Level.tsx (1)

12-15: 쿼리 키 일치성 확인 필요 — useGetArcons의 queryKey가 ['arcons']인지 검증 및 QueryKey 팩토리 도입 권장

invalidateQueries(['arcons'])가 새로고침 없이 반영되려면 useGetArcons가 정확히 ['arcons'] 쿼리키를 사용해야 합니다. 스크립트로 구현을 찾지 못해(파일 검색 실패) 로컬에서 useGetArcons 구현과 queryKey를 확인하고, 쿼리 키를 상수/팩토리로 통일하세요.

apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

142-146: LGTM — 읽음 처리 후 arcons 무효화 추가 (queryKey 일치 확인됨)
apps/client/src/shared/apis/queries.ts의 useGetArcons가 queryKey: ['arcons']로 정의되어 있고, apps/client/src/pages/myBookmark/MyBookmark.tsx에서 동일한 ['arcons']로 invalidateQueries가 호출되고 있습니다.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/client/src/pages/remind/Remind.tsx (1)

49-53: 버그: close()는 정의되어 있지 않습니다. closeMenu()로 교체 필요

상단에서 useAnchoredMenu 반환값을 close: closeMenu로 디스트럭처링했고, close 식별자는 존재하지 않습니다. 현재 코드는 런타임 ReferenceError를 유발합니다.

         queryClient.invalidateQueries({ queryKey: ['remindArticles'] });
-        close();
+        closeMenu();
🧹 Nitpick comments (1)
apps/client/src/pages/remind/Remind.tsx (1)

104-109: Query key 하드코딩 최소화 제안

문자열 키 하드코딩 대신 중앙화된 queryKeys 유틸을 도입하면 오타/불일치 리스크를 줄일 수 있습니다. 예: queryKeys.arcons()로 통일.

예시:

// queryKeys.ts
export const queryKeys = {
  remindArticles: () => ['remindArticles'] as const,
  arcons: () => ['arcons'] as const,
};

// 사용처
queryClient.invalidateQueries({ queryKey: queryKeys.remindArticles() });
queryClient.invalidateQueries({ queryKey: queryKeys.arcons() });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f703a26 and f41dddd.

⛔ Files ignored due to path filters (6)
  • packages/design-system/src/icons/source/chippi_profile.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_1.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_2.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_3.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_4.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_5.svg is excluded by !**/*.svg
📒 Files selected for processing (1)
  • apps/client/src/pages/remind/Remind.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: storybook
🔇 Additional comments (1)
apps/client/src/pages/remind/Remind.tsx (1)

107-109: 승인 — 'arcons' 쿼리 키 일관성 확인됨

apps/client/src/shared/apis/queries.ts(59)에서 queryKey: ['arcons']로 정의되어 있고 apps/client/src/pages/remind/Remind.tsx(108) 및 apps/client/src/pages/myBookmark/MyBookmark.tsx(145)에서 invalidateQueries로 동일 키를 사용합니다. 'acorn'은 쿼리 키가 아니라 lock 파일 등에서 패키지명으로만 발견됩니다.

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: 1

🧹 Nitpick comments (4)
apps/client/src/pages/level/components/NextAcornTime.tsx (3)

24-26: 접근성: 시간 값은 <time> + role="timer" + aria-atomic로 노티파이하세요.

라이브 업데이트 시 스크린 리더 공지가 더 안정적입니다. 의미론도 좋아집니다.

-      <span className="body2-m text-main400 tabular-nums" aria-live="polite">
-        {nextAcornTime}✨
-      </span>
+      <time
+        className="body2-m text-main400 tabular-nums"
+        aria-live="polite"
+        aria-atomic="true"
+        role="timer"
+        dateTime={nextAcornTime}
+      >
+        {nextAcornTime}✨
+      </time>

20-20: aria-hidden 값 명시

불리언을 명시하면 의도가 분명해집니다.

-      <Icon name="ic_clock_active" width={20} height={20} aria-hidden />
+      <Icon name="ic_clock_active" width={20} height={20} aria-hidden={true} />

21-23: 하드코딩 텍스트의 i18n 여부 확인

제품이 i18n 대상이면 리소스 키로 분리해 주세요.

apps/client/src/pages/level/Level.tsx (1)

20-20: 네이밍/매직 넘버 정리 제안

isLevel5는 의미가 혼재됩니다(레벨5 OR 데일리 캡). 상수와 의도형 네이밍으로 분리하세요.

-  const isLevel5 = info.level === 5 || acornCount >= 7;
+  const DAILY_ACORN_CAP = 7; // TODO: 서버/공용 상수로 단일화
+  const shouldShowNextAcornTime = info.level === 5 || acornCount >= DAILY_ACORN_CAP;

아래 사용처도 함께 변경:

-          {isLevel5 && (
+          {shouldShowNextAcornTime && (

질문: 데일리 캡 7개는 서버에서 내려오는 값인가요? 클라이언트 매직 넘버면 공용 상수로 중앙화가 필요합니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f41dddd and 2b12bbe.

📒 Files selected for processing (2)
  • apps/client/src/pages/level/Level.tsx (3 hunks)
  • apps/client/src/pages/level/components/NextAcornTime.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-04T08:38:00.620Z
Learnt from: jjangminii
PR: Pinback-Team/pinback-client#54
File: apps/client/src/pages/level/components/TreeStatusCard.tsx:12-12
Timestamp: 2025-09-04T08:38:00.620Z
Learning: In TreeStatusCard component, the progress bar calculation `info.level * 20` is intentional design - level 1 starts at 20%, level 2 at 40%, etc., up to level 5 at 100%. This ensures no level shows 0% progress for UX purposes.

Applied to files:

  • apps/client/src/pages/level/Level.tsx
🧬 Code graph analysis (1)
apps/client/src/pages/level/components/NextAcornTime.tsx (1)
packages/design-system/src/lib/utils.ts (1)
  • cn (108-110)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: storybook
🔇 Additional comments (2)
apps/client/src/pages/level/components/NextAcornTime.tsx (1)

15-19: 구조/스타일 합성 방식 LGTM

cn으로 베이스 스타일 + 외부 className 합성 방식 적절합니다.

apps/client/src/pages/level/Level.tsx (1)

10-10: 컴포넌트 분리 import LGTM

경로/책임 분리 적절합니다.

Comment on lines 61 to 66
{isLevel5 && (
<NextAcornTime
className="absolute bottom-[5.2rem] left-1/2 z-[10] -translate-x-1/2"
nextAcornTime={'22:22:22'}
/>
)}
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

하드코딩된 시간 값은 기능 오동작 위험(고정 ‘22:22:22’).

실데이터/계산 값으로 교체하세요. 또한 React Query 무효화 시 이 값이 갱신되는지 확인 필요합니다.

-            <NextAcornTime
+            <NextAcornTime
               className="absolute bottom-[5.2rem] left-1/2 z-[10] -translate-x-1/2"
-              nextAcornTime={'22:22:22'}
+              nextAcornTime={nextAcornTime}
             />

추가(파일 상단 계산부 인근):

// 예시: 실제 필드에 맞게 매핑/포맷하세요.
const nextAcornTime = data?.nextAcornTime ?? ''; // TODO: 서버 스키마에 맞춰 치환

검증 포인트

  • acorn 증가/소모 후 ['arcons']가 invalidate되어 datanextAcornTime가 즉시 반영되는지.

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching repository for NextAcornTime, hardcoded '22:22:22', and nextAcornTime..."
rg -n --hidden --glob '!node_modules' "NextAcornTime" || true
rg -n --hidden --glob '!node_modules' "22:22:22" || true
rg -n --hidden --glob '!node_modules' "nextAcornTime" || true

echo
echo "Printing apps/client/src/pages/level/Level.tsx (if exists):"
if [ -f "apps/client/src/pages/level/Level.tsx" ]; then
  sed -n '1,240p' apps/client/src/pages/level/Level.tsx || true
else
  echo "File not found: apps/client/src/pages/level/Level.tsx"
fi

Length of output: 3365


하드코딩된 시간 제거 — 실제 API/쿼리값으로 교체 및 갱신 검증 필요

Level.tsx에서 NextAcornTime에 '22:22:22'이 하드코딩되어 있습니다. 서버/쿼리에서 받은 값으로 교체하고, 도토리 변경 후 React Query 무효화 시 값이 즉시 반영되는지 확인하세요.

파일: apps/client/src/pages/level/Level.tsx — isLevel5 블록(NextAcornTime 호출)

-            <NextAcornTime
+            <NextAcornTime
               className="absolute bottom-[5.2rem] left-1/2 z-[10] -translate-x-1/2"
-              nextAcornTime={'22:22:22'}
+              nextAcornTime={nextAcornTime}
             />

추가(파일 상단 계산부 인근):

// 예시: 실제 필드에 맞게 매핑/포맷하세요.
const nextAcornTime = data?.nextAcornTime ?? ''; // TODO: 서버 스키마에 맞춰 치환

검증 포인트

  • acorn 증가/소모 후 ['arcons']가 invalidate되어 data.nextAcornTime(또는 파생값)이 즉시 반영되는지 확인.

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: 2

♻️ Duplicate comments (1)
apps/client/src/pages/level/Level.tsx (1)

68-73: 하드코딩 값 제거 반영 확인 — 잘 처리되었습니다

이전 리뷰의 '22:22:22' 하드코딩 이슈가 nextAcornTime으로 치환되어 목적에 부합합니다.

🧹 Nitpick comments (5)
apps/client/src/pages/level/hooks/useCountdown.ts (1)

3-4: 카운트다운 종료 시 후속 동작(onComplete) 콜백 지원 제안

레벨/도토리 상태를 즉시 반영하려면 00:00:00 도달 시 쿼리 무효화 등을 실행할 수 있도록 콜백을 받는 형태가 유용합니다.

-export function useCountdown(targetTime: string) {
+export function useCountdown(targetTime: string, onComplete?: () => void) {
   const [timeLeft, setTimeLeft] = useState('00:00:00');

위 효과 내부 종료 분기에서 onComplete?.() 호출을 추가하세요.

-    if (!update()) return;
+    if (!update()) {
+      onComplete?.();
+      return;
+    }
...
-      if (!update()) {
+      if (!update()) {
+        onComplete?.();
         clearInterval(id);
       }
apps/client/src/pages/level/Level.tsx (4)

26-27: 디버깅 로그 제거

프로덕션 번들에 남지 않도록 console.log는 제거하세요.

-  console.log(data.acornCount, data.nextRemind);
-  console.log('현재시간:', now);

21-21: 사용되지 않는 변수 제거 및 관련 import 정리

now는 로그 제거 후 미사용입니다. 함께 제거하고 formatLocalDateTime import도 정리하세요.

-  const now = formatLocalDateTime(new Date());

추가(라인 11):

-import { formatLocalDateTime } from '@shared/utils/formatDateTime';

68-73: nextAcornTime 존재 여부까지 가드

API 스키마가 null/빈 문자열을 반환할 가능성에 대비해 표시 가드를 추가하세요.

-{isLevel5 && (
+{isLevel5 && !!nextAcornTime && (
   <NextAcornTime
     className="absolute bottom-[5.2rem] left-1/2 z-[10] -translate-x-1/2"
     nextAcornTime={nextAcornTime}
   />
 )}

24-25: 매직 넘버 의미 부여

level === 5>= 7은 상수/도메인 규칙로 추출해 의미를 드러내세요. (예: MAX_TREE_LEVEL, REWARD_THRESHOLD 등)

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b12bbe and 7d075a5.

📒 Files selected for processing (5)
  • apps/client/src/pages/level/Level.tsx (3 hunks)
  • apps/client/src/pages/level/components/NextAcornTime.tsx (1 hunks)
  • apps/client/src/pages/level/hooks/useCountdown.ts (1 hunks)
  • apps/client/src/shared/apis/queries.ts (1 hunks)
  • apps/client/src/shared/types/api.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/client/src/shared/apis/queries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/client/src/pages/level/components/NextAcornTime.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/client/src/pages/level/Level.tsx (2)
apps/client/src/shared/utils/formatDateTime.ts (1)
  • formatLocalDateTime (1-9)
apps/client/src/pages/level/components/NextAcornTime.tsx (1)
  • NextAcornTime (10-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: storybook

Comment on lines +6 to +28
useEffect(() => {
const target = new Date(targetTime).getTime();

const interval = setInterval(() => {
const now = new Date().getTime();
const diff = target - now;

if (diff <= 0) {
setTimeLeft('00:00:00');
clearInterval(interval);
return;
}

const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);

const format = (n: number) => String(n).padStart(2, '0');
setTimeLeft(`${format(hours)}:${format(minutes)}:${format(seconds)}`);
}, 1000);

return () => clearInterval(interval);
}, [targetTime]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

유효하지 않은 날짜 입력 시 'NaN:NaN:NaN' 표시 가능 — 방어 로직과 초기 1회 갱신 추가 제안

new Date(targetTime)Invalid Date이면 diffNaN이 되어 UI에 NaN:NaN:NaN이 찍힐 수 있습니다. 또한 마운트 직후 1초간 '00:00:00'이 보이는 UX 지연이 있습니다. 아래처럼 유효성 검사, 즉시 1회 갱신, Date.now() 사용으로 보강하세요.

-  useEffect(() => {
-    const target = new Date(targetTime).getTime();
-
-    const interval = setInterval(() => {
-      const now = new Date().getTime();
-      const diff = target - now;
-
-      if (diff <= 0) {
-        setTimeLeft('00:00:00');
-        clearInterval(interval);
-        return;
-      }
-
-      const hours = Math.floor(diff / (1000 * 60 * 60));
-      const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
-      const seconds = Math.floor((diff % (1000 * 60)) / 1000);
-
-      const format = (n: number) => String(n).padStart(2, '0');
-      setTimeLeft(`${format(hours)}:${format(minutes)}:${format(seconds)}`);
-    }, 1000);
-
-    return () => clearInterval(interval);
-  }, [targetTime]);
+  useEffect(() => {
+    const targetMs = Date.parse(targetTime);
+    const format = (n: number) => String(n).padStart(2, '0');
+    // 유효하지 않은 날짜 문자열 방어
+    if (!targetTime || Number.isNaN(targetMs)) {
+      setTimeLeft('00:00:00');
+      return;
+    }
+
+    const update = () => {
+      const diff = targetMs - Date.now();
+      if (diff <= 0) {
+        setTimeLeft('00:00:00');
+        return false;
+      }
+      const hours = Math.floor(diff / 3_600_000);
+      const minutes = Math.floor((diff % 3_600_000) / 60_000);
+      const seconds = Math.floor((diff % 60_000) / 1_000);
+      setTimeLeft(`${format(hours)}:${format(minutes)}:${format(seconds)}`);
+      return true;
+    };
+
+    // 즉시 1회 갱신 후 interval 시작
+    if (!update()) return;
+    const id = window.setInterval(() => {
+      if (!update()) {
+        clearInterval(id);
+      }
+    }, 1000);
+    return () => clearInterval(id);
+  }, [targetTime]);
📝 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 target = new Date(targetTime).getTime();
const interval = setInterval(() => {
const now = new Date().getTime();
const diff = target - now;
if (diff <= 0) {
setTimeLeft('00:00:00');
clearInterval(interval);
return;
}
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
const format = (n: number) => String(n).padStart(2, '0');
setTimeLeft(`${format(hours)}:${format(minutes)}:${format(seconds)}`);
}, 1000);
return () => clearInterval(interval);
}, [targetTime]);
useEffect(() => {
const targetMs = Date.parse(targetTime);
const format = (n: number) => String(n).padStart(2, '0');
// 유효하지 않은 날짜 문자열 방어
if (!targetTime || Number.isNaN(targetMs)) {
setTimeLeft('00:00:00');
return;
}
const update = () => {
const diff = targetMs - Date.now();
if (diff <= 0) {
setTimeLeft('00:00:00');
return false;
}
const hours = Math.floor(diff / 3_600_000);
const minutes = Math.floor((diff % 3_600_000) / 60_000);
const seconds = Math.floor((diff % 60_000) / 1_000);
setTimeLeft(`${format(hours)}:${format(minutes)}:${format(seconds)}`);
return true;
};
// 즉시 1회 갱신 후 interval 시작
if (!update()) return;
const id = window.setInterval(() => {
if (!update()) {
clearInterval(id);
}
}, 1000);
return () => clearInterval(id);
}, [targetTime]);
🤖 Prompt for AI Agents
In apps/client/src/pages/level/hooks/useCountdown.ts around lines 6-28, the
effect currently can compute NaN when new Date(targetTime) is invalid and only
updates after 1s; validate targetTime/date first and bail to a safe fallback
(e.g., setTimeLeft('00:00:00') and do not start the interval) when Date is
invalid, perform an immediate single update before starting setInterval so UI
shows correct time on mount, use Date.now() instead of new Date().getTime() for
now, and keep the existing cleanup (clearInterval) to avoid leaks.

Comment on lines 11 to 14
export type AcornsResponse = {
acornCount: number;
remindDateTime: string;
nextRemind: string;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

시간 필드의 스키마 명확화(타임존/널 가능성) 및 전역 치환 검증 필요

nextRemind: string은 타임존 포함 여부가 불명확합니다. ISO‑8601(Z 포함) 보장을 명시하고, 값이 없을 수 있으면 null 허용을 권장합니다. 또한 기존 remindDateTime 참조가 남아있지 않은지 리포지토리 전역 검증이 필요합니다.

 export type AcornsResponse = {
   acornCount: number;
-  nextRemind: string;
+  /** ISO-8601 (예: 2025-09-16T12:34:56Z). 없으면 null */
+  nextRemind: string | null;
 };

검증 스크립트:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# 과거 명칭 사용 흔적 탐지
rg -nP --hidden --glob '!**/node_modules/**' '\b(remind(ed)?DateTime|remindDateTime)\b' -C2 || true
# 새 필드 사용 맥락 확인
rg -nP --hidden --glob '!**/node_modules/**' '\bnextRemind\b' -C2 || true

Length of output: 297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== pwd ==="
pwd || true

echo
echo "=== ls (root, first 40) ==="
ls -la | sed -n '1,40p' || true

echo
echo "=== git info ==="
git rev-parse --is-inside-work-tree 2>/dev/null || true
git status --porcelain -b 2>/dev/null || true

echo
echo "=== rg: remindDateTime variants (include ignored/hidden) ==="
rg -nP -uu '\b(remind(ed)?DateTime|remindDateTime)\b' -C2 || true

echo
echo "=== rg: nextRemind (include ignored/hidden) ==="
rg -nP -uu '\bnextRemind\b' -C2 || true

echo
echo "=== Check specific file from PR: apps/client/src/shared/types/api.ts ==="
if [ -f "apps/client/src/shared/types/api.ts" ]; then
  echo "FOUND: apps/client/src/shared/types/api.ts"
  sed -n '1,200p' apps/client/src/shared/types/api.ts || true
else
  echo "NOT FOUND: apps/client/src/shared/types/api.ts"
fi

echo
echo "=== Search for AcornsResponse across .ts files ==="
rg -n --hidden --glob '!**/node_modules/**' 'AcornsResponse' -S -g '**/*.ts' || true

echo
echo "=== Summary counts ==="
rg -nP -uu '\b(remind(ed)?DateTime|remindDateTime)\b' --count || true
rg -nP -uu '\bnextRemind\b' --count || true

Length of output: 3677


시간 필드 스펙화 — nextRemind를 ISO‑8601(타임존 포함)으로 문서화하고 null 허용, 소비처 수정 필요

검증 결과: 리포지토리에 remindDateTime 관련 참조는 없음. nextRemind는 apps/client/src/shared/types/api.ts에 string으로 선언되어 있으며, apps/client/src/pages/level/Level.tsx(사용부)와 apps/client/src/shared/apis/queries.ts(타입 임포트/훅)에서 소비되고 있음.
권장: nextRemind를 ISO‑8601(Z 포함)임을 주석으로 명시하고, 값이 없을 수 있으면 null 허용으로 변경 — 변경 시 Level.tsx 및 관련 훅/사용부를 null-safe하게 처리(형 검사 또는 기본값 적용).

 export type AcornsResponse = {
   acornCount: number;
-  nextRemind: string;
+  /** ISO-8601 (예: 2025-09-16T12:34:56Z). 없으면 null */
+  nextRemind: string | null;
 };
📝 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
export type AcornsResponse = {
acornCount: number;
remindDateTime: string;
nextRemind: string;
};
export type AcornsResponse = {
acornCount: number;
/** ISO-8601 (예: 2025-09-16T12:34:56Z). 없으면 null */
nextRemind: string | null;
};
🤖 Prompt for AI Agents
In apps/client/src/shared/types/api.ts around lines 11 to 14, the
AcornsResponse.nextRemind field is declared as string but should be documented
and typed as an ISO‑8601 timestamp including timezone (e.g. Z) and allow null;
change the type to string | null and add a short comment that the value is an
ISO‑8601 datetime with timezone, then update the consumers
(apps/client/src/pages/level/Level.tsx and
apps/client/src/shared/apis/queries.ts) to handle null safely (add null checks
or supply a fallback/default and adjust any type guards/usages accordingly).

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

♻️ Duplicate comments (1)
apps/client/src/pages/level/Level.tsx (1)

63-68: 하드코딩 시간 제거, 데이터 연동 확인 완료

이전 커멘트의 '22:22:22' 하드코딩 이슈가 해소되었고, 이제 API 응답(nextRemind)으로 연결되었습니다. 좋습니다.

🧹 Nitpick comments (3)
apps/client/src/pages/level/Level.tsx (3)

10-10: 경로 스타일 일관화: 상대 경로 대신 path alias 권장

이 파일의 다른 import와 동일하게 @pages/... alias를 사용하면 유지보수성과 번들 중복 위험을 줄일 수 있습니다.

-import NextAcornTime from './components/NextAcornTime';
+import NextAcornTime from '@pages/level/components/NextAcornTime';

22-22: 매직 넘버(7) 제거 — 레벨 기준은 단일 소스(getTreeLevel)로 결정

레벨 테이블이 바뀌면 >= 7 하드코딩과 getTreeLevel의 기준이 어긋날 수 있습니다. info.level만으로 표시 조건을 판단하는 편이 안전합니다.

-const isLevel5 = info.level === 5 || acornCount >= 7;
+const isLevel5 = info.level === 5;

63-68: 값 미존재 시 렌더링 가드 추가 (방어코드)

nextRemind가 일시적으로 없을 때까지도 컴포넌트를 렌더링하면 카운트다운 훅에서 예외/NaN이 발생할 수 있습니다. 존재 여부를 함께 체크하세요.

-{isLevel5 && (
+{isLevel5 && nextAcornTime && (
   <NextAcornTime
     className="absolute bottom-[5.2rem] left-1/2 z-[10] -translate-x-1/2"
     nextAcornTime={nextAcornTime}
   />
 )}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d075a5 and e758466.

📒 Files selected for processing (1)
  • apps/client/src/pages/level/Level.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/client/src/pages/level/Level.tsx (2)
apps/client/src/shared/utils/treeLevel.ts (1)
  • getTreeLevel (28-31)
apps/client/src/pages/level/components/NextAcornTime.tsx (1)
  • NextAcornTime (10-32)
🔇 Additional comments (1)
apps/client/src/pages/level/Level.tsx (1)

19-19: 서버 타임존/포맷 확인 필요 — useCountdown이 new Date(targetTime)로 파싱합니다

  • 발견: apps/client/src/pages/level/hooks/useCountdown.ts에서 targetTime을 new Date(targetTime).getTime()로 파싱합니다.
  • 상태: 코드 검색에서 'nextRemind'의 생성/타입(포맷)을 찾지 못했습니다 — data.nextRemind의 포맷(ISO±TZ / epoch(ms) / 타임존 없음) 확인 필요.
  • 권장 조치: 서버가 nextRemind를 ISO 8601 + 타임존(예: 2025-09-16T12:00:00Z) 또는 epoch(ms)로 내려주도록 하거나, 클라이언트에서 명시적 UTC 파싱(예: dayjs/luxon) 또는 useCountdown을 number 지원으로 변경하세요. 임시 해결: 타임존이 누락된 문자열이면 끝에 'Z'를 붙여 UTC로 파싱하도록 처리하세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix] sp1 QA 레벨페이지 1차 반영

1 participant