-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(client): tree 레벨 컴포넌트 구현 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5029750
7c9a28d
2e03753
8c67c8a
6ba83f1
a97c1e7
b75597d
ceb5f80
878d41c
c8649e2
0802bb4
c643660
152c232
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| const Level = () => { | ||
| return <div>Level</div>; | ||
| return <div className="bg-secondary flex flex-col gap-[2rem] p-[1rem]"></div>; | ||
| }; | ||
|
|
||
| export default Level; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { cn } from '@pinback/design-system/utils'; | ||
| import { Level } from '@pinback/design-system/ui'; | ||
| import { Icon, type IconName } from '@pinback/design-system/icons'; | ||
| import { TREE_LEVEL_TABLE, type TreeLevel } from '../utils/treeLevel'; | ||
|
|
||
| const LEVEL_TOOLTIP_ICON = { | ||
| 1: 'tooltip_1', | ||
| 2: 'tooltip_2', | ||
| 3: 'tooltip_3', | ||
| 4: 'tooltip_4', | ||
| 5: 'tooltip_5', | ||
| } as const satisfies Record<TreeLevel, IconName>; | ||
|
|
||
| export default function LevelInfoCard() { | ||
| const rows = [...TREE_LEVEL_TABLE].reverse(); | ||
|
|
||
| return ( | ||
| <section | ||
| className={cn( | ||
| 'bg-white-bg common-shadow w-[24.6rem] rounded-[1.2rem] px-[1.6rem] py-[2.4rem]' | ||
| )} | ||
| aria-label="지식나무 숲 레벨 안내" | ||
| > | ||
| <h2 className="sub2-sb text-font-black-1 mb-[1.2rem] flex items-center justify-center"> | ||
| 치삐의 지식나무 숲 레벨 | ||
| </h2> | ||
|
|
||
| <ul> | ||
| {rows.map((row) => ( | ||
| <li | ||
| key={row.level} | ||
| className="flex w-full items-center justify-between py-[1.2rem]" | ||
| > | ||
| <div className="flex w-full items-center gap-[1.2rem]"> | ||
| <div className="bg-gray0 flex h-[4.6rem] w-[4.6rem] items-center justify-center rounded-[0.8rem]"> | ||
| <Icon | ||
| name={LEVEL_TOOLTIP_ICON[row.level]} | ||
| width={46} | ||
| height={46} | ||
| className="rounded-[0.8rem]" | ||
| aria-label={`${row.level} 썸네일 아이콘`} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="ml-[0.8rem] flex flex-1 flex-col gap-[0.4rem]"> | ||
| <div className="flex justify-between"> | ||
| <span className="sub5-sb text-font-black-1">{row.name}</span> | ||
| <Level level={row.level} aria-label={`레벨 ${row.level}`} /> | ||
| </div> | ||
|
|
||
| <span className="caption2-m text-font-gray-3"> | ||
| {row.rangeLabel} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
|
|
||
| <div className="bg-gray0 mt-[0.8rem] rounded-[0.4rem] px-[0.8rem] py-[1.2rem]"> | ||
| <p className="caption2-m text-font-gray-3 flex items-center justify-center"> | ||
| 정보를 1분 동안 읽고 도토리를 모아보세요. 치삐를 행복하게 만들 수 | ||
| 있어요. | ||
| </p> | ||
| </div> | ||
| </section> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,38 @@ | ||||||||
| import { Level, Progress } from '@pinback/design-system/ui'; | ||||||||
| import { cn } from '@pinback/design-system/utils'; | ||||||||
| import { getTreeLevel } from '../utils/treeLevel'; | ||||||||
|
|
||||||||
| export interface TreeStatusCardProps { | ||||||||
| acorns: number; | ||||||||
| } | ||||||||
|
|
||||||||
| export default function TreeStatusCard({ acorns }: TreeStatusCardProps) { | ||||||||
| const info = getTreeLevel(acorns); | ||||||||
|
|
||||||||
| const barPercent = Math.min(100, info.level * 20); | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 진행률 계산이 의도와 다르게 동작합니다 (레벨 1에서도 20%로 표기). 현재 - const barPercent = Math.min(100, info.level * 20);
+ // 레벨 내 진행률(0~100)
+ const barPercent = Math.round(info.progressToNext * 100);추가로 “전체 레벨 진행률(0~100)”이 필요하다면 utils에 계산 함수를 두고 여기서는 그 값을 사용하도록 분리하는 것을 권장합니다. 예시는 아래 코멘트에 첨부했습니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 레벨 1 시작점도 20%로 보이도록 의도 되었습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jjangminii 네, 이해했습니다! 레벨 1부터 20%로 시작하는 것이 의도된 디자인이군요. 항상 어느 정도의 진행률을 시각적으로 보여주는 UX 관점에서 좋은 접근입니다. 제 이전 제안을 철회하고, 현재 구현이 올바르다는 점을 확인했습니다. 명확히 설명해주셔서 감사합니다. ✏️ Learnings added
|
||||||||
|
|
||||||||
| return ( | ||||||||
| <div | ||||||||
| className={cn('bg-white-bg w-[32.3rem] rounded-[1.2rem] p-[1.2rem]')} | ||||||||
| role="group" | ||||||||
| aria-label={`${info.name} 진행 카드`} | ||||||||
| > | ||||||||
| <div className="flex items-baseline"> | ||||||||
| <span className="head1 text-main500">{barPercent}%</span> | ||||||||
| </div> | ||||||||
|
|
||||||||
| <div className="mt-[0.8rem] flex items-center gap-[0.4rem]"> | ||||||||
| <span className="sub4-sb text-font-gray-2">{info.name}</span> | ||||||||
| <Level level={info.level} /> | ||||||||
| </div> | ||||||||
|
|
||||||||
| <div className="mt-[1.6rem]"> | ||||||||
| <Progress | ||||||||
| value={barPercent} | ||||||||
| variant="tree" | ||||||||
| aria-label={`${info.name} 레벨 진행률`} | ||||||||
| /> | ||||||||
| </div> | ||||||||
| </div> | ||||||||
| ); | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,50 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type TreeLevelRowShape = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| level: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| min: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| max?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rangeLabel: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export const TREE_LEVEL_TABLE = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { level: 1, name: '잊힌 기록의 숲', min: 0, max: 0, rangeLabel: '0개' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { level: 2, name: '햇살의 터전', min: 1, max: 2, rangeLabel: '1–2개' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { level: 3, name: '기록의 오솔길', min: 3, max: 4, rangeLabel: '3–4개' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { level: 4, name: '지식 나무 언덕', min: 5, max: 6, rangeLabel: '5–6개' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { level: 5, name: '도토리 만개 숲', min: 7, rangeLabel: '7개 이상' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ] as const satisfies readonly TreeLevelRowShape[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export type TreeLevel = (typeof TREE_LEVEL_TABLE)[number]['level']; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export type TreeLevelRow = TreeLevelRowShape; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export type TreeLevelResult = TreeLevelRow & { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| progressToNext: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| nextMin?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| remainingToNext?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function findLevelRow(count: number, rows: readonly TreeLevelRow[]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const idx = rows.findIndex( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (r) => count >= r.min && (r.max === undefined || count <= r.max) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const i = idx === -1 ? 0 : idx; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { row: rows[i], next: rows[i + 1] as TreeLevelRow | undefined }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 범위 판정 로직을 max 대신 next.min 기준으로 단순화하세요 현재 아래처럼 변경 제안: function findLevelRow(count: number, rows: readonly TreeLevelRow[]) {
- const idx = rows.findIndex(
- (r) => count >= r.min && (r.max === undefined || count <= r.max)
- );
+ const idx = rows.findIndex((r, i) => {
+ const next = rows[i + 1];
+ return count >= r.min && (next ? count < next.min : true);
+ });
const i = idx === -1 ? 0 : idx;
return { row: rows[i], next: rows[i + 1] as TreeLevelRow | undefined };
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function calcProgress(count: number, row: TreeLevelRow, next?: TreeLevelRow) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!next) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| progressToNext: 1 as const, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+37
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거는 어떤경우의 조건문일까요?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 조건문은 next가 없을 때 현재가 마지막 레벨일 때 실행됩니다-! 도토리가 7개 이상이면 최종 레벨(5)에 도달한 상태라 progressToNext는 1이고 다음 단계 값들은 undefined가 됩니다 |
||||||||||||||||||||||||||||||||||||||||||||||||||
| nextMin: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| remainingToNext: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const span = Math.max(1, next.min - row.min); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const progressToNext = Math.min(1, (count - row.min) / span); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const remainingToNext = Math.max(0, next.min - count); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { progressToNext, nextMin: next.min, remainingToNext }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion progressToNext 하한 미클램프 — 음수 진행률 가능
아래처럼 보정 제안: const span = Math.max(1, next.min - row.min);
- const progressToNext = Math.min(1, (count - row.min) / span);
+ const progressToNext = Math.max(0, Math.min(1, (count - row.min) / span));
const remainingToNext = Math.max(0, next.min - count);참고: TreeStatusCard에서 진행 바는 의도적으로 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분은 서버에서 전달해주는 값이 양의 정수라고 확정지었기에 제거했습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jjangminii 네, 서버에서 양의 정수를 보장한다면 음수 진행률 케이스는 발생하지 않겠네요. API 계약이 명확하다면 불필요한 방어 코드를 제거하는 것이 맞습니다. 시스템 아키텍처 관점에서 합리적인 결정입니다! 🐰📋 ✏️ Learnings added
🧠 Learnings used |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export function getTreeLevel(acorns: number): TreeLevelResult { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { row, next } = findLevelRow(acorns, TREE_LEVEL_TABLE); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { ...row, ...calcProgress(acorns, row, next) }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export { Icon } from './components/icon'; | ||
| export type { IconName } from './iconNames'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
빈 컨테이너만 렌더링되어 페이지가 비어 있습니다 — TreeStatusCard/LevelInfoCard를 배치해 주세요.
PR 목적(#53)에 비해 현재 UI가 노출되지 않습니다. 최소 상태 카드와 레벨 안내 카드를 렌더링해 기능을 확인 가능하게 해주세요. 아래 예시는 시맨틱 마크업(main, h1)과 DS 스페이싱 스케일을 함께 적용했습니다. 각 컴포넌트의 export 형태에 맞춰 import 문은 필요시 조정하세요.
원하시면 PR에 맞춰 실제 import 경로/타입을 반영한 패치를 바로 드릴게요.
📝 Committable suggestion
🤖 Prompt for AI Agents
💡 Verification agent
🧩 Analysis chain
임의값 대신 스케일 토큰 사용,
bg-secondary토큰 존재 확인 권장gap-[2rem],p-[1rem]대신 스케일(gap-8,p-4)을 권장합니다. 또한bg-secondary가 팀 Tailwind 토큰에 정의돼 있는지 확인해 주세요.다음 스크립트로 토큰 정의를 빠르게 확인할 수 있습니다:
🏁 Script executed:
Length of output: 179
🏁 Script executed:
Length of output: 1861
스케일 토큰 및
bg-secondary정의 확인 필요secondary색상 토큰이 정의되어 있지 않습니다.bg-secondary사용 전 토큰 정의 여부 확인 및 필요 시 추가하세요.🤖 Prompt for AI Agents