-
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 7 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,41 @@ | ||
| import TreeStatusCard from './components/TreeStatusCard'; | ||
| import LevelInfoCard from './components/LevelInfoCard'; | ||
| import { Icon } from '@pinback/design-system/icons'; | ||
|
|
||
| const Level = () => { | ||
| return <div>Level</div>; | ||
| return ( | ||
| <div className="bg-secondary flex flex-col gap-[2rem] p-[1rem]"> | ||
| <Icon | ||
| name={'tooltip_1'} | ||
| width={46} | ||
| height={46} | ||
| className="rounded-[0.8rem]" | ||
| /> | ||
| <Icon | ||
| name={'ic_plus'} | ||
| width={46} | ||
| height={46} | ||
| className="rounded-[0.8rem]" | ||
| /> | ||
| <Icon | ||
| name={'ic_plus'} | ||
| width={46} | ||
| height={46} | ||
| className="rounded-[0.8rem]" | ||
| /> | ||
| <Icon name={'tooltip_4'} className="rounded-[0.8rem]" /> | ||
| <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,58 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type TreeLevel = 1 | 2 | 3 | 4 | 5; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface TreeLevelRow { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| level: TreeLevel; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| min: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rangeLabel: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const TREE_LEVEL_TABLE: readonly TreeLevelRow[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
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. 지금도 좋은 구조지만, 만약 level에 6이 추가된다면 그렇다면
Suggested change
이렇게 되면 원본 export const TREE_LEVEL_TABLE: readonly TreeLevelRow[] = [그리고 as const가 애초에 readonly의 의미를 가지기도 하고요! 어떻게 생각하시나요??
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. 확실히 지금 구조에서는 레벨 변동이나 추가에 있어서 불편할것같다는 생각이 드네요.. 지금 구조만 생각하고 추후 생각은 못했는데 이 부분은 진혁님 말씀처럼 수정하는게 좋을 것 같습니다-! 좋은 의견 감사합니다 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface TreeLevelResult extends TreeLevelRow { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| progressToNext: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nextMin?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remainingToNext?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function getTreeLevel(acorns: number): TreeLevelResult { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const count = Math.max(0, Math.floor(acorns ?? 0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
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. 단순 궁금증으로 acorns 개수를 floor와 max로 count를 계산하는 이유가 궁금해요!
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. 레벨 판정을 정수화나 이상값에 대해 안정적으로 하려고했는데 들어오는값이 안정적으로 들어온다면 굳이 필요없는 부분이긴합니다-! 이 부분은 서버와 상의해보고 다시 정리해봐도 좋을것같아요
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. 서버랑 상의했습니다 이부분은 굳이 필요하지 않을거같아 수정하겠습니다
Member
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. NaN/Infinity 입력 시 NaN 전파 가능 — 입력 정규화 보강 필요
-export function getTreeLevel(acorns: number): TreeLevelResult {
- const count = Math.max(0, Math.floor(acorns ?? 0));
+export function getTreeLevel(acorns: number): TreeLevelResult {
+ const n = Number(acorns);
+ const count = Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const row = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TREE_LEVEL_TABLE.find( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (r) => count >= r.min && (r.max === undefined || count <= r.max) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) ?? TREE_LEVEL_TABLE[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const idx = TREE_LEVEL_TABLE.findIndex((r) => r.level === row.level); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const next = TREE_LEVEL_TABLE[idx + 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!next) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { ...row, progressToNext: 1 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...row, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| progressToNext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nextMin: next.min, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remainingToNext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
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. 확실히 지금 함수는 행 선택, 다음 단계 탐색, 진행도 계산을 한꺼번에 하고 있어서 책임이 많아 보이고 가독성도 떨어지네요.. 이 부분은 함수 분리하겠습니다-!
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 computeTreeLevel(acorns: number): TreeLevel { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const c = Math.max(0, Math.floor(acorns ?? 0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (c >= 7) return 5; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (c >= 5) return 4; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (c >= 3) return 3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (c >= 1) return 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.
넵넵-!