Conversation
WalkthroughLayout에서 OptionsMenuButton을 제거하고 Outlet만 렌더링하도록 정리했습니다. Level 페이지는 자체 상태(acorns=1 하드코드)로 getTreeLevel을 호출해 LevelScene, LevelInfoCard, Badge, TreeStatusCard 등을 렌더링하도록 교체되었고, LevelScene과 타입/유틸 리팩터가 추가되었습니다. 테마 변수명이 변경되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant LevelPage as Level (pages/level/Level.tsx)
participant Utils as getTreeLevel
participant Scene as LevelScene
participant Info as LevelInfoCard
participant Status as TreeStatusCard
User->>LevelPage: 페이지 진입 (acorns 기본값)
LevelPage->>Utils: getTreeLevel(acorns)
Utils-->>LevelPage: info { level, progress... }
LevelPage->>Scene: render(level = info.level)
LevelPage->>Status: render(acorns)
User->>LevelPage: 정보 버튼 클릭
LevelPage->>Info: InfoCard 오버레이 표시
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
✨ 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. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (6)
apps/client/src/pages/level/components/LevelScene.tsx (2)
12-19: 타입 안정성 개선:satisfies로 키 누락 방지
Record<TreeLevel, string>명시와as const를 함께 쓰는 대신,satisfies로 키 완전성을 보장하면서 리터럴 타입을 유지하세요.-const SCENE_BY_LEVEL: Record<TreeLevel, string> = { +const SCENE_BY_LEVEL = { -} as const; +} as const satisfies Record<TreeLevel, string>;
31-37: 이미지 로딩 최적화 소소한 개선Above-the-fold가 아니라면
loading="lazy"와decoding="async"추가를 고려하세요.- <img + <img src={src} alt={`지식나무 숲 Lv.${level}`} className="h-auto w-full select-none rounded-[1.2rem] object-cover" + loading="lazy" + decoding="async" draggable={false} />apps/client/src/pages/level/Level.tsx (4)
9-14: 필수 prop에 디폴트값이 의미 없음 — 선택(prop)화 또는 디폴트 제거
LevelPageProps에서acorns가 필수인데, 함수 파라미터에 기본값이 있어도 TS 상으론 전달 강제입니다. 선택(prop)으로 바꾸거나 디폴트를 제거하세요. 기본값 유지가 목적이면 아래처럼 권장합니다.-interface LevelPageProps { - acorns: number; -} +interface LevelPageProps { + acorns?: number; +} -export default function Level({ acorns = 3 }: LevelPageProps) { +export default function Level({ acorns = 3 }: LevelPageProps) {
19-19: 불필요한 캐스팅 제거
getTreeLevel이 반환하는info.level이 이미TreeLevel이라면 캐스팅 불필요합니다.- <LevelScene level={info.level as TreeLevel} /> + <LevelScene level={info.level} />
26-34: 접근성: Hover 전용 토글은 터치 환경에서 비노출모바일/터치에서
hover가 없어 정보 카드 접근이 어려울 수 있습니다. 클릭/포커스 토글이 되는 접근성 컴포넌트(예:@radix-ui/react-popover또는 디자인시스템 Popover)를 고려하세요.aria-expanded,aria-controls,aria-haspopup등을 함께 설정하면 좋습니다.
46-48: 불필요 TODO 주석 정리이미 Badge를 구현했으므로 TODO 주석은 제거해 주세요.
- {/* TODO: 오늘 모은 도토리 개수 배지 */} <Badge text={'오늘 모은 도토리 개수'} countNum={acorns} />
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (5)
apps/client/src/assets/chippi_level1.pngis excluded by!**/*.pngapps/client/src/assets/chippi_level2.pngis excluded by!**/*.pngapps/client/src/assets/chippi_level3.pngis excluded by!**/*.pngapps/client/src/assets/chippi_level4.pngis excluded by!**/*.pngapps/client/src/assets/chippi_level5.pngis excluded by!**/*.png
📒 Files selected for processing (4)
apps/client/src/layout/Layout.tsx(0 hunks)apps/client/src/pages/level/Level.tsx(1 hunks)apps/client/src/pages/level/components/LevelScene.tsx(1 hunks)packages/tailwind-config/shared-styles.css(1 hunks)
💤 Files with no reviewable changes (1)
- apps/client/src/layout/Layout.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-04T08:38:00.583Z
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.583Z
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/Level.tsx (4)
apps/client/src/pages/level/utils/treeLevel.ts (2)
getTreeLevel(47-50)TreeLevel(17-17)packages/design-system/src/lib/utils.ts (1)
cn(108-110)apps/client/src/pages/level/components/LevelInfoCard.tsx (1)
LevelInfoCard(14-68)apps/client/src/pages/level/components/TreeStatusCard.tsx (1)
TreeStatusCard(9-38)
🪛 Biome (2.1.2)
packages/tailwind-config/shared-styles.css
[error] 183-183: expected , but instead found --color-subcolor
Remove --color-subcolor
(parse)
[error] 183-183: Unexpected value or character.
Expected one of:
(parse)
[error] 183-183: expected , but instead found ;
Remove ;
(parse)
⏰ 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
| /* Secondary Color */ | ||
| --color-secondary: #f4f2ec; | ||
| --color-subcolor: #f4f2ec; | ||
|
|
||
| /* Grayscale */ |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
토큰명 리네임은 합리적이나 Biome 파서 오류로 CI 차단 가능
Tailwind v4 @theme 문법을 Biome(2.1.2)이 아직 정상 파싱하지 못해 parse 에러가 납니다. 코드 자체는 정상 CSS이므로 린터 설정/우회가 필요합니다.
권장 조치:
- 단기: 해당 파일에서 Biome 파싱을 비활성화(override)하거나
@theme블록만 무시하도록 설정. - 중기: Biome 업데이트 또는
packages/tailwind-config/**/*.css경로를 Biome 대상에서 제외.
추가로, 점진적 마이그레이션을 위해 이전 토큰도 함께 노출해두면 레거시 클래스(bg-secondary)가 남아 있어도 안전합니다.
/* Secondary Color */
- --color-subcolor: #f4f2ec;
+ --color-subcolor: #f4f2ec;
+ /* temporary alias for backward-compat */
+ --color-secondary: var(--color-subcolor);확인 스크립트:
🏁 Script executed:
#!/bin/bash
# 1) 레거시 사용처 존재 여부
rg -nP 'bg-secondary|--color-secondary'
# 2) Biome가 해당 파일에서만 실패하는지 확인
pnpm biome check packages/tailwind-config/shared-styles.css || trueLength of output: 11255
Biome 파서 오류로 CI 실패 확인 – 단기 대응 필요
- Biome(2.1.2)가
packages/tailwind-config/shared-styles.css의@theme블록 내 CSS 변수 정의를 파싱하지 못해 207개 에러 발생 확인 - 단기: 해당 파일에 대해 Biome 파서 무시/override 설정 추가
- 중기: Biome 최신 버전으로 업그레이드하거나
packages/tailwind-config/**/*.css경로를 검사 대상에서 제외 - 레거시 토큰(
--color-secondary) 사용처(packages/design-system/styles.css)가 존재하므로, 점진적 마이그레이션을 위해 alias 변수 추가 권장
/* Secondary Color */
+ /* temporary alias for backward-compat */
+ --color-secondary: var(--color-subcolor);📝 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.
| /* Secondary Color */ | |
| --color-secondary: #f4f2ec; | |
| --color-subcolor: #f4f2ec; | |
| /* Grayscale */ | |
| /* Secondary Color */ | |
| /* temporary alias for backward-compat */ | |
| --color-secondary: var(--color-subcolor); | |
| --color-subcolor: #f4f2ec; | |
| /* Grayscale */ |
🧰 Tools
🪛 Biome (2.1.2)
[error] 183-183: expected , but instead found --color-subcolor
Remove --color-subcolor
(parse)
[error] 183-183: Unexpected value or character.
Expected one of:
(parse)
[error] 183-183: expected , but instead found ;
Remove ;
(parse)
🤖 Prompt for AI Agents
In packages/tailwind-config/shared-styles.css around lines 182 to 185, Biome
2.1.2 fails parsing CSS variable definitions inside the @theme block causing CI
errors; as a short-term fix add a Biome parser override/ignore entry for this
file (or for packages/tailwind-config/**/*.css) in the repo's Biome config so
the file is skipped by the parser, and also add a legacy alias variable
declaration mapping --color-secondary to --color-subcolor within this CSS file
to keep compatibility with packages/design-system/styles.css; for mid-term
consider upgrading Biome or excluding the path from parsing altogether.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
apps/client/src/pages/level/Level.tsx (1)
36-41: 숨김 상태에서도 클릭되는 오버레이 — pointer-events 기본값 수정보이지 않을 때(
opacity:0)도 클릭/포커스가 가능합니다. 기본을pointer-events-none으로 두고 보일 때만 활성화하세요. (이전 코멘트와 동일 제안)- className={cn( - 'pointer-events-auto absolute left-0 top-[2.4rem] z-[20]', - 'opacity-0 transition-opacity duration-150', - 'peer-hover:opacity-100 peer-focus-visible:opacity-100' - )} + className={cn( + 'pointer-events-none absolute left-0 top-[2.4rem] z-[20]', + 'opacity-0 transition-opacity duration-150', + 'peer-hover:pointer-events-auto peer-focus-visible:pointer-events-auto', + 'peer-hover:opacity-100 peer-focus-visible:opacity-100' + )}
🧹 Nitpick comments (2)
apps/client/src/pages/level/Level.tsx (2)
19-20: 불필요한 타입 단언 제거 검토
info.level as TreeLevel단언은 불필요할 가능성이 큽니다. 반환 타입이 이미TreeLevel이라면 캐스트를 제거하세요. TS 에러가 난다면getTreeLevel반환 타입을 정교화하는 편이 낫습니다.- <LevelScene level={info.level as TreeLevel} /> + <LevelScene level={info.level} />
46-47: 불필요한 TODO 제거이미 배지를 렌더링하고 있으므로 TODO는 제거해 코드 노이즈를 줄이세요.
- {/* TODO: 오늘 모은 도토리 개수 배지 */} <Badge text={'오늘 모은 도토리 개수'} countNum={acorns} />
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/client/src/pages/level/Level.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-04T08:38:00.583Z
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.583Z
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/Level.tsx (5)
apps/client/src/pages/level/utils/treeLevel.ts (2)
getTreeLevel(47-50)TreeLevel(17-17)packages/design-system/src/lib/utils.ts (1)
cn(108-110)apps/client/src/pages/level/components/LevelScene.tsx (1)
LevelScene(24-40)apps/client/src/pages/level/components/LevelInfoCard.tsx (1)
LevelInfoCard(14-68)apps/client/src/pages/level/components/TreeStatusCard.tsx (1)
TreeStatusCard(9-38)
| type="button" | ||
| className="peer p-[0.4rem]" | ||
| aria-describedby="level-info-card" | ||
| > | ||
| <Icon name="ic_info" width={20} height={20} /> | ||
| </button> | ||
| <div |
There was a problem hiding this comment.
🛠️ Refactor suggestion
아이콘 전용 버튼: 접근 가능한 이름과 ARIA 속성 정정 필요
현재 버튼은 시각적 아이콘만 있고 접근 가능한 이름이 없습니다. 또한 aria-describedby는 설명 텍스트 연결 용도라 팝오버/카드 토글에는 부적절합니다. 버튼에 이름을 부여하고 제어 대상은 aria-controls로 연결하세요.
- <button
+ <button
type="button"
className="peer p-[0.4rem]"
- aria-describedby="level-info-card"
+ aria-controls="level-info-card"
+ aria-haspopup="dialog"
+ aria-label="레벨 안내 보기"
>팁: 완전한 접근성(포커스 트랩, ESC 닫기, aria-expanded 반영 등)을 원하시면 Radix UI Popover/디자인시스템 컴포넌트로 교체하는 것을 권장합니다. 필요하면 구현 도와드릴게요.
📝 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.
| <button | |
| type="button" | |
| className="peer p-[0.4rem]" | |
| aria-describedby="level-info-card" | |
| > | |
| <Icon name="ic_info" width={20} height={20} /> | |
| </button> | |
| <button | |
| type="button" | |
| className="peer p-[0.4rem]" | |
| aria-controls="level-info-card" | |
| aria-haspopup="dialog" | |
| aria-label="레벨 안내 보기" | |
| > | |
| <Icon name="ic_info" width={20} height={20} /> | |
| </button> |
🤖 Prompt for AI Agents
In apps/client/src/pages/level/Level.tsx around lines 27 to 33, the icon-only
button lacks an accessible name and uses aria-describedby incorrectly for
toggling a popover; replace aria-describedby with aria-controls pointing to the
popover/card ID, give the button an accessible label (either aria-label or
include visually-hidden text) and add aria-expanded (true/false) and
aria-haspopup="dialog" (or "menu" as appropriate) to reflect toggle state;
ensure the popover element has the matching id and update code where the popover
is opened/closed to set aria-expanded accordingly (or swap to Radix UI
Popover/design-system component if you want a full accessible implementation).
constantly-dev
left a comment
There was a problem hiding this comment.
고생하셨습니다! 간단한 코멘트 몇 개 달았으니 확인해주세요!
추가로 저 png level 이미지는 이후에 최적화해도 좋을 것 같네요 👍
| interface LevelPageProps { | ||
| acorns?: number; | ||
| } | ||
|
|
||
| export default function Level({ acorns = 3 }: LevelPageProps) { |
There was a problem hiding this comment.
acorns가 optional인 부분과 default가 3이 맞는지 궁금합니다!
There was a problem hiding this comment.
default 값은 테스트 해보느라 제가 임의로 넣어둔 값이었어요 이부분은 삭제하겠습니다
샌각해보니 저 acorns은 Props가 아닌 페이지에서 API 연결 후 받아오는 값이라 수정하겠습니다-!
| </div> | ||
| </div> | ||
| </div> | ||
| {/* TODO: 오늘 모은 도토리 개수 배지 */} |
There was a problem hiding this comment.
아직 수정 안된 부분입니다 따로 원래 있던 뱃지 사용하고있는 코드입니다
| // components/LevelScene.tsx | ||
| import * as React from 'react'; | ||
| import { cn } from '@pinback/design-system/utils'; | ||
| import { type TreeLevel } from '../utils/treeLevel'; |
There was a problem hiding this comment.
Level 페이지 컴포넌트에도 해당 type 쓰이던데 util에서 불러오는 것 같아서! util파일이 아닌 type으로 따로 빼고 재사용 해도 되지 않을까요?
There was a problem hiding this comment.
유틸이 아닌 타입으로 뺴는게 맞는거 같아보이네요 수정하겠습니다-!
| interface LevelSceneProps extends React.HTMLAttributes<HTMLDivElement> { | ||
| level: TreeLevel; | ||
| } |
There was a problem hiding this comment.
트리셰이킹이 되긴 하겠지만 React 전체 모듈을 불러올 이유가 크게 없다면 그냥 HTMLAttributes만 불러와도 될 것 같아요!
React 전체 import 제거하고!
| export default function LevelScene({ | ||
| level, | ||
| className, | ||
| ...rest | ||
| }: LevelSceneProps) { | ||
| const src = SCENE_BY_LEVEL[level]; | ||
| return ( | ||
| <div className={cn('relative w-full', className)} {...rest}> |
There was a problem hiding this comment.
단순 궁금증으로 rest props로 들어오는 예시가 있을까요??
|
|
||
| /* Secondary Color */ | ||
| --color-secondary: #f4f2ec; | ||
| --color-subcolor: #f4f2ec; |
| import LevelScene from './components/LevelScene'; | ||
| import LevelInfoCard from './components/LevelInfoCard'; | ||
| import TreeStatusCard from './components/TreeStatusCard'; |
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/level/utils/treeLevel.ts (1)
28-31: getTreeLevel 입력 유효성 보강 (NaN/음수/실수 방지)API/연동 단계에서 비정상 값이 들어오면 진행률 NaN 등 부작용이 납니다. 정수화+하한 0 클램프를 권장합니다.
-export function getTreeLevel(acorns: number): TreeLevelResult { - const { row, next } = findLevelRow(acorns, TREE_LEVEL_TABLE); - return { ...row, ...calcProgress(acorns, row, next) }; -} +export function getTreeLevel(acorns: number): TreeLevelResult { + const n = Math.floor(Number(acorns)); + const count = Number.isFinite(n) && n >= 0 ? n : 0; + const { row, next } = findLevelRow(count, TREE_LEVEL_TABLE); + return { ...row, ...calcProgress(count, row, next) }; +}
♻️ Duplicate comments (2)
apps/client/src/pages/level/Level.tsx (2)
24-30: 아이콘 버튼 접근성: aria-describedby 오용, 이름 누락아이콘 전용 버튼에 접근 가능한 이름이 없고, 토글 대상 연결에 aria-describedby를 사용했습니다. aria-controls/aria-haspopup로 교체하고 라벨을 추가하세요. (완전한 a11y는 상태 기반 Popover로 aria-expanded 동기화 권장)
- <button + <button type="button" className="peer flex items-center justify-center p-[0.4rem]" - aria-describedby="level-info-card" + aria-controls="level-info-card" + aria-haspopup="dialog" + aria-label="레벨 안내 보기" >
23-42: 키보드 포커스 이동 시 오버레이 닫힘 — group-hover/focus-within 추가버튼에서 오버레이로 포커스가 이동하면 peer 기반 제어가 끊깁니다. 래퍼에 group을 주고 group-hover/group-focus-within을 추가해 포커스 유지 시 노출되게 하세요.
- <div className="relative items-center"> + <div className="relative group items-center"> @@ - <div + <div id="level-info-card" className={cn( - 'pointer-events-none absolute left-0 top-[3rem] z-[20]', - 'opacity-0 transition-opacity duration-150', - 'peer-hover:pointer-events-auto peer-focus-visible:pointer-events-auto', - 'peer-hover:opacity-100 peer-focus-visible:opacity-100' + 'pointer-events-none absolute left-0 top-[3rem] z-[20]', + 'opacity-0 transition-opacity duration-150', + 'peer-hover:pointer-events-auto peer-focus-visible:pointer-events-auto', + 'group-hover:pointer-events-auto group-focus-within:pointer-events-auto', + 'peer-hover:opacity-100 peer-focus-visible:opacity-100', + 'group-hover:opacity-100 group-focus-within:opacity-100' )} >
🧹 Nitpick comments (8)
apps/client/src/pages/level/types/treeLevelType.ts (2)
9-15: 단일 출처 테이블 설계 훌륭함; 무결성 검증(테스트/개발시 어서션) 추가 제안레벨 구간이 변경될 때 실수(겹침/누락)를 막기 위해 개발 환경에서만 구간 일관성 검사를 권장합니다. 또한 rangeLabel은 min/max로부터 파생 가능하므로(복제 데이터) 소비 측에서 계산하도록 전환을 고려해 주세요.
아래 유틸은 파일 외부(예: 테스트 또는 초기화 코드)에서 추가:
// dev-only: 테이블 무결성 검사 export function assertTreeLevelTable(table = TREE_LEVEL_TABLE) { table.forEach((row, i) => { if (i === 0) { if (row.min !== 0) throw new Error('첫 구간 min은 0이어야 합니다.'); } if (row.max != null && row.min > row.max) { throw new Error(`레벨 ${row.level}: min <= max 이어야 합니다.`); } if (i > 0) { const prev = table[i - 1]; const prevEnd = prev.max ?? Infinity; if (row.min !== (prev.max == null ? prevEnd : prevEnd + 1)) { throw new Error(`레벨 ${row.level}: 이전 구간과 불연속입니다.`); } } if (i < table.length - 1 && row.max == null) { throw new Error(`마지막 레벨 외에는 max가 필요합니다.`); } }); } // (선택) rangeLabel 파생 export function formatRangeLabel(min: number, max?: number) { return max == null ? `${min}개 이상` : min === max ? `${min}개` : `${min}–${max}개`; }
21-25: progressToNext의 범위(0–100) 문서화/브랜딩 타입 고려UI 진행률 계약을 타입으로 더 명확히 하고 싶다면 브랜딩 타입을 사용해도 좋습니다. (선택)
적용 diff(해당 라인 내 변경):
-export type TreeLevelResult = TreeLevelRow & { - progressToNext: number; +export type TreeLevelResult = TreeLevelRow & { + progressToNext: Percent; nextMin?: number; remainingToNext?: number; };추가 타입 선언(파일 상단 등 외부에 배치):
type Percent = number & { readonly __brand: 'Percent0to100' };apps/client/src/pages/level/components/LevelInfoCard.tsx (2)
15-15: 원본 불변 역순 변환은 OK; toReversed()로 간결화 가능(환경 지원 시)ES2023을 타깃/폴리필 중이라면 아래가 더 명료합니다. 미지원이면 현행 유지 권장.
- const rows = [...TREE_LEVEL_TABLE].reverse(); + const rows = TREE_LEVEL_TABLE.toReversed();환경 확인: tsconfig lib에 ES2023(Array) 포함 여부와 빌드 타깃/브라우저 지원을 점검해 주세요.
36-43: 아이콘 접근성 라벨을 더 구체적으로화면읽기에서 의미가 선명하도록 이름 포함을 권장합니다.
- <Icon - name={LEVEL_TOOLTIP_ICON[row.level]} - width={46} - height={46} - className="rounded-[0.8rem]" - aria-label={`${row.level} 썸네일 아이콘`} - /> + <Icon + name={LEVEL_TOOLTIP_ICON[row.level]} + width={46} + height={46} + className="rounded-[0.8rem]" + aria-label={`${row.name} (레벨 ${row.level}) 아이콘`} + />apps/client/src/pages/level/Level.tsx (3)
11-13: 임시 하드코드 값 커밋 지양 + 입력 가드 계획acorns=1 하드코드는 남기지 말고 API 연동 시점에 제거하세요. 또한 음수/NaN/실수 대비 입력 가드는 유틸(getTreeLevel)에서 처리하도록 별도 코멘트에 제안했습니다.
15-15: 모바일 주소창 변동 대응(h-dvh 대신 svh 사용 권장)iOS 등에서 dvh가 튀는 이슈가 있어 svh 사용을 권장합니다.
- <div className={cn('bg-subcolor mx-auto h-dvh w-full overflow-hidden')}> + <div className={cn('bg-subcolor mx-auto min-h-[100svh] w-full overflow-hidden')}>
17-17: 불필요한 타입 단언 제거getTreeLevel의 반환 타입이 TreeLevelResult라면 level은 이미 TreeLevel일 가능성이 높습니다. 캐스트 제거해보세요(유틸 타입 정합성은 아래 코멘트 참조).
- <LevelScene level={info.level as TreeLevel} /> + <LevelScene level={info.level} />apps/client/src/pages/level/utils/treeLevel.ts (1)
15-26: progressToNext 하한 보장이상치 대비로 [0,1] 범위를 확실히 보장하는 편이 안전합니다.
- const span = Math.max(1, next.min - row.min); - const progressToNext = Math.min(1, (count - row.min) / span); + const span = Math.max(1, next.min - row.min); + const raw = (count - row.min) / span; + const progressToNext = Math.max(0, Math.min(1, raw));
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/client/src/pages/level/Level.tsx(1 hunks)apps/client/src/pages/level/components/LevelInfoCard.tsx(1 hunks)apps/client/src/pages/level/components/LevelScene.tsx(1 hunks)apps/client/src/pages/level/components/TreeStatusCard.tsx(1 hunks)apps/client/src/pages/level/types/treeLevelType.ts(1 hunks)apps/client/src/pages/level/utils/treeLevel.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/pages/level/components/LevelScene.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-04T08:38:00.583Z
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.583Z
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/components/TreeStatusCard.tsx
🧬 Code graph analysis (2)
apps/client/src/pages/level/Level.tsx (6)
apps/client/src/pages/level/utils/treeLevel.ts (1)
getTreeLevel(28-31)packages/design-system/src/lib/utils.ts (1)
cn(108-110)apps/client/src/pages/level/components/LevelScene.tsx (1)
LevelScene(23-34)apps/client/src/pages/level/types/treeLevelType.ts (1)
TreeLevel(17-17)apps/client/src/pages/level/components/LevelInfoCard.tsx (1)
LevelInfoCard(14-68)apps/client/src/pages/level/components/TreeStatusCard.tsx (1)
TreeStatusCard(9-38)
apps/client/src/pages/level/types/treeLevelType.ts (1)
apps/client/src/pages/level/utils/treeLevel.ts (1)
TREE_LEVEL_TABLE(33-33)
🔇 Additional comments (4)
apps/client/src/pages/level/types/treeLevelType.ts (1)
17-17: TreeLevel 유니온을 테이블에서 자동 파생한 점 좋습니다단일 소스 유지에 유리하고 추론 품질도 좋습니다. 별도 조치 필요 없습니다.
apps/client/src/pages/level/components/TreeStatusCard.tsx (1)
3-3: 경로 별칭(@pages)으로의 import 정리 LGTM모듈 경로 일관성이 좋아졌습니다. 기존 학습 노트의 의도적 계산(info.level * 20)과도 충돌 없습니다.
apps/client/src/pages/level/components/LevelInfoCard.tsx (1)
4-4: 타입/테이블 중앙화에 따른 import 변경 LGTM의존성 방향이 명확해졌습니다.
apps/client/src/pages/level/Level.tsx (1)
33-38: pointer-events 기본값 수정은 훌륭합니다보이지 않을 때 클릭 방지 처리(pointer-events-none 기본)는 적절합니다.
jllee000
left a comment
There was a problem hiding this comment.
고생하셨습니다!
pr에 남겨주신 화면 레이아웃이 깨지는(가려지는) 부분은 디바이스 크기나 화면에 따라.. 피그마랑 차이가 계속 생길 거 같네요!! 이런 케이스가 많이 보이게 된다면, 추후에 px로 지정한 공컴들을 고정값이 아니라 w-full로 하고 외부에서 % 로 감싸는 방식으로 수정해야 할 수도 있겠단 생각이 드네요,,,ㅠㅠ
| id="level-info-card" | ||
| className={cn( | ||
| 'pointer-events-none absolute left-0 top-[3rem] z-[20]', | ||
| 'opacity-0 transition-opacity duration-150', | ||
| 'peer-hover:pointer-events-auto peer-focus-visible:pointer-events-auto', | ||
| 'peer-hover:opacity-100 peer-focus-visible:opacity-100' |
There was a problem hiding this comment.
이거 cn이 스타일값 객체들을 여러개 합칠 때 사용하는 걸로 아는데요!
이 코드 경우에, 분기나 객체 구분이 안보이는데, z-[20] opacity-0 ..처럼 쭉 이어서 나열하지않고 cn을 쓰신 이유가 궁금합니다!
There was a problem hiding this comment.
맞습니당 이 코드에 따로 분기나 객체 구분은 없어요-! 다만 한줄로 쭉 나열해서 쓰다보면 가독성이 좋지 않아 cn으로 구분해서 사용했습니다-!
| src={src} | ||
| draggable={false} | ||
| className="pointer-events-none h-full w-full select-none rounded-[1.2rem] object-contain object-[right_bottom]" |
There was a problem hiding this comment.
와우 이미지 드래그나 복사 불가하게 제한해둔 거 너무 세심하고 꼼꼼하시네유
There was a problem hiding this comment.
테스트하다가 드래그되는게 불편해서 따로 처리해뒀습니당-!
| import { Level, Progress } from '@pinback/design-system/ui'; | ||
| import { cn } from '@pinback/design-system/utils'; | ||
| import { getTreeLevel } from '../utils/treeLevel'; | ||
| import { getTreeLevel } from '@pages/level/utils/treeLevel'; |
There was a problem hiding this comment.
제 코드에도 상대경로로 작업한게 많은데, 이거보고 저도 바꿔놔야겠다고 생각났네요
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
📷 Screenshot
Summary by CodeRabbit