Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion apps/client/src/pages/level/Level.tsx
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>
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했씁니당! 일정 수이상 커지면 걍 최대 100%되게 잘 해두신 것 같네용
머지 전에는 이 확인용으로 불러온 코드들 삭제해주세용!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵넵-!

};

export default Level;
68 changes: 68 additions & 0 deletions apps/client/src/pages/level/components/LevelInfoCard.tsx
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>
);
}
38 changes: 38 additions & 0 deletions apps/client/src/pages/level/components/TreeStatusCard.tsx
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);
Copy link

@coderabbitai coderabbitai bot Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

진행률 계산이 의도와 다르게 동작합니다 (레벨 1에서도 20%로 표기).

현재 info.level * 20은 레벨 1에서 20%, 레벨 5에서 100%를 표시하여 0개 도토리(레벨 1 시작점)도 20%로 보이는 오류가 있습니다. getTreeLevel이 제공하는 progressToNext(0~1)를 사용해 레벨 내 진행률을 표시하세요.

-  const barPercent = Math.min(100, info.level * 20);
+  // 레벨 내 진행률(0~100)
+  const barPercent = Math.round(info.progressToNext * 100);

추가로 “전체 레벨 진행률(0~100)”이 필요하다면 utils에 계산 함수를 두고 여기서는 그 값을 사용하도록 분리하는 것을 권장합니다. 예시는 아래 코멘트에 첨부했습니다.

📝 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.

Suggested change
const barPercent = Math.min(100, info.level * 20);
// 레벨 내 진행률(0~100)
const barPercent = Math.round(info.progressToNext * 100);
🤖 Prompt for AI Agents
In apps/client/src/pages/level/components/TreeStatusCard.tsx around line 12, the
progress bar percent is computed incorrectly with info.level * 20 (which shows
level 1 as 20%); replace that with the per-level progress value provided by
getTreeLevel by using info.progressToNext (0–1) multiplied by 100 (optionally
rounded and clamped with Math.min(100, ...)), so the bar reflects progress
within the current level; if you also need an overall 0–100 level progress, move
that calculation into a utils helper and consume the helper here instead.

Copy link
Collaborator Author

@jjangminii jjangminii Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

레벨 1 시작점도 20%로 보이도록 의도 되었습니다

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jjangminii 네, 이해했습니다! 레벨 1부터 20%로 시작하는 것이 의도된 디자인이군요. 항상 어느 정도의 진행률을 시각적으로 보여주는 UX 관점에서 좋은 접근입니다.

제 이전 제안을 철회하고, 현재 구현이 올바르다는 점을 확인했습니다. 명확히 설명해주셔서 감사합니다.


✏️ Learnings added
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.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


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>
);
}
50 changes: 50 additions & 0 deletions apps/client/src/pages/level/utils/treeLevel.ts
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

범위 판정 로직을 max 대신 next.min 기준으로 단순화하세요

현재 (min ≤ count ≤ max)는 다음 레벨의 min과 정보 중복이며, 0과 1 사이 실수(예: 0.5)가 레벨1에 매칭되지 않는 가장자리 케이스도 있습니다. next?.min을 상한으로 쓰면 중복 제거와 함께 반열림 구간 [min, 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

‼️ 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.

Suggested change
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 };
}
function findLevelRow(count: number, rows: readonly TreeLevelRow[]) {
const idx = rows.findIndex((r, i) => {
const next = rows[i + 1];
// use [min, next.min) for all but last row, and [min, ∞) for the final row
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 };
}
🤖 Prompt for AI Agents
In apps/client/src/pages/level/utils/treeLevel.ts around lines 26-32, the range
check currently uses r.max and allows edge cases and duplicate info with next
row; change the logic to locate the row by comparing count >= r.min and count <
next?.min (treating upper bound as exclusive) so ranges become [min, next.min)
and remove reliance on max; if no matching index found use index 0, and return {
row: rows[i], next: rows[i+1] } as before.


function calcProgress(count: number, row: TreeLevelRow, next?: TreeLevelRow) {
if (!next)
return {
progressToNext: 1 as const,
Comment on lines +35 to +37
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 어떤경우의 조건문일까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Copy link

@coderabbitai coderabbitai bot Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

progressToNext 하한 미클램프 — 음수 진행률 가능

count < row.min이면 진행률이 음수가 될 수 있습니다. UI 바인딩 안정성을 위해 0–1로 확실히 클램프하세요.

아래처럼 보정 제안:

   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에서 진행 바는 의도적으로 level * 20을 사용한다는 러닝을 반영하고 있습니다. 본 변경은 progressToNext를 사용하는 다른 뷰/ARIA에서의 안전성 향상 목적입니다.

📝 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.

Suggested change
function calcProgress(count: number, row: TreeLevelRow, next?: TreeLevelRow) {
if (!next)
return {
progressToNext: 1 as const,
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 };
}
function calcProgress(count: number, row: TreeLevelRow, next?: TreeLevelRow) {
if (!next)
return {
progressToNext: 1 as const,
nextMin: undefined,
remainingToNext: undefined,
};
const span = Math.max(1, next.min - row.min);
const progressToNext = Math.max(0, Math.min(1, (count - row.min) / span));
const remainingToNext = Math.max(0, next.min - count);
return { progressToNext, nextMin: next.min, remainingToNext };
}
🤖 Prompt for AI Agents
In apps/client/src/pages/level/utils/treeLevel.ts around lines 34 to 45,
progressToNext can become negative when count < row.min; clamp progressToNext to
the 0–1 range to prevent negative progress values. Replace the current
computation with a clamped value (e.g., compute raw = (count - row.min) / span
and set progressToNext = Math.max(0, Math.min(1, raw))) and keep nextMin and
remainingToNext logic unchanged so other consumers/ARIA remain safe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 서버에서 전달해주는 값이 양의 정수라고 확정지었기에 제거했습니다

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jjangminii 네, 서버에서 양의 정수를 보장한다면 음수 진행률 케이스는 발생하지 않겠네요. API 계약이 명확하다면 불필요한 방어 코드를 제거하는 것이 맞습니다.

시스템 아키텍처 관점에서 합리적인 결정입니다!

🐰📋
╭─────╮
│ 📝✓ │ Understanding system contracts!
╰─────╯


✏️ Learnings added
Learnt from: jjangminii
PR: Pinback-Team/pinback-client#54
File: apps/client/src/pages/level/utils/treeLevel.ts:34-45
Timestamp: 2025-09-05T06:58:12.328Z
Learning: In the Pinback client application, the server API guarantees that acorn count values are positive integers, so defensive clamping for negative progress calculations in tree level utilities is unnecessary.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
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.


export function getTreeLevel(acorns: number): TreeLevelResult {
const { row, next } = findLevelRow(acorns, TREE_LEVEL_TABLE);
return { ...row, ...calcProgress(acorns, row, next) };
}
5 changes: 5 additions & 0 deletions packages/design-system/src/icons/iconNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,10 @@ export const iconNames = [
'ic_details_disable',
'ic_info',
'ic_plus',
'tooltip_1',
'tooltip_2',
'tooltip_3',
'tooltip_4',
'tooltip_5',
] as const;
export type IconName = (typeof iconNames)[number];
1 change: 1 addition & 0 deletions packages/design-system/src/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Icon } from './components/icon';
export type { IconName } from './iconNames';
10 changes: 10 additions & 0 deletions packages/design-system/src/icons/source/tooltip_1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/design-system/src/icons/source/tooltip_2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/design-system/src/icons/source/tooltip_3.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/design-system/src/icons/source/tooltip_4.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/design-system/src/icons/source/tooltip_5.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading