-
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 11 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,21 @@ | ||
| import TreeStatusCard from './components/TreeStatusCard'; | ||
| import LevelInfoCard from './components/LevelInfoCard'; | ||
|
|
||
| const Level = () => { | ||
| return <div>Level</div>; | ||
| return ( | ||
| <div className="bg-secondary flex flex-col gap-[2rem] p-[1rem]"> | ||
| <LevelInfoCard /> | ||
| <TreeStatusCard acorns={0} /> | ||
| <TreeStatusCard acorns={1} /> | ||
| <TreeStatusCard acorns={2} /> | ||
| <TreeStatusCard acorns={3} /> | ||
| <TreeStatusCard acorns={4} /> | ||
| <TreeStatusCard acorns={5} /> | ||
| <TreeStatusCard acorns={6} /> | ||
| <TreeStatusCard acorns={7} /> | ||
| <TreeStatusCard acorns={987987} /> | ||
| </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.
확인했씁니당! 일정 수이상 커지면 걍 최대 100%되게 잘 해두신 것 같네용
머지 전에는 이 확인용으로 불러온 코드들 삭제해주세용!
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.
넵넵-!