Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c46b2d9
Feat: 사이드바 컴포넌트 및 관련 요소 추가
jjangminii Aug 27, 2025
6b5a6eb
Feat: 사이드바 컴포넌트 수정
jjangminii Aug 30, 2025
2950def
feat: 컴파운드+헤드리스 패턴으로 변경
jjangminii Aug 31, 2025
0d86a14
feat: 아이콘 이름 변수 수정
jjangminii Aug 31, 2025
cedcc4d
feat: 사이드바 컴포넌트
jjangminii Sep 3, 2025
7a8cbac
feat: 사이드바 및 관련 컴포넌트 재구성
jjangminii Sep 3, 2025
10570ab
feat: 코드리뷰 반영
jjangminii Sep 4, 2025
b723c2c
feat: 아코디언 아이템
jjangminii Sep 4, 2025
74fd048
feat: 프로필 카드 추가
jjangminii Sep 5, 2025
f6a16bf
Merge branch 'develop' of https://github.com/Pinback-Team/pinback-cli…
jjangminii Sep 5, 2025
5eb5b84
feat: 카테고리 아이템 인덱스 추가
jjangminii Sep 5, 2025
30ba55d
Merge branch 'develop' of https://github.com/Pinback-Team/pinback-cli…
jjangminii Sep 5, 2025
1defad1
feat: 카테고리 선택시 색 변경
jjangminii Sep 5, 2025
6e3745b
feat: OptionsMenuButton 연결
jjangminii Sep 5, 2025
a387a2b
feat: 로고 추가
jjangminii Sep 5, 2025
21edd3d
feat: 사이드바 디자인, 카테고리 초기값 수정
jjangminii Sep 5, 2025
f59e34b
feat: 마이페이지 컴포넌트
jjangminii Sep 5, 2025
189d1fd
feat: 활성화 상태관리
jjangminii Sep 5, 2025
003ad82
feat: 팝업 연결
jjangminii Sep 5, 2025
b763b49
refactor: 파일 분리
jjangminii Sep 5, 2025
533b1bd
feat: 경로 변경
jjangminii Sep 5, 2025
b96f9a2
chore: 주석 제거
jjangminii Sep 5, 2025
9142ec1
Merge branch 'develop' of https://github.com/Pinback-Team/pinback-cli…
jjangminii Sep 9, 2025
0a7f961
feat: 단일 button으로 전환
jjangminii Sep 9, 2025
de67f64
feat: 코드리뷰 반영
jjangminii Sep 9, 2025
1787935
feat: 아이콘 타입 분리
jjangminii Sep 10, 2025
662b576
feat: 코드리뷰 반영
jjangminii Sep 10, 2025
2ade739
feat: type 파일 이동
jjangminii Sep 10, 2025
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
1 change: 1 addition & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"react": "^19.1.1",
Comment on lines 12 to 14
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.

cva client에서 의존성 설치가 안되어있어서 추가했습니다-! 이부분 혹시 따로 디자인시스템에서 가져와야한는 것일까요?

"react-dom": "^19.1.1",
"react-router-dom": "^7.8.2"
Expand Down
Empty file added apps/client/public/.gitkeep
Empty file.
1 change: 0 additions & 1 deletion apps/client/public/vite.svg

This file was deleted.

2 changes: 2 additions & 0 deletions apps/client/src/layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Outlet } from 'react-router-dom';
import { Sidebar } from '../shared/components/sidebar/Sidebar';

