-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(client): sidebar 구현 #52
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 all commits
c46b2d9
6b5a6eb
2950def
0d86a14
cedcc4d
7a8cbac
10570ab
b723c2c
74fd048
f6a16bf
5eb5b84
30ba55d
1defad1
6e3745b
a387a2b
21edd3d
f59e34b
189d1fd
003ad82
b763b49
533b1bd
b96f9a2
9142ec1
0a7f961
de67f64
1787935
662b576
2ade739
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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
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. sidebar 위치 잘 정의해주셨네요!! 이후 해당 PR 머지되면 사이드바 제 쪽에서 가져와서 추가 layout 작업 하겠습니다~ |
||
| </> | ||
| ); | ||
|
|
||
| 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
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. 외부 open을 받아서 쓰시는 이유는 아무래도 open 상태를 컴포넌트 외부에서도 쓰이기 때문에 부모로 올리신 게 아닐까 추측해보는데, 내부에서도 open 상태를 분리하신 설계 관점에서 이유가 궁금합니다!
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. 밑에 코멘트에 답변 남겨뒀습니다-! |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const panelId = useId(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const toggle = () => { | ||||||||||||||||||||||||||||||||||||
| const next = !isOpen; | ||||||||||||||||||||||||||||||||||||
| onOpenChange?.(next); | ||||||||||||||||||||||||||||||||||||
| if (open === undefined) setInternalOpen(next); | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+41
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. 외부/내부 open 상태가 나눠져서 이렇게 하신거군요.
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. 원래 외부에서 관리하려던 이유는 라우팅과 상태를 동기화하려고 했습니다. 현재 탭이면 아코디언을 강제로 펼치거나 다른 아코디언이 추후 추가된다면 외부에서 그룹으로 관리하도록하려고 했습니다-! 하지만 이런 부분은 현재 단계에서는 적용되어있지않아 외부에서 관리를 굳이 해야하나 생각이 들 수 있습니다..하지만 아코디언을 강제로 펼치는 ux는 기획측과 상의 후 적용해보는 것에 대해 어떤가 생각중 입니다.
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. 그렇다면 외부에서도 라우팅 상태와 동기화..?를 위해 외부에서 사용하기 위함이라고 이해했는데 그 부분이 맞을까요? |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| 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
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 접근성: 패널에 role/aria 속성 부여 (키보드 포커스/리더 배려) 접힘 상태에서도 포커스 가능 요소가 존재할 수 있으므로 <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]'
)}
>추가 제안: 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| <div className="min-h-0 gap-[0.2rem] bg-none py-[0.4rem]"> | ||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| 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> | ||
| ); | ||
| } |
| 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
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 onClick 타입을 React MouseEventHandler로 교정하세요 현재 +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 |
||
| //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> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { Icon } from '@pinback/design-system/icons'; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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> | ||
| ); | ||
| } | ||
|
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. 해당 파일의 목적이 무엇인가요...? |
| 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 }}> | ||
|
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.
|
||
| <OptionsMenuButton | ||
| onEdit={() => { | ||
| if (id != null) onEdit(id, name); | ||
| onClose(); | ||
| }} | ||
| onDelete={() => { | ||
| if (id != null) onDelete(id, name); | ||
| onClose(); | ||
| }} | ||
| /> | ||
| </div>, | ||
| document.body | ||
| ); | ||
| } | ||
| 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]"> | ||
|
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. z-index...
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. z-index 토큰화의 필요성을 느끼고있습니다.. 제가 너무 무분별하게 숫자를 키운감도 있고요..
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. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 11000은 어쩌다가...
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. ....그 어떤것 밑에도 깔리고싶지 않았어요 ㅎ |
||
| <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
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. 💡 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' -SLength of output: 3252 Option B 사용: 로컬 draft state에 입력값을 저장하고 Popup에 value/onChange로 바인딩한 뒤 onRightClick에서 onCreateConfirm/onEditConfirm 호출 시 해당 draft를 인자로 전달하세요 🤖 Prompt for AI Agents |
||
|
|
||
| {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 | ||
| ); | ||
| } | ||
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.
오옹 이 친구는 뭔가뇽
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.
cva client에서 의존성 설치가 안되어있어서 추가했습니다-! 이부분 혹시 따로 디자인시스템에서 가져와야한는 것일까요?