Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion apps/client/src/pages/level/Level.tsx
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>;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

빈 컨테이너만 렌더링되어 페이지가 비어 있습니다 — TreeStatusCard/LevelInfoCard를 배치해 주세요.

PR 목적(#53)에 비해 현재 UI가 노출되지 않습니다. 최소 상태 카드와 레벨 안내 카드를 렌더링해 기능을 확인 가능하게 해주세요. 아래 예시는 시맨틱 마크업(main, h1)과 DS 스페이싱 스케일을 함께 적용했습니다. 각 컴포넌트의 export 형태에 맞춰 import 문은 필요시 조정하세요.

+import TreeStatusCard from './components/TreeStatusCard';
+import LevelInfoCard from './components/LevelInfoCard';
 
-const Level = () => {
-  return <div className="bg-secondary flex flex-col gap-[2rem] p-[1rem]"></div>;
-};
+const Level = () => {
+  return (
+    <main className="bg-secondary flex flex-col gap-8 p-4">
+      <h1 className="text-xl font-semibold">나의 나무 레벨</h1>
+      <TreeStatusCard />
+      <LevelInfoCard />
+    </main>
+  );
+};
 
 export default Level;

원하시면 PR에 맞춰 실제 import 경로/타입을 반영한 패치를 바로 드릴게요.

📝 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
return <div className="bg-secondary flex flex-col gap-[2rem] p-[1rem]"></div>;
// apps/client/src/pages/level/Level.tsx
import React from 'react';
import TreeStatusCard from './components/TreeStatusCard';
import LevelInfoCard from './components/LevelInfoCard';
-const Level = () => {
- return <div className="bg-secondary flex flex-col gap-[2rem] p-[1rem]"></div>;
const Level = () => {
return (
<main className="bg-secondary flex flex-col gap-8 p-4">
<h1 className="text-xl font-semibold">나의 나무 레벨</h1>
<TreeStatusCard />
<LevelInfoCard />
</main>
);
};
export default Level;
🤖 Prompt for AI Agents
In apps/client/src/pages/level/Level.tsx around line 2, the component currently
returns an empty container so the page shows nothing; render the TreeStatusCard
and LevelInfoCard inside the container (wrap with semantic main and an h1) using
the design system spacing classes instead of raw values (e.g., gap-4, p-4 or the
project's DS equivalents) and adjust/add import statements for TreeStatusCard
and LevelInfoCard to match their exported names; if those components require
props, pass minimal mock or real props (status, level data) so they render
without errors.

💡 Verification agent

🧩 Analysis chain

임의값 대신 스케일 토큰 사용, bg-secondary 토큰 존재 확인 권장

gap-[2rem], p-[1rem] 대신 스케일(gap-8, p-4)을 권장합니다. 또한 bg-secondary가 팀 Tailwind 토큰에 정의돼 있는지 확인해 주세요.

다음 스크립트로 토큰 정의를 빠르게 확인할 수 있습니다:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# tailwind 설정 파일 위치 확인
fd -HI 'tailwind.config*' -t f

# bg-secondary 사용처 확인
rg -nP --type=ts --type=tsx -C2 '\bbg-secondary\b' apps/client

# tailwind 설정에 secondary 색상 토큰 존재 여부 힌트
for f in $(fd -HI 'tailwind.config*' -t f); do
  echo "==> $f"
  rg -n -C2 'secondary' "$f" || true
done

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# tailwind 설정 파일에서 secondary 정의 확인
echo "==> apps/extension/tailwind.config.ts"
grep -R -nE "secondary\b" -C3 apps/extension/tailwind.config.ts || true

# 앱 내 bg-secondary 사용처 확인
grep -R -n "bg-secondary" apps/client || true

# 임의값 클래스 확인
grep -R -n "gap-\[" apps/client || true
grep -R -n "p-\[" apps/client || true

Length of output: 1861


스케일 토큰 및 bg-secondary 정의 확인 필요

  • gap-[2rem] → gap-8, p-[1rem] → p-4 로 교체
  • apps/extension/tailwind.config.ts에 secondary 색상 토큰이 정의되어 있지 않습니다. bg-secondary 사용 전 토큰 정의 여부 확인 및 필요 시 추가하세요.
🤖 Prompt for AI Agents
In apps/client/src/pages/level/Level.tsx around line 2, the returned div uses
arbitrary spacing tokens and an undefined color token; replace gap-[2rem] with
gap-8 and p-[1rem] with p-4 in the className, and then verify that
apps/extension/tailwind.config.ts defines a 'secondary' color token (add an
appropriate hex/rgb value under theme.extend.colors.secondary if it's missing)
before using bg-secondary.

};

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