const Layout = () => {
return (
<>
{/* TODO: 필요시 레이아웃 추가 */}
{/* TODO: 사이드바 추가 */}

<Sidebar />
<Outlet />
Comment on lines +10 to 11
Copy link
Member

Choose a reason for hiding this comment

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

sidebar 위치 잘 정의해주셨네요!! 이후 해당 PR 머지되면 사이드바 제 쪽에서 가져와서 추가 layout 작업 하겠습니다~

</>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/pages/level/Level.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { cn } from '@pinback/design-system/utils';
import LevelScene from '@pages/level/components/LevelScene';
import LevelInfoCard from '@pages/level/components/LevelInfoCard';
import TreeStatusCard from '@pages/level/components/TreeStatusCard';
import { getTreeLevel } from '@pages/level/utils/treeLevel';
import { getTreeLevel } from '@shared/utils/treeLevel';
import { TreeLevel } from '@pages/level/types/treeLevelType';
import { Badge } from '@pinback/design-system/ui';

Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/pages/level/components/TreeStatusCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Level, Progress } from '@pinback/design-system/ui';
import { cn } from '@pinback/design-system/utils';
import { getTreeLevel } from '@pages/level/utils/treeLevel';
import { getTreeLevel } from '@shared/utils/treeLevel';

export interface TreeStatusCardProps {
acorns: number;
Expand Down
68 changes: 68 additions & 0 deletions apps/client/src/shared/components/sidebar/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useState, useId } from 'react';
import { cn } from '@pinback/design-system/utils';
import SideItem from './SideItem';
import { IconToken } from './types/IconTokenType';

interface AccordionItemProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
icon: IconToken;
label: string;
children: React.ReactNode;
active: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
trailing?: boolean;
className?: string;
onClick: () => void;
}

