Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughReact Query 캐시 무효화 대상 일부가 조정되고, 레벨 페이지에 NextAcornTime 컴포넌트와 useCountdown 훅이 추가되었으며, AcornsResponse 타입 필드명 변경 및 사소한 포맷 정리가 포함되었습니다. Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing touches
🧪 Generate unit tests
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.
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. Comment Pre-merge checks❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
|
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
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
📒 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가 호출되고 있습니다.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (6)
packages/design-system/src/icons/source/chippi_profile.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_1.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_2.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_3.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_4.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_5.svgis 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 파일 등에서 패키지명으로만 발견됩니다.
There was a problem hiding this comment.
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
📒 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경로/책임 분리 적절합니다.
| {isLevel5 && ( | ||
| <NextAcornTime | ||
| className="absolute bottom-[5.2rem] left-1/2 z-[10] -translate-x-1/2" | ||
| nextAcornTime={'22:22:22'} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
💡 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되어data와nextAcornTime가 즉시 반영되는지.
🏁 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"
fiLength 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(또는 파생값)이 즉시 반영되는지 확인.
There was a problem hiding this comment.
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는 로그 제거 후 미사용입니다. 함께 제거하고formatLocalDateTimeimport도 정리하세요.- 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
📒 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
| 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]); |
There was a problem hiding this comment.
유효하지 않은 날짜 입력 시 'NaN:NaN:NaN' 표시 가능 — 방어 로직과 초기 1회 갱신 추가 제안
new Date(targetTime)가 Invalid Date이면 diff가 NaN이 되어 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.
| 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.
| export type AcornsResponse = { | ||
| acornCount: number; | ||
| remindDateTime: string; | ||
| nextRemind: string; | ||
| }; |
There was a problem hiding this comment.
🛠️ 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 || trueLength 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 || trueLength 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.
| 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).
There was a problem hiding this comment.
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
📒 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로 파싱하도록 처리하세요.
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
📷 Screenshot
2025-09-17.3.26.40.mov
Summary by CodeRabbit