export default function AccordionItem({
icon,
label,
children,
active,
open,
defaultOpen = false,
onOpenChange,
trailing = true,
className,
onClick,
}: AccordionItemProps) {
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const isOpen = open ?? internalOpen;
Comment on lines +32 to +33
Copy link
Member

Choose a reason for hiding this comment

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

외부 open을 받아서 쓰시는 이유는 아무래도 open 상태를 컴포넌트 외부에서도 쓰이기 때문에 부모로 올리신 게 아닐까 추측해보는데, 내부에서도 open 상태를 분리하신 설계 관점에서 이유가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

밑에 코멘트에 답변 남겨뒀습니다-!


const panelId = useId();

const toggle = () => {
const next = !isOpen;
onOpenChange?.(next);
if (open === undefined) setInternalOpen(next);
};
Comment on lines +37 to +41
Copy link
Member

Choose a reason for hiding this comment

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

외부/내부 open 상태가 나눠져서 이렇게 하신거군요.
위 코멘트에서 이유를 질문 드린 게 있어서 같이 이야기 해보고 싶어요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

원래 외부에서 관리하려던 이유는 라우팅과 상태를 동기화하려고 했습니다. 현재 탭이면 아코디언을 강제로 펼치거나 다른 아코디언이 추후 추가된다면 외부에서 그룹으로 관리하도록하려고 했습니다-! 하지만 이런 부분은 현재 단계에서는 적용되어있지않아 외부에서 관리를 굳이 해야하나 생각이 들 수 있습니다..하지만 아코디언을 강제로 펼치는 ux는 기획측과 상의 후 적용해보는 것에 대해 어떤가 생각중 입니다.
내부 제어는 토글 인터랙션은 컴포넌트 내부에서 처리하고, 부모 상태와는 독립적으로 동작시키려 했습니다. 변경 사실만 onOpenChange로 부모에 알리도록 했습니다. 이 부분은 깃허브나 다른 아코디언 ux를 참고했을 때 토글이 부모 요소와 관련 없이 작동하는 경우가 있어 내부에서 처리하도록했습니다

Copy link
Member

Choose a reason for hiding this comment

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

그렇다면 외부에서도 라우팅 상태와 동기화..?를 위해 외부에서 사용하기 위함이라고 이해했는데 그 부분이 맞을까요?
외부에서 필요한 거라면 외부 상태 하나만 두고 그 상태를 내부에 props로 사용하게 만드는 설계는 불가능한지 궁금합니다!


return (
<div className={cn(className)}>
<SideItem
icon={icon}
label={label}
active={active}
trailing={trailing}
open={isOpen}
onTrailingClick={toggle}
onClick={onClick}
/>

<div
id={panelId}
className={cn(
'grid overflow-hidden transition-[grid-template-rows]',
isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
>
Comment on lines +55 to +61
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성: 패널에 role/aria 속성 부여 (키보드 포커스/리더 배려)

접힘 상태에서도 포커스 가능 요소가 존재할 수 있으므로 aria-hidden으로 힌트를 주고, role="region"/aria-label을 부여해 의미를 명확히 합니다. 추후 트리거 버튼에 aria-controls/aria-expanded 연동을 추가하면 더 좋습니다.

       <div
         id={panelId}
-        className={cn(
+        role="region"
+        aria-hidden={!isOpen}
+        aria-label={`${label} 패널`}
+        className={cn(
           'grid overflow-hidden transition-[grid-template-rows]',
           isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
         )}
       >

추가 제안: SideItem의 트레일링 버튼에 접근성 속성을 전달할 수 있도록 trailingButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>를 수용하고, 여기서 aria-controls={panelId}, aria-expanded={isOpen}를 전달하면 완성됩니다. 원하시면 패치 제안 드리겠습니다.

📝 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
<div
id={panelId}
className={cn(
'grid overflow-hidden transition-[grid-template-rows]',
isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
>
<div
id={panelId}
role="region"
aria-hidden={!isOpen}
aria-label={`${label} 패널`}
className={cn(
'grid overflow-hidden transition-[grid-template-rows]',
isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
>
🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/AccordionItem.tsx around lines 51
to 57, the panel markup lacks accessibility semantics and state linkage: add
role="region", a descriptive aria-label (or aria-labelledby if a title element
exists), and toggle aria-hidden based on isOpen so screenreaders know when
content is hidden; additionally update the SideItem API to accept
trailingButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement> and pass
aria-controls={panelId} and aria-expanded={isOpen} from the trigger button to
link control and panel (ensure panelId is stable and used on the panel element).

<div className="min-h-0 gap-[0.2rem] bg-none py-[0.4rem]">
{children}
</div>
</div>
</div>
);
}
61 changes: 61 additions & 0 deletions apps/client/src/shared/components/sidebar/CategoryItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Icon } from '@pinback/design-system/icons';
import { cn } from '@pinback/design-system/utils';

interface CategoryItemProps {
id: number;
label: string;
active: boolean;
className?: string;
onClick: (id: number) => void;
onOptionsClick?: (id: number, anchorEl: HTMLElement) => void;
}

export default function CategoryItem({
id,
label,
active = false,
className,
onClick,
onOptionsClick,
}: CategoryItemProps) {
return (
<div
className={cn(
'flex h-[3.6rem] w-full items-center justify-between rounded-[0.4rem] px-[1.2rem]',
active ? 'bg-main0' : 'bg-white-bg',
'transition-colors',
className
)}
>
<button
type="button"
aria-pressed={active}
onClick={() => onClick(id)}
className={cn(
'body4-r flex-1 text-left',
active ? 'text-main600' : 'text-font-gray-2'
)}
>
{label}
</button>

<button
type="button"
aria-haspopup="menu"
aria-label="카테고리 옵션"
className="ml-2"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onOptionsClick?.(id, e.currentTarget);
}}
>
<Icon
name={active ? 'ic_details_category' : 'ic_details_disable'}
aria-hidden
className="h-[1.8rem] w-[1.8rem]"
/>
</button>
</div>
);
}
27 changes: 27 additions & 0 deletions apps/client/src/shared/components/sidebar/CreateItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Icon } from '@pinback/design-system/icons';
import { cn } from '@pinback/design-system/utils';

interface CreateItemProps {
onClick: () => void;
}
Comment on lines +4 to +6
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

onClick 타입을 React MouseEventHandler로 교정하세요

현재 () => void를 DOM onClick에 그대로 전달하면 TS에서 타입 불일치가 날 수 있습니다. button으로 전환 시 핸들러 타입을 명시적으로 맞춰주세요.

+import type { MouseEventHandler } from 'react';
 
 interface CreateItemProps {
-  onClick: () => void;
+  onClick: MouseEventHandler<HTMLButtonElement>;
 }
 
 export default function CreateItem({ onClick }: CreateItemProps) {

Also applies to: 9-9

🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/CreateItem.tsx around lines 4-6
(and also line 9), the onClick prop is typed as () => void which can conflict
with DOM button onClick types; change the prop type to
React.MouseEventHandler<HTMLButtonElement> (or import type { MouseEventHandler }
from 'react' and use MouseEventHandler<HTMLButtonElement>) so the prop matches a
button's event handler signature, and update any usages to accept the event
parameter if needed.

//TODO: onClick 이벤트 추가

export default function CreateItem({ onClick }: CreateItemProps) {
return (
<button
type="button"
className={cn(
'bg-white-bg flex h-[3.6rem] w-full gap-[0.8rem] rounded-[0.4rem] p-[0.8rem]',
'items-center transition-colors'
)}
onClick={onClick}
>
<Icon
name={'ic_plus'}
aria-hidden
className={cn('h-[1.6rem] w-[1.6rem]')}
/>
<p className={'body4-r text-main500'}>카테고리 추가</p>
</button>
);
}
61 changes: 61 additions & 0 deletions apps/client/src/shared/components/sidebar/MyLevelItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Icon } from '@pinback/design-system/icons';
import { Level, Progress } from '@pinback/design-system/ui';
import { cn } from '@pinback/design-system/utils';
import { getTreeLevel } from '@shared/utils/treeLevel';

interface MyLevelItemProps {
acorns: number;
className?: string;
isActive: boolean;
onClick: () => void;
}

export default function MyLevelItem({
acorns,
onClick,
isActive: active,
}: MyLevelItemProps) {
const info = getTreeLevel(acorns);

const barPercent = Math.min(100, info.level * 20);

return (
<div
onClick={onClick}
className={cn(
'h-[6.2rem] w-full rounded-[0.4rem] border p-[0.8rem] transition-colors',
'flex flex-row justify-between gap-[0.8rem]',
'bg-white-bg border-transparent',
'hover:bg-main0 hover:border-main400',
active && 'bg-main0 border-main400'
)}
>
<div
className="flex size-[4.6rem] items-center justify-center"
aria-hidden
>
<Icon
name="chippi_profile"
width={46}
height={46}
className="rounded-[0.4rem]"
/>
</div>

<div className="flex w-full flex-col justify-between">
<div className="flex items-center justify-between gap-2">
<span className="sub5-sb text-font-gray-2">{info.name}</span>
<Level level={info.level} />
</div>

<div className="w-full py-[0.7rem]">
<Progress
value={barPercent}
variant="profile"
aria-label={`${info.name} 레벨 진행률`}
/>
</div>
</div>
</div>
);
}
45 changes: 45 additions & 0 deletions apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx
Copy link
Member

Choose a reason for hiding this comment

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

해당 파일의 목적이 무엇인가요...?

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createPortal } from 'react-dom';
import OptionsMenuButton from '@shared/components/optionsMenuButton/OptionsMenuButton';

interface OptionsMenuPortalProps {
open: boolean;
style?: React.CSSProperties | null;
containerRef: React.RefObject<HTMLDivElement | null>;
categoryId: number | null;
getCategoryName: (id: number | null) => string;
onEdit: (id: number, name: string) => void;
onDelete: (id: number, name: string) => void;
onClose: () => void;
}

export default function OptionsMenuPortal({
open,
style,
containerRef,
categoryId,
getCategoryName,
onEdit,
onDelete,
onClose,
}: OptionsMenuPortalProps) {
if (!open || !style) return null;

const id = categoryId;
const name = getCategoryName(categoryId);

return createPortal(
<div ref={containerRef} style={{ ...style, zIndex: 10000 }}>
Copy link
Member

Choose a reason for hiding this comment

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

zIndex를 이후에 token화 해도 좋겠네요!

<OptionsMenuButton
onEdit={() => {
if (id != null) onEdit(id, name);
onClose();
}}
onDelete={() => {
if (id != null) onDelete(id, name);
onClose();
}}
/>
</div>,
document.body
);
}
65 changes: 65 additions & 0 deletions apps/client/src/shared/components/sidebar/PopupPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createPortal } from 'react-dom';
import { Popup } from '@pinback/design-system/ui';
import type { PopupState } from '@shared/hooks/useCategoryPopups';

interface Props {
popup: PopupState;
onClose: () => void;
onCreateConfirm?: () => void;
onEditConfirm?: (id: number, draft?: string) => void;
onDeleteConfirm?: (id: number) => void;
}

export default function PopupPortal({
popup,
onClose,
onCreateConfirm,
onEditConfirm,
onDeleteConfirm,
}: Props) {
if (!popup) return null;

return createPortal(
<div className="fixed inset-0 z-[11000]">
Copy link
Member

Choose a reason for hiding this comment

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

z-index...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

z-index 토큰화의 필요성을 느끼고있습니다.. 제가 너무 무분별하게 숫자를 키운감도 있고요..

Copy link
Collaborator

Choose a reason for hiding this comment

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

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 11000은 어쩌다가...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

....그 어떤것 밑에도 깔리고싶지 않았어요 ㅎ

<div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div className="absolute inset-0 grid place-items-center p-4">
{popup.kind === 'create' && (
<Popup
type="input"
title="카테고리 추가하기"
left="취소"
right="추가"
placeholder="카테고리 제목을 입력해주세요"
onLeftClick={onClose}
onRightClick={() => onCreateConfirm?.()}
/>
)}
Comment on lines +26 to +36
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

입력값 전달 누락 — 생성/수정 confirm으로 텍스트가 전달되지 않습니다

현재 onCreateConfirm/onEditConfirm에 인풋 텍스트가 전달되지 않아 API 연동 시 값 손실 위험이 큽니다. 디자인시스템 Popup의 onRightClick 시그니처를 확인해 값 전달을 연결해 주세요.

옵션 A(권장, Popup이 입력값을 인자로 넘기는 경우):

-interface Props {
+interface Props {
   popup: PopupState;
   onClose: () => void;
-  onCreateConfirm?: () => void;
-  onEditConfirm?: (id: number, draft?: string) => void;
+  onCreateConfirm?: (name: string) => void;
+  onEditConfirm?: (id: number, name: string) => void;
   onDeleteConfirm?: (id: number) => void;
 }
@@
-            onRightClick={() => onCreateConfirm?.()}
+            onRightClick={(value: string) => onCreateConfirm?.(value)}
@@
-            onRightClick={() => onEditConfirm?.(popup.id)}
+            onRightClick={(value: string) => onEditConfirm?.(popup.id, value)}

옵션 B(Popup이 값을 인자로 주지 않는 경우): 로컬 draft state를 두고 Popup에 value/onChange(혹은 유사 props)를 바인딩한 뒤 confirm에서 draft를 전달해 주세요.

시그니처 확인용 스크립트:

Also applies to: 38-47, 50-59


🏁 Script executed:

#!/bin/bash
# 디자인시스템 Popup Props 검색
fd -t f -a 'Popup.(tsx|ts|jsx|js)' 2>/dev/null
rg -nP -C2 'export (interface|type) .*Popup.*Props|function\s+Popup\(|onRightClick' -S

Length of output: 3252


Option B 사용: 로컬 draft state에 입력값을 저장하고 Popup에 value/onChange로 바인딩한 뒤 onRightClick에서 onCreateConfirm/onEditConfirm 호출 시 해당 draft를 인자로 전달하세요

🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/PopupPortal.tsx around lines 26 to
36, the Popup is currently uncontrolled for text input; implement Option B by
adding a local draft state (e.g., useState<string>), pass draft as value and an
onChange handler to Popup, update draft on user input, and when the right button
is clicked call onCreateConfirm (and onEditConfirm for edit branch) with the
current draft string as an argument instead of calling them with no params.


{popup.kind === 'edit' && (
<Popup
type="input"
title="카테고리 수정하기"
left="취소"
right="확인"
placeholder={popup.name}
onLeftClick={onClose}
onRightClick={() => onEditConfirm?.(popup.id)}
/>
)}

{popup.kind === 'delete' && (
<Popup
type="subtext"
title="정말 삭제하시겠어요?"
subtext="저장된 내용이 모두 삭제됩니다."
left="취소"
right="삭제"
onLeftClick={onClose}
onRightClick={() => onDeleteConfirm?.(popup.id)}
/>
)}
</div>
</div>,
document.body
);
}
Loading
Loading