From ea973d6776a92e96fcde9cc91739dd3130e4885b Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Mon, 6 Jul 2026 16:23:44 +0900 Subject: [PATCH 01/43] =?UTF-8?q?chore:=20CLAUDE.md=20gitignore=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 33f14f9..32939d8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,6 @@ next-env.d.ts # agent files (개인 작업용) AGENTS.md +CLAUDE.md .agents/ .claude/ From 4f2ee1c81eab2da130cf40ffcd9129ed5c415530 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Mon, 6 Jul 2026 16:51:44 +0900 Subject: [PATCH 02/43] =?UTF-8?q?feat:=20=EC=9C=84=EC=A0=AF=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EC=B9=B4=EB=93=9C=20=EB=B0=8F=20AppShell=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=8A=AC=EB=A1=AF=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/side-project/[workspaceId]/page.tsx | 0 src/shared/ui/widget-card.tsx | 39 ++++++++++++++++++++ src/widgets/layout/app-shell/index.ts | 2 + src/widgets/layout/app-shell/ui/AppShell.tsx | 27 ++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/app/side-project/[workspaceId]/page.tsx create mode 100644 src/shared/ui/widget-card.tsx create mode 100644 src/widgets/layout/app-shell/index.ts create mode 100644 src/widgets/layout/app-shell/ui/AppShell.tsx diff --git a/src/app/side-project/[workspaceId]/page.tsx b/src/app/side-project/[workspaceId]/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/ui/widget-card.tsx b/src/shared/ui/widget-card.tsx new file mode 100644 index 0000000..78e87b2 --- /dev/null +++ b/src/shared/ui/widget-card.tsx @@ -0,0 +1,39 @@ +// 대시보드 위젯 공통 셸 — 흰 카드 컨테이너 + 헤더(제목/액션) 구성 요소 +import * as React from 'react'; + +import { cn } from '@/shared/lib/utils'; + +function WidgetCard({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( +
+ {children} +
+ ); +} + +function WidgetCardHeader({ title, action }: { title: string; action?: React.ReactNode }) { + return ( +
+

{title}

+ {action} +
+ ); +} + +function WidgetCardAction({ className, ...props }: React.ComponentProps<'button'>) { + return ( + + ); +} From d8acd1e126fa2e4a917b2cde9ff506185d201e6c Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Mon, 6 Jul 2026 17:26:20 +0900 Subject: [PATCH 10/43] =?UTF-8?q?refactor:=20AppShell=EC=9D=84=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=EC=8A=A4=ED=8E=98=EC=9D=B4=EC=8A=A4=20layout=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/side-project/[workspaceId]/layout.tsx | 21 +++++++++++++++++++ src/views/dashboard/ui/DashboardPage.tsx | 21 +++++-------------- 2 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 src/app/side-project/[workspaceId]/layout.tsx diff --git a/src/app/side-project/[workspaceId]/layout.tsx b/src/app/side-project/[workspaceId]/layout.tsx new file mode 100644 index 0000000..b0bb20f --- /dev/null +++ b/src/app/side-project/[workspaceId]/layout.tsx @@ -0,0 +1,21 @@ +// 워크스페이스 공통 레이아웃 — 사이드 프로젝트 하위 모든 페이지(대시보드/보드/캘린더 등)를 +// AppShell(사이드바·탑바 슬롯 + 메인)로 감싼다. 실제 사이드바/탑바 컴포넌트가 완성되면 +// 여기 AppShell의 sidebar/topbar props에 연결하면 전 페이지에 반영된다. +import type { ReactNode } from 'react'; +import { Plus_Jakarta_Sans } from 'next/font/google'; + +import { AppShell } from '@/widgets/layout/app-shell'; + +// Figma 지정 폰트 (랜딩과 동일) — 한글은 시스템 폰트로 fallback +const jakarta = Plus_Jakarta_Sans({ + subsets: ['latin'], + weight: ['400', '600', '700', '800'], +}); + +export default function WorkspaceLayout({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/src/views/dashboard/ui/DashboardPage.tsx b/src/views/dashboard/ui/DashboardPage.tsx index ca1682d..71e8f89 100644 --- a/src/views/dashboard/ui/DashboardPage.tsx +++ b/src/views/dashboard/ui/DashboardPage.tsx @@ -1,30 +1,19 @@ -// 사이드 프로젝트 대시보드 페이지 — AppShell(빈 사이드바/탑바 슬롯) 안에 -// 위젯 그리드와 레이아웃 편집 토글을 조립한다. +// 사이드 프로젝트 대시보드 페이지 — 위젯 그리드와 레이아웃 편집 토글을 조립한다. +// AppShell(사이드바/탑바 슬롯)과 폰트는 상위 워크스페이스 layout이 담당한다. 'use client'; -import { Plus_Jakarta_Sans } from 'next/font/google'; - import { DashboardEditToggle } from '@/widgets/dashboard/dashboard-edit-toggle'; -import { AppShell } from '@/widgets/layout/app-shell'; import { useDashboardLayout } from '../model/useDashboardLayout'; import DashboardGrid from './DashboardGrid'; -// Figma 지정 폰트 (랜딩과 동일) — 한글은 시스템 폰트로 fallback -const jakarta = Plus_Jakarta_Sans({ - subsets: ['latin'], - weight: ['400', '600', '700', '800'], -}); - export default function DashboardPage() { const { layout, editMode, handleLayoutChange, toggleEdit } = useDashboardLayout(); return ( -
- - - + <> + -
+ ); } From b32c64d312db08cc9d01a57b0cf00d472baeabb5 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Mon, 6 Jul 2026 17:31:17 +0900 Subject: [PATCH 11/43] =?UTF-8?q?refactor:=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=AA=A8=EB=8D=B8=EC=9A=A9=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=95=EC=86=8C=ED=95=98=EA=B3=A0=20UI=EB=A5=BC=20=EA=B0=81?= =?UTF-8?q?=20=EC=9C=84=EC=A0=AF=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/backlog-item/index.ts | 3 +-- .../backlog-item/ui/BacklogItemRow.tsx | 15 ----------- src/entities/meeting-note/index.ts | 3 +-- .../meeting-note/ui/MeetingNoteRow.tsx | 16 ------------ src/entities/sprint/index.ts | 3 +-- src/entities/sprint/ui/SprintBanner.tsx | 20 -------------- src/entities/task/index.ts | 3 +-- src/entities/task/ui/TaskRow.tsx | 20 -------------- .../dashboard-backlog/ui/Backlog.tsx | 13 +++++++--- .../dashboard-my-tasks/ui/MyTasks.tsx | 26 +++++++++++++++---- .../dashboard-recent-notes/ui/RecentNotes.tsx | 14 +++++++--- .../ui/SprintSummary.tsx | 19 +++++++++++--- 12 files changed, 62 insertions(+), 93 deletions(-) delete mode 100644 src/entities/backlog-item/ui/BacklogItemRow.tsx delete mode 100644 src/entities/meeting-note/ui/MeetingNoteRow.tsx delete mode 100644 src/entities/sprint/ui/SprintBanner.tsx delete mode 100644 src/entities/task/ui/TaskRow.tsx diff --git a/src/entities/backlog-item/index.ts b/src/entities/backlog-item/index.ts index 49cc417..6c020ca 100644 --- a/src/entities/backlog-item/index.ts +++ b/src/entities/backlog-item/index.ts @@ -1,5 +1,4 @@ -// backlog-item 엔티티의 Public API -export { BacklogItemRow } from './ui/BacklogItemRow'; +// backlog-item 엔티티의 Public API (모델) export { mockBacklog, BACKLOG_PRIORITY_COLOR, diff --git a/src/entities/backlog-item/ui/BacklogItemRow.tsx b/src/entities/backlog-item/ui/BacklogItemRow.tsx deleted file mode 100644 index ced42f3..0000000 --- a/src/entities/backlog-item/ui/BacklogItemRow.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// 백로그 항목 한 줄 — 우선순위 점 + 제목 + 예상 포인트. 리스트(
    )는 소비 측에서 감싼다. -import { BACKLOG_PRIORITY_COLOR, type BacklogItem } from '../model/backlog-item'; - -export function BacklogItemRow({ item }: { item: BacklogItem }) { - return ( -
  • - - {item.title} - {item.point}pt -
  • - ); -} diff --git a/src/entities/meeting-note/index.ts b/src/entities/meeting-note/index.ts index 2972afd..349f658 100644 --- a/src/entities/meeting-note/index.ts +++ b/src/entities/meeting-note/index.ts @@ -1,3 +1,2 @@ -// meeting-note 엔티티의 Public API -export { MeetingNoteRow } from './ui/MeetingNoteRow'; +// meeting-note 엔티티의 Public API (모델) export { mockMeetingNotes, type MeetingNote } from './model/meeting-note'; diff --git a/src/entities/meeting-note/ui/MeetingNoteRow.tsx b/src/entities/meeting-note/ui/MeetingNoteRow.tsx deleted file mode 100644 index c1e9456..0000000 --- a/src/entities/meeting-note/ui/MeetingNoteRow.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// 회의록 한 줄 — 문서 아이콘 + 제목 + 작성일. 리스트(
      )는 소비 측에서 감싼다. -import { FileText } from 'lucide-react'; - -import type { MeetingNote } from '../model/meeting-note'; - -export function MeetingNoteRow({ note }: { note: MeetingNote }) { - return ( -
    • - -
      -

      {note.title}

      -

      {note.date}

      -
      -
    • - ); -} diff --git a/src/entities/sprint/index.ts b/src/entities/sprint/index.ts index c2d7b2a..f4a4177 100644 --- a/src/entities/sprint/index.ts +++ b/src/entities/sprint/index.ts @@ -1,5 +1,4 @@ -// sprint 엔티티의 Public API -export { SprintBanner } from './ui/SprintBanner'; +// sprint 엔티티의 Public API (모델) export { currentSprint, sprintStats, diff --git a/src/entities/sprint/ui/SprintBanner.tsx b/src/entities/sprint/ui/SprintBanner.tsx deleted file mode 100644 index 72e9bf3..0000000 --- a/src/entities/sprint/ui/SprintBanner.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// 스프린트 배너 — 스프린트 엔티티를 그라데이션 카드로 표현(기간/남은 일수 강조) -import type { Sprint } from '../model/sprint'; - -export function SprintBanner({ sprint }: { sprint: Sprint }) { - return ( -
      -
      -

      {sprint.name}

      -

      {sprint.period}

      -
      -

      - {sprint.daysLeft}일 - 남음 -

      -
      - ); -} diff --git a/src/entities/task/index.ts b/src/entities/task/index.ts index c459099..b0aa204 100644 --- a/src/entities/task/index.ts +++ b/src/entities/task/index.ts @@ -1,3 +1,2 @@ -// task 엔티티의 Public API -export { TaskRow } from './ui/TaskRow'; +// task 엔티티의 Public API (모델) export { mockTasks, TASK_STATUS, type Task, type TaskStatus } from './model/task'; diff --git a/src/entities/task/ui/TaskRow.tsx b/src/entities/task/ui/TaskRow.tsx deleted file mode 100644 index e2b034b..0000000 --- a/src/entities/task/ui/TaskRow.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// 업무 한 줄 — 상태 점 + 제목 + 포인트 + 상태 뱃지. 리스트(
        )는 소비 측에서 감싼다. -import { TASK_STATUS, type Task } from '../model/task'; - -export function TaskRow({ task }: { task: Task }) { - const status = TASK_STATUS[task.status]; - - return ( -
      • - - {task.title} - {task.point}pt - - {status.label} - -
      • - ); -} diff --git a/src/widgets/dashboard/dashboard-backlog/ui/Backlog.tsx b/src/widgets/dashboard/dashboard-backlog/ui/Backlog.tsx index 1be16fa..fc9d173 100644 --- a/src/widgets/dashboard/dashboard-backlog/ui/Backlog.tsx +++ b/src/widgets/dashboard/dashboard-backlog/ui/Backlog.tsx @@ -1,5 +1,5 @@ -// 백로그 위젯 — backlog-item 엔티티(BacklogItemRow)를 리스트로 조립 -import { BacklogItemRow, mockBacklog } from '@/entities/backlog-item'; +// 백로그 위젯 — backlog-item 모델을 받아 대시보드용 compact 리스트로 렌더 +import { BACKLOG_PRIORITY_COLOR, mockBacklog } from '@/entities/backlog-item'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/ui/widget-card'; export default function Backlog() { @@ -8,7 +8,14 @@ export default function Backlog() { 보드} />
          {mockBacklog.map((item) => ( - +
        • + + {item.title} + {item.point}pt +
        • ))}
        diff --git a/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx b/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx index 612f7ae..60f7a7b 100644 --- a/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx +++ b/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx @@ -1,5 +1,5 @@ -// 내 업무 위젯 — task 엔티티(TaskRow)를 리스트로 조립 -import { mockTasks, TaskRow } from '@/entities/task'; +// 내 업무 위젯 — task 모델을 받아 대시보드용 compact 리스트로 렌더 +import { mockTasks, TASK_STATUS } from '@/entities/task'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/ui/widget-card'; export default function MyTasks() { @@ -7,9 +7,25 @@ export default function MyTasks() { 스프린트} />
          - {mockTasks.map((task) => ( - - ))} + {mockTasks.map((task) => { + const status = TASK_STATUS[task.status]; + return ( +
        • + + {task.title} + {task.point}pt + + {status.label} + +
        • + ); + })}
        ); diff --git a/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx b/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx index 3367881..56df3e2 100644 --- a/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx +++ b/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx @@ -1,5 +1,7 @@ -// 최근 회의록 위젯 — meeting-note 엔티티(MeetingNoteRow)를 리스트로 조립 -import { MeetingNoteRow, mockMeetingNotes } from '@/entities/meeting-note'; +// 최근 회의록 위젯 — meeting-note 모델을 받아 대시보드용 리스트로 렌더 +import { FileText } from 'lucide-react'; + +import { mockMeetingNotes } from '@/entities/meeting-note'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/ui/widget-card'; export default function RecentNotes() { @@ -8,7 +10,13 @@ export default function RecentNotes() { 전체} />
          {mockMeetingNotes.map((note) => ( - +
        • + +
          +

          {note.title}

          +

          {note.date}

          +
          +
        • ))}
        diff --git a/src/widgets/dashboard/dashboard-sprint-summary/ui/SprintSummary.tsx b/src/widgets/dashboard/dashboard-sprint-summary/ui/SprintSummary.tsx index 56c1ba5..a1ad74e 100644 --- a/src/widgets/dashboard/dashboard-sprint-summary/ui/SprintSummary.tsx +++ b/src/widgets/dashboard/dashboard-sprint-summary/ui/SprintSummary.tsx @@ -1,5 +1,5 @@ -// 스프린트 요약 위젯 — 스프린트 배너 + 포인트 통계 3종을 하나의 카드로 조립(레이아웃만 담당) -import { SprintBanner, currentSprint, sprintStats } from '@/entities/sprint'; +// 스프린트 요약 위젯 — 스프린트 배너 + 포인트 통계 3종을 하나의 카드로 조립 +import { currentSprint, sprintStats } from '@/entities/sprint'; import { StatCard } from '@/shared/ui/stat-card'; export default function SprintSummary() { @@ -7,7 +7,20 @@ export default function SprintSummary() { return (
        - + {/* 스프린트 배너 (그라데이션) */} +
        +
        +

        {currentSprint.name}

        +

        {currentSprint.period}

        +
        +

        + {currentSprint.daysLeft}일 + 남음 +

        +
        {/* 포인트 통계 3종 (계획/완료 상단, 남은 하단 전체 폭) */}
        From 676c6c89cd975ee1646bb89ae8de1339e35a95c6 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Mon, 6 Jul 2026 17:47:48 +0900 Subject: [PATCH 12/43] =?UTF-8?q?feat:=20=EC=9C=84=EC=A0=AF=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=ED=8C=90=EC=A0=95=20=EC=9C=A0=ED=8B=B8=20=EB=B0=8F?= =?UTF-8?q?=20=EB=82=B4=20=EC=97=85=EB=AC=B4=C2=B7=ED=9A=8C=EC=9D=98?= =?UTF-8?q?=EB=A1=9D=20sm=20=EB=B3=80=ED=98=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/lib/widget-size.ts | 10 +++++++ src/views/dashboard/config/widgets.tsx | 12 +++++--- src/views/dashboard/ui/DashboardGrid.tsx | 21 +++++++++----- .../dashboard-my-tasks/ui/MyTasks.tsx | 28 +++++++++++++++++-- .../dashboard-recent-notes/ui/RecentNotes.tsx | 27 ++++++++++++++++-- 5 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 src/shared/lib/widget-size.ts diff --git a/src/shared/lib/widget-size.ts b/src/shared/lib/widget-size.ts new file mode 100644 index 0000000..c8aca29 --- /dev/null +++ b/src/shared/lib/widget-size.ts @@ -0,0 +1,10 @@ +// 대시보드 위젯 크기 토큰 — 그리드 타일의 폭(w)·높이(h)로 sm/md/lg를 판정한다. +// 타일을 리사이즈하면 이 값이 바뀌어 위젯이 밀도가 다른 변형을 렌더한다. +export type WidgetSize = 'sm' | 'md' | 'lg'; + +export function getWidgetSize(w: number, h: number): WidgetSize { + // 폭이 좁거나 높이가 낮으면 리스트를 담을 수 없으므로 sm(요약) + if (w <= 3 || h <= 2) return 'sm'; + if (w <= 5) return 'md'; + return 'lg'; +} diff --git a/src/views/dashboard/config/widgets.tsx b/src/views/dashboard/config/widgets.tsx index 224a030..2054a37 100644 --- a/src/views/dashboard/config/widgets.tsx +++ b/src/views/dashboard/config/widgets.tsx @@ -1,8 +1,9 @@ // 대시보드 위젯 레지스트리 — 각 타일의 그리드 기본 배치와 렌더러를 한곳에 모은다. -// 위젯을 추가/제거하려면 이 배열만 수정하면 된다. +// render는 현재 타일 크기(sm/md/lg)를 받아 밀도가 다른 변형을 렌더할 수 있다. import type { ReactNode } from 'react'; import type { Layout, LayoutItem } from 'react-grid-layout'; +import type { WidgetSize } from '@/shared/lib/widget-size'; import { Backlog } from '@/widgets/dashboard/dashboard-backlog'; import { MyTasks } from '@/widgets/dashboard/dashboard-my-tasks'; import { RecentNotes } from '@/widgets/dashboard/dashboard-recent-notes'; @@ -12,7 +13,7 @@ import { Velocity } from '@/widgets/dashboard/dashboard-velocity'; interface DashboardWidget { /** 그리드 기본 배치 (i는 위젯 식별자) */ layout: LayoutItem; - render: () => ReactNode; + render: (size: WidgetSize) => ReactNode; } // 12컬럼 기준 — 상단 요약(전체 폭), 하단 위젯 2×2 @@ -21,12 +22,15 @@ export const DASHBOARD_WIDGETS: DashboardWidget[] = [ layout: { i: 'sprint-summary', x: 0, y: 0, w: 12, h: 4, minW: 6, minH: 4 }, render: () => , }, - { layout: { i: 'my-tasks', x: 0, y: 4, w: 6, h: 5, minW: 3, minH: 4 }, render: () => }, + { + layout: { i: 'my-tasks', x: 0, y: 4, w: 6, h: 5, minW: 3, minH: 4 }, + render: (size) => , + }, { layout: { i: 'velocity', x: 6, y: 4, w: 6, h: 5, minW: 3, minH: 4 }, render: () => }, { layout: { i: 'backlog', x: 0, y: 9, w: 6, h: 5, minW: 3, minH: 4 }, render: () => }, { layout: { i: 'recent-notes', x: 6, y: 9, w: 6, h: 5, minW: 3, minH: 3 }, - render: () => , + render: (size) => , }, ]; diff --git a/src/views/dashboard/ui/DashboardGrid.tsx b/src/views/dashboard/ui/DashboardGrid.tsx index 3ba4caf..cf217e1 100644 --- a/src/views/dashboard/ui/DashboardGrid.tsx +++ b/src/views/dashboard/ui/DashboardGrid.tsx @@ -1,15 +1,17 @@ // 대시보드 그리드 — react-grid-layout(v2)로 위젯 타일을 배치/드래그/리사이즈한다. -// 편집 모드일 때만 드래그·리사이즈가 활성화된다. +// 편집 모드일 때만 드래그·리사이즈가 활성화된다. 각 타일의 현재 크기(w/h)로 +// sm/md/lg를 판정해 위젯에 전달하므로, 리사이즈하면 위젯 내용 밀도가 바뀐다. // // 그리드는 컨테이너 실측 width와 localStorage에 의존하므로 SSR/hydration 시점에는 // 렌더하지 않고 클라이언트 마운트 이후에만 렌더한다(useSyncExternalStore로 SSR-안전하게 게이팅). -// 이렇게 하면 서버/클라이언트 HTML이 어긋나는 hydration mismatch가 발생하지 않는다. 'use client'; import { useSyncExternalStore } from 'react'; import ReactGridLayout, { useContainerWidth } from 'react-grid-layout'; import type { Layout } from 'react-grid-layout'; +import { getWidgetSize } from '@/shared/lib/widget-size'; + import { DASHBOARD_WIDGETS } from '../config/widgets'; const emptySubscribe = () => () => {}; @@ -43,11 +45,16 @@ export default function DashboardGrid({ layout, editMode, onLayoutChange }: Dash dragConfig={{ enabled: editMode }} resizeConfig={{ enabled: editMode }} > - {DASHBOARD_WIDGETS.map((widget) => ( -
        - {widget.render()} -
        - ))} + {DASHBOARD_WIDGETS.map((widget) => { + // 현재 레이아웃에서 이 위젯의 실제 크기를 찾아 sm/md/lg 판정 + const item = layout.find((entry) => entry.i === widget.layout.i) ?? widget.layout; + const size = getWidgetSize(item.w, item.h); + return ( +
        + {widget.render(size)} +
        + ); + })} )}
        diff --git a/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx b/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx index 60f7a7b..27732c5 100644 --- a/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx +++ b/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx @@ -1,11 +1,33 @@ -// 내 업무 위젯 — task 모델을 받아 대시보드용 compact 리스트로 렌더 +// 내 업무 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 +// · sm: 진행 중 개수 헤드라인 + 대기 건수 요약 +// · md/lg: task 리스트(상태 뱃지) import { mockTasks, TASK_STATUS } from '@/entities/task'; +import type { WidgetSize } from '@/shared/lib/widget-size'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/ui/widget-card'; -export default function MyTasks() { +const header = ( + 전체 보기} /> +); + +export default function MyTasks({ size = 'md' }: { size?: WidgetSize }) { + if (size === 'sm') { + const inProgress = mockTasks.filter((task) => task.status === 'progress').length; + const todo = mockTasks.filter((task) => task.status === 'todo').length; + return ( + + {header} +
        +

        {inProgress}

        +

        진행 중 · 대기 {todo}건

        +
        +
        + ); + } + + // md / lg — 리스트 return ( - 스프린트} /> + {header}
          {mockTasks.map((task) => { const status = TASK_STATUS[task.status]; diff --git a/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx b/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx index 56df3e2..3206cf4 100644 --- a/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx +++ b/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx @@ -1,13 +1,34 @@ -// 최근 회의록 위젯 — meeting-note 모델을 받아 대시보드용 리스트로 렌더 +// 최근 회의록 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 +// · sm: 가장 최근 회의록 1건(제목만) +// · md/lg: 회의록 리스트(제목 + 작성일) import { FileText } from 'lucide-react'; import { mockMeetingNotes } from '@/entities/meeting-note'; +import type { WidgetSize } from '@/shared/lib/widget-size'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/ui/widget-card'; -export default function RecentNotes() { +const header = ( + 전체 보기} /> +); + +export default function RecentNotes({ size = 'md' }: { size?: WidgetSize }) { + if (size === 'sm') { + const latest = mockMeetingNotes[0]; + return ( + + {header} +
          + + {latest.title} +
          +
          + ); + } + + // md / lg — 리스트 return ( - 전체} /> + {header}
            {mockMeetingNotes.map((note) => (
          • From ebc9533dc8b45246b36a5f110f794df9ad9a9e26 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Mon, 6 Jul 2026 21:30:59 +0900 Subject: [PATCH 13/43] =?UTF-8?q?=20refactor:=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EB=B3=84=20side-project=20=EA=B7=B8=EB=A3=B9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B2=A9=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/side-project/[workspaceId]/page.tsx | 2 +- .../{ => side-project}/backlog-item/index.ts | 0 .../backlog-item/model/backlog-item.ts | 0 .../{ => side-project}/meeting-note/index.ts | 0 .../meeting-note/model/meeting-note.ts | 0 .../{ => side-project}/sprint/index.ts | 0 .../{ => side-project}/sprint/model/sprint.ts | 2 +- src/entities/{ => side-project}/task/index.ts | 0 .../{ => side-project}/task/model/task.ts | 0 .../{ => side-project}/lib/widget-size.ts | 0 .../{ => side-project}/ui/stat-card.tsx | 0 .../{ => side-project}/ui/widget-card.tsx | 0 .../dashboard/config/widgets.tsx | 22 +++++++++---------- .../{ => side-project}/dashboard/index.ts | 0 .../dashboard/model/useDashboardLayout.ts | 0 .../dashboard/ui/DashboardGrid.tsx | 2 +- .../dashboard/ui/DashboardPage.tsx | 2 +- .../dashboard-backlog/index.ts | 0 .../dashboard-backlog/ui/Backlog.tsx | 4 ++-- .../dashboard-edit-toggle/index.ts | 0 .../ui/DashboardEditToggle.tsx | 0 .../dashboard-my-tasks/index.ts | 0 .../dashboard-my-tasks/ui/MyTasks.tsx | 6 ++--- .../dashboard-recent-notes/index.ts | 0 .../dashboard-recent-notes/ui/RecentNotes.tsx | 6 ++--- .../dashboard-sprint-summary/index.ts | 0 .../ui/SprintSummary.tsx | 4 ++-- .../dashboard-velocity/index.ts | 0 .../dashboard-velocity/ui/Velocity.tsx | 4 ++-- 29 files changed, 27 insertions(+), 27 deletions(-) rename src/entities/{ => side-project}/backlog-item/index.ts (100%) rename src/entities/{ => side-project}/backlog-item/model/backlog-item.ts (100%) rename src/entities/{ => side-project}/meeting-note/index.ts (100%) rename src/entities/{ => side-project}/meeting-note/model/meeting-note.ts (100%) rename src/entities/{ => side-project}/sprint/index.ts (100%) rename src/entities/{ => side-project}/sprint/model/sprint.ts (93%) rename src/entities/{ => side-project}/task/index.ts (100%) rename src/entities/{ => side-project}/task/model/task.ts (100%) rename src/shared/{ => side-project}/lib/widget-size.ts (100%) rename src/shared/{ => side-project}/ui/stat-card.tsx (100%) rename src/shared/{ => side-project}/ui/widget-card.tsx (100%) rename src/views/{ => side-project}/dashboard/config/widgets.tsx (54%) rename src/views/{ => side-project}/dashboard/index.ts (100%) rename src/views/{ => side-project}/dashboard/model/useDashboardLayout.ts (100%) rename src/views/{ => side-project}/dashboard/ui/DashboardGrid.tsx (97%) rename src/views/{ => side-project}/dashboard/ui/DashboardPage.tsx (88%) rename src/widgets/{dashboard => side-project}/dashboard-backlog/index.ts (100%) rename src/widgets/{dashboard => side-project}/dashboard-backlog/ui/Backlog.tsx (93%) rename src/widgets/{dashboard => side-project}/dashboard-edit-toggle/index.ts (100%) rename src/widgets/{dashboard => side-project}/dashboard-edit-toggle/ui/DashboardEditToggle.tsx (100%) rename src/widgets/{dashboard => side-project}/dashboard-my-tasks/index.ts (100%) rename src/widgets/{dashboard => side-project}/dashboard-my-tasks/ui/MyTasks.tsx (91%) rename src/widgets/{dashboard => side-project}/dashboard-recent-notes/index.ts (100%) rename src/widgets/{dashboard => side-project}/dashboard-recent-notes/ui/RecentNotes.tsx (88%) rename src/widgets/{dashboard => side-project}/dashboard-sprint-summary/index.ts (100%) rename src/widgets/{dashboard => side-project}/dashboard-sprint-summary/ui/SprintSummary.tsx (90%) rename src/widgets/{dashboard => side-project}/dashboard-velocity/index.ts (100%) rename src/widgets/{dashboard => side-project}/dashboard-velocity/ui/Velocity.tsx (91%) diff --git a/src/app/side-project/[workspaceId]/page.tsx b/src/app/side-project/[workspaceId]/page.tsx index 1ce8d17..94229ce 100644 --- a/src/app/side-project/[workspaceId]/page.tsx +++ b/src/app/side-project/[workspaceId]/page.tsx @@ -1,5 +1,5 @@ // 사이드 프로젝트 워크스페이스 대시보드 라우트 -import { DashboardPage } from '@/views/dashboard'; +import { DashboardPage } from '@/views/side-project/dashboard'; export default function Page() { return ; diff --git a/src/entities/backlog-item/index.ts b/src/entities/side-project/backlog-item/index.ts similarity index 100% rename from src/entities/backlog-item/index.ts rename to src/entities/side-project/backlog-item/index.ts diff --git a/src/entities/backlog-item/model/backlog-item.ts b/src/entities/side-project/backlog-item/model/backlog-item.ts similarity index 100% rename from src/entities/backlog-item/model/backlog-item.ts rename to src/entities/side-project/backlog-item/model/backlog-item.ts diff --git a/src/entities/meeting-note/index.ts b/src/entities/side-project/meeting-note/index.ts similarity index 100% rename from src/entities/meeting-note/index.ts rename to src/entities/side-project/meeting-note/index.ts diff --git a/src/entities/meeting-note/model/meeting-note.ts b/src/entities/side-project/meeting-note/model/meeting-note.ts similarity index 100% rename from src/entities/meeting-note/model/meeting-note.ts rename to src/entities/side-project/meeting-note/model/meeting-note.ts diff --git a/src/entities/sprint/index.ts b/src/entities/side-project/sprint/index.ts similarity index 100% rename from src/entities/sprint/index.ts rename to src/entities/side-project/sprint/index.ts diff --git a/src/entities/sprint/model/sprint.ts b/src/entities/side-project/sprint/model/sprint.ts similarity index 93% rename from src/entities/sprint/model/sprint.ts rename to src/entities/side-project/sprint/model/sprint.ts index e0e406b..ff0cd27 100644 --- a/src/entities/sprint/model/sprint.ts +++ b/src/entities/side-project/sprint/model/sprint.ts @@ -1,5 +1,5 @@ // 스프린트 도메인 모델 + 목데이터 -import type { Stat } from '@/shared/ui/stat-card'; +import type { Stat } from '@/shared/side-project/ui/stat-card'; export interface Sprint { name: string; diff --git a/src/entities/task/index.ts b/src/entities/side-project/task/index.ts similarity index 100% rename from src/entities/task/index.ts rename to src/entities/side-project/task/index.ts diff --git a/src/entities/task/model/task.ts b/src/entities/side-project/task/model/task.ts similarity index 100% rename from src/entities/task/model/task.ts rename to src/entities/side-project/task/model/task.ts diff --git a/src/shared/lib/widget-size.ts b/src/shared/side-project/lib/widget-size.ts similarity index 100% rename from src/shared/lib/widget-size.ts rename to src/shared/side-project/lib/widget-size.ts diff --git a/src/shared/ui/stat-card.tsx b/src/shared/side-project/ui/stat-card.tsx similarity index 100% rename from src/shared/ui/stat-card.tsx rename to src/shared/side-project/ui/stat-card.tsx diff --git a/src/shared/ui/widget-card.tsx b/src/shared/side-project/ui/widget-card.tsx similarity index 100% rename from src/shared/ui/widget-card.tsx rename to src/shared/side-project/ui/widget-card.tsx diff --git a/src/views/dashboard/config/widgets.tsx b/src/views/side-project/dashboard/config/widgets.tsx similarity index 54% rename from src/views/dashboard/config/widgets.tsx rename to src/views/side-project/dashboard/config/widgets.tsx index 2054a37..bf3710c 100644 --- a/src/views/dashboard/config/widgets.tsx +++ b/src/views/side-project/dashboard/config/widgets.tsx @@ -3,12 +3,12 @@ import type { ReactNode } from 'react'; import type { Layout, LayoutItem } from 'react-grid-layout'; -import type { WidgetSize } from '@/shared/lib/widget-size'; -import { Backlog } from '@/widgets/dashboard/dashboard-backlog'; -import { MyTasks } from '@/widgets/dashboard/dashboard-my-tasks'; -import { RecentNotes } from '@/widgets/dashboard/dashboard-recent-notes'; -import { SprintSummary } from '@/widgets/dashboard/dashboard-sprint-summary'; -import { Velocity } from '@/widgets/dashboard/dashboard-velocity'; +import type { WidgetSize } from '@/shared/side-project/lib/widget-size'; +import { Backlog } from '@/widgets/side-project/dashboard-backlog'; +import { MyTasks } from '@/widgets/side-project/dashboard-my-tasks'; +import { RecentNotes } from '@/widgets/side-project/dashboard-recent-notes'; +import { SprintSummary } from '@/widgets/side-project/dashboard-sprint-summary'; +import { Velocity } from '@/widgets/side-project/dashboard-velocity'; interface DashboardWidget { /** 그리드 기본 배치 (i는 위젯 식별자) */ @@ -19,17 +19,17 @@ interface DashboardWidget { // 12컬럼 기준 — 상단 요약(전체 폭), 하단 위젯 2×2 export const DASHBOARD_WIDGETS: DashboardWidget[] = [ { - layout: { i: 'sprint-summary', x: 0, y: 0, w: 12, h: 4, minW: 6, minH: 4 }, + layout: { i: 'sprint-summary', x: 0, y: 0, w: 12, h: 4, minW: 2, minH: 2 }, render: () => , }, { - layout: { i: 'my-tasks', x: 0, y: 4, w: 6, h: 5, minW: 3, minH: 4 }, + layout: { i: 'my-tasks', x: 0, y: 4, w: 6, h: 5, minW: 2, minH: 2 }, render: (size) => , }, - { layout: { i: 'velocity', x: 6, y: 4, w: 6, h: 5, minW: 3, minH: 4 }, render: () => }, - { layout: { i: 'backlog', x: 0, y: 9, w: 6, h: 5, minW: 3, minH: 4 }, render: () => }, + { layout: { i: 'velocity', x: 6, y: 4, w: 6, h: 5, minW: 2, minH: 2 }, render: () => }, + { layout: { i: 'backlog', x: 0, y: 9, w: 6, h: 5, minW: 2, minH: 2 }, render: () => }, { - layout: { i: 'recent-notes', x: 6, y: 9, w: 6, h: 5, minW: 3, minH: 3 }, + layout: { i: 'recent-notes', x: 6, y: 9, w: 6, h: 5, minW: 2, minH: 2 }, render: (size) => , }, ]; diff --git a/src/views/dashboard/index.ts b/src/views/side-project/dashboard/index.ts similarity index 100% rename from src/views/dashboard/index.ts rename to src/views/side-project/dashboard/index.ts diff --git a/src/views/dashboard/model/useDashboardLayout.ts b/src/views/side-project/dashboard/model/useDashboardLayout.ts similarity index 100% rename from src/views/dashboard/model/useDashboardLayout.ts rename to src/views/side-project/dashboard/model/useDashboardLayout.ts diff --git a/src/views/dashboard/ui/DashboardGrid.tsx b/src/views/side-project/dashboard/ui/DashboardGrid.tsx similarity index 97% rename from src/views/dashboard/ui/DashboardGrid.tsx rename to src/views/side-project/dashboard/ui/DashboardGrid.tsx index cf217e1..0987243 100644 --- a/src/views/dashboard/ui/DashboardGrid.tsx +++ b/src/views/side-project/dashboard/ui/DashboardGrid.tsx @@ -10,7 +10,7 @@ import { useSyncExternalStore } from 'react'; import ReactGridLayout, { useContainerWidth } from 'react-grid-layout'; import type { Layout } from 'react-grid-layout'; -import { getWidgetSize } from '@/shared/lib/widget-size'; +import { getWidgetSize } from '@/shared/side-project/lib/widget-size'; import { DASHBOARD_WIDGETS } from '../config/widgets'; diff --git a/src/views/dashboard/ui/DashboardPage.tsx b/src/views/side-project/dashboard/ui/DashboardPage.tsx similarity index 88% rename from src/views/dashboard/ui/DashboardPage.tsx rename to src/views/side-project/dashboard/ui/DashboardPage.tsx index 71e8f89..f9aeb67 100644 --- a/src/views/dashboard/ui/DashboardPage.tsx +++ b/src/views/side-project/dashboard/ui/DashboardPage.tsx @@ -2,7 +2,7 @@ // AppShell(사이드바/탑바 슬롯)과 폰트는 상위 워크스페이스 layout이 담당한다. 'use client'; -import { DashboardEditToggle } from '@/widgets/dashboard/dashboard-edit-toggle'; +import { DashboardEditToggle } from '@/widgets/side-project/dashboard-edit-toggle'; import { useDashboardLayout } from '../model/useDashboardLayout'; import DashboardGrid from './DashboardGrid'; diff --git a/src/widgets/dashboard/dashboard-backlog/index.ts b/src/widgets/side-project/dashboard-backlog/index.ts similarity index 100% rename from src/widgets/dashboard/dashboard-backlog/index.ts rename to src/widgets/side-project/dashboard-backlog/index.ts diff --git a/src/widgets/dashboard/dashboard-backlog/ui/Backlog.tsx b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx similarity index 93% rename from src/widgets/dashboard/dashboard-backlog/ui/Backlog.tsx rename to src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx index fc9d173..0f12d42 100644 --- a/src/widgets/dashboard/dashboard-backlog/ui/Backlog.tsx +++ b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx @@ -1,6 +1,6 @@ // 백로그 위젯 — backlog-item 모델을 받아 대시보드용 compact 리스트로 렌더 -import { BACKLOG_PRIORITY_COLOR, mockBacklog } from '@/entities/backlog-item'; -import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/ui/widget-card'; +import { BACKLOG_PRIORITY_COLOR, mockBacklog } from '@/entities/side-project/backlog-item'; +import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/side-project/ui/widget-card'; export default function Backlog() { return ( diff --git a/src/widgets/dashboard/dashboard-edit-toggle/index.ts b/src/widgets/side-project/dashboard-edit-toggle/index.ts similarity index 100% rename from src/widgets/dashboard/dashboard-edit-toggle/index.ts rename to src/widgets/side-project/dashboard-edit-toggle/index.ts diff --git a/src/widgets/dashboard/dashboard-edit-toggle/ui/DashboardEditToggle.tsx b/src/widgets/side-project/dashboard-edit-toggle/ui/DashboardEditToggle.tsx similarity index 100% rename from src/widgets/dashboard/dashboard-edit-toggle/ui/DashboardEditToggle.tsx rename to src/widgets/side-project/dashboard-edit-toggle/ui/DashboardEditToggle.tsx diff --git a/src/widgets/dashboard/dashboard-my-tasks/index.ts b/src/widgets/side-project/dashboard-my-tasks/index.ts similarity index 100% rename from src/widgets/dashboard/dashboard-my-tasks/index.ts rename to src/widgets/side-project/dashboard-my-tasks/index.ts diff --git a/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx similarity index 91% rename from src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx rename to src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx index 27732c5..95d9f44 100644 --- a/src/widgets/dashboard/dashboard-my-tasks/ui/MyTasks.tsx +++ b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx @@ -1,9 +1,9 @@ // 내 업무 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 // · sm: 진행 중 개수 헤드라인 + 대기 건수 요약 // · md/lg: task 리스트(상태 뱃지) -import { mockTasks, TASK_STATUS } from '@/entities/task'; -import type { WidgetSize } from '@/shared/lib/widget-size'; -import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/ui/widget-card'; +import { mockTasks, TASK_STATUS } from '@/entities/side-project/task'; +import type { WidgetSize } from '@/shared/side-project/lib/widget-size'; +import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/side-project/ui/widget-card'; const header = ( 전체 보기} /> diff --git a/src/widgets/dashboard/dashboard-recent-notes/index.ts b/src/widgets/side-project/dashboard-recent-notes/index.ts similarity index 100% rename from src/widgets/dashboard/dashboard-recent-notes/index.ts rename to src/widgets/side-project/dashboard-recent-notes/index.ts diff --git a/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx b/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx similarity index 88% rename from src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx rename to src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx index 3206cf4..1ea9235 100644 --- a/src/widgets/dashboard/dashboard-recent-notes/ui/RecentNotes.tsx +++ b/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx @@ -3,9 +3,9 @@ // · md/lg: 회의록 리스트(제목 + 작성일) import { FileText } from 'lucide-react'; -import { mockMeetingNotes } from '@/entities/meeting-note'; -import type { WidgetSize } from '@/shared/lib/widget-size'; -import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/ui/widget-card'; +import { mockMeetingNotes } from '@/entities/side-project/meeting-note'; +import type { WidgetSize } from '@/shared/side-project/lib/widget-size'; +import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/side-project/ui/widget-card'; const header = ( 전체 보기} /> diff --git a/src/widgets/dashboard/dashboard-sprint-summary/index.ts b/src/widgets/side-project/dashboard-sprint-summary/index.ts similarity index 100% rename from src/widgets/dashboard/dashboard-sprint-summary/index.ts rename to src/widgets/side-project/dashboard-sprint-summary/index.ts diff --git a/src/widgets/dashboard/dashboard-sprint-summary/ui/SprintSummary.tsx b/src/widgets/side-project/dashboard-sprint-summary/ui/SprintSummary.tsx similarity index 90% rename from src/widgets/dashboard/dashboard-sprint-summary/ui/SprintSummary.tsx rename to src/widgets/side-project/dashboard-sprint-summary/ui/SprintSummary.tsx index a1ad74e..ca8534b 100644 --- a/src/widgets/dashboard/dashboard-sprint-summary/ui/SprintSummary.tsx +++ b/src/widgets/side-project/dashboard-sprint-summary/ui/SprintSummary.tsx @@ -1,6 +1,6 @@ // 스프린트 요약 위젯 — 스프린트 배너 + 포인트 통계 3종을 하나의 카드로 조립 -import { currentSprint, sprintStats } from '@/entities/sprint'; -import { StatCard } from '@/shared/ui/stat-card'; +import { currentSprint, sprintStats } from '@/entities/side-project/sprint'; +import { StatCard } from '@/shared/side-project/ui/stat-card'; export default function SprintSummary() { const [planned, done, remaining] = sprintStats; diff --git a/src/widgets/dashboard/dashboard-velocity/index.ts b/src/widgets/side-project/dashboard-velocity/index.ts similarity index 100% rename from src/widgets/dashboard/dashboard-velocity/index.ts rename to src/widgets/side-project/dashboard-velocity/index.ts diff --git a/src/widgets/dashboard/dashboard-velocity/ui/Velocity.tsx b/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx similarity index 91% rename from src/widgets/dashboard/dashboard-velocity/ui/Velocity.tsx rename to src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx index bdbaa0c..72b0c77 100644 --- a/src/widgets/dashboard/dashboard-velocity/ui/Velocity.tsx +++ b/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx @@ -1,7 +1,7 @@ // 벨로시티 위젯 — 스프린트별 계획/완료 포인트를 막대로 비교 // 막대가 2그룹뿐이라 별도 차트 라이브러리 없이 순수 CSS(div height %)로 구현한다. -import { sprintVelocity, VELOCITY_MAX } from '@/entities/sprint'; -import { WidgetCard, WidgetCardHeader } from '@/shared/ui/widget-card'; +import { sprintVelocity, VELOCITY_MAX } from '@/entities/side-project/sprint'; +import { WidgetCard, WidgetCardHeader } from '@/shared/side-project/ui/widget-card'; export default function Velocity() { return ( From 988957960a40a9700453056c3c6919d8e428252a Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Mon, 6 Jul 2026 21:42:34 +0900 Subject: [PATCH 14/43] =?UTF-8?q?refactor:=20=ED=8E=B8=EC=A7=91=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=EB=9E=99=EC=85=98=EC=9D=84=20edit-dashboard-?= =?UTF-8?q?layout=20feature=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edit-dashboard-layout/index.ts | 3 +++ .../model/useDashboardLayout.ts | 23 ++++++++++--------- .../ui/DashboardEditToggle.tsx | 2 +- .../dashboard/ui/DashboardPage.tsx | 12 ++++++---- .../dashboard-edit-toggle/index.ts | 2 -- 5 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 src/features/side-project/edit-dashboard-layout/index.ts rename src/{views/side-project/dashboard => features/side-project/edit-dashboard-layout}/model/useDashboardLayout.ts (50%) rename src/{widgets/side-project/dashboard-edit-toggle => features/side-project/edit-dashboard-layout}/ui/DashboardEditToggle.tsx (91%) delete mode 100644 src/widgets/side-project/dashboard-edit-toggle/index.ts diff --git a/src/features/side-project/edit-dashboard-layout/index.ts b/src/features/side-project/edit-dashboard-layout/index.ts new file mode 100644 index 0000000..ddec3d1 --- /dev/null +++ b/src/features/side-project/edit-dashboard-layout/index.ts @@ -0,0 +1,3 @@ +// edit-dashboard-layout feature의 Public API +export { useDashboardLayout } from './model/useDashboardLayout'; +export { default as DashboardEditToggle } from './ui/DashboardEditToggle'; diff --git a/src/views/side-project/dashboard/model/useDashboardLayout.ts b/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts similarity index 50% rename from src/views/side-project/dashboard/model/useDashboardLayout.ts rename to src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts index f74eb2a..399f0f0 100644 --- a/src/views/side-project/dashboard/model/useDashboardLayout.ts +++ b/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts @@ -1,26 +1,27 @@ -// 대시보드 레이아웃 상태 훅 — react-grid-layout 배치 + 편집 모드를 관리하고 -// 편집한 배치를 localStorage에 저장해 새로고침에도 유지한다. (zustand 등 외부 스토어 불필요) +// 대시보드 레이아웃 편집 상태 훅 — react-grid-layout 배치 + 편집 모드를 관리하고 +// 편집한 배치를 localStorage에 저장해 새로고침에도 유지한다. +// +// 기본 레이아웃은 뷰가 소유(위젯 레지스트리에서 파생)하므로 인자로 주입받는다. +// → feature가 뷰에 의존하지 않아 레이어 방향(shared ← entities ← features ← widgets ← views)을 지킨다. +// DB 연동 시 localStorage 부분만 api/changeLayout 서버액션(React Query)으로 교체하면 된다. import { useCallback, useState } from 'react'; import type { Layout } from 'react-grid-layout'; -import { DEFAULT_LAYOUT } from '../config/widgets'; - const STORAGE_KEY = 'syncly-dashboard-layout'; // 저장된 레이아웃을 읽어온다. SSR(window 없음)이나 파싱 실패 시 기본 레이아웃 사용. -// 그리드는 마운트 이후에만 렌더되므로(useContainerWidth) hydration 불일치가 발생하지 않는다. -function loadLayout(): Layout { - if (typeof window === 'undefined') return DEFAULT_LAYOUT; +function loadLayout(defaultLayout: Layout): Layout { + if (typeof window === 'undefined') return defaultLayout; try { const saved = window.localStorage.getItem(STORAGE_KEY); - return saved ? (JSON.parse(saved) as Layout) : DEFAULT_LAYOUT; + return saved ? (JSON.parse(saved) as Layout) : defaultLayout; } catch { - return DEFAULT_LAYOUT; + return defaultLayout; } } -export function useDashboardLayout() { - const [layout, setLayout] = useState(loadLayout); +export function useDashboardLayout(defaultLayout: Layout) { + const [layout, setLayout] = useState(() => loadLayout(defaultLayout)); const [editMode, setEditMode] = useState(false); const handleLayoutChange = useCallback((next: Layout) => { diff --git a/src/widgets/side-project/dashboard-edit-toggle/ui/DashboardEditToggle.tsx b/src/features/side-project/edit-dashboard-layout/ui/DashboardEditToggle.tsx similarity index 91% rename from src/widgets/side-project/dashboard-edit-toggle/ui/DashboardEditToggle.tsx rename to src/features/side-project/edit-dashboard-layout/ui/DashboardEditToggle.tsx index db1755e..fa0314a 100644 --- a/src/widgets/side-project/dashboard-edit-toggle/ui/DashboardEditToggle.tsx +++ b/src/features/side-project/edit-dashboard-layout/ui/DashboardEditToggle.tsx @@ -1,4 +1,4 @@ -// 레이아웃 편집 토글 — 우하단 플로팅 버튼. 상태는 갖지 않고 props로만 제어되는 순수 위젯. +// 레이아웃 편집 토글 — 우하단 플로팅 버튼. 상태는 갖지 않고 props로만 제어되는 순수 컴포넌트. import { Check, Pencil } from 'lucide-react'; interface DashboardEditToggleProps { diff --git a/src/views/side-project/dashboard/ui/DashboardPage.tsx b/src/views/side-project/dashboard/ui/DashboardPage.tsx index f9aeb67..8a46e56 100644 --- a/src/views/side-project/dashboard/ui/DashboardPage.tsx +++ b/src/views/side-project/dashboard/ui/DashboardPage.tsx @@ -1,14 +1,18 @@ -// 사이드 프로젝트 대시보드 페이지 — 위젯 그리드와 레이아웃 편집 토글을 조립한다. +// 사이드 프로젝트 대시보드 페이지 — 위젯 그리드와 레이아웃 편집 기능을 조립한다. +// 편집 상태/토글은 edit-dashboard-layout feature가 담당하고, 뷰는 기본 레이아웃만 주입한다. // AppShell(사이드바/탑바 슬롯)과 폰트는 상위 워크스페이스 layout이 담당한다. 'use client'; -import { DashboardEditToggle } from '@/widgets/side-project/dashboard-edit-toggle'; +import { + DashboardEditToggle, + useDashboardLayout, +} from '@/features/side-project/edit-dashboard-layout'; -import { useDashboardLayout } from '../model/useDashboardLayout'; +import { DEFAULT_LAYOUT } from '../config/widgets'; import DashboardGrid from './DashboardGrid'; export default function DashboardPage() { - const { layout, editMode, handleLayoutChange, toggleEdit } = useDashboardLayout(); + const { layout, editMode, handleLayoutChange, toggleEdit } = useDashboardLayout(DEFAULT_LAYOUT); return ( <> diff --git a/src/widgets/side-project/dashboard-edit-toggle/index.ts b/src/widgets/side-project/dashboard-edit-toggle/index.ts deleted file mode 100644 index 040a1c5..0000000 --- a/src/widgets/side-project/dashboard-edit-toggle/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// dashboard-edit-toggle 위젯의 Public API -export { default as DashboardEditToggle } from './ui/DashboardEditToggle'; From 6f425b19e2c554ec6cfbd4590176d6b2ed2a278a Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Tue, 7 Jul 2026 09:45:13 +0900 Subject: [PATCH 15/43] =?UTF-8?q?feat:=20=EC=98=A4=EB=8A=98=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=9C=84=EC=A0=AF=20sm=20=EB=B3=80=ED=98=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../side-project/schedule-event/index.ts | 2 + .../schedule-event/model/schedule-event.ts | 14 +++++ .../model/useDashboardLayout.ts | 7 ++- src/shared/side-project/lib/widget-size.ts | 4 +- .../side-project/dashboard/config/widgets.tsx | 21 +++++-- .../dashboard-backlog/ui/Backlog.tsx | 23 +++++++- .../dashboard-today-schedule/index.ts | 2 + .../ui/TodaySchedule.tsx | 56 +++++++++++++++++++ 8 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 src/entities/side-project/schedule-event/index.ts create mode 100644 src/entities/side-project/schedule-event/model/schedule-event.ts create mode 100644 src/widgets/side-project/dashboard-today-schedule/index.ts create mode 100644 src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx diff --git a/src/entities/side-project/schedule-event/index.ts b/src/entities/side-project/schedule-event/index.ts new file mode 100644 index 0000000..a25a83f --- /dev/null +++ b/src/entities/side-project/schedule-event/index.ts @@ -0,0 +1,2 @@ +// schedule-event 엔티티의 Public API (모델) +export { mockTodaySchedule, type ScheduleEvent } from './model/schedule-event'; diff --git a/src/entities/side-project/schedule-event/model/schedule-event.ts b/src/entities/side-project/schedule-event/model/schedule-event.ts new file mode 100644 index 0000000..e648976 --- /dev/null +++ b/src/entities/side-project/schedule-event/model/schedule-event.ts @@ -0,0 +1,14 @@ +// 일정(ScheduleEvent) 도메인 모델 + 오늘 일정 목데이터 +export interface ScheduleEvent { + title: string; + time: string; +} + +export const mockTodaySchedule: ScheduleEvent[] = [ + { title: '디자인 리뷰 회의', time: '11:00' }, + { title: '팀 점심', time: '12:30' }, + { title: '스프린트 데일리', time: '14:00' }, + { title: '코드 리뷰', time: '15:00' }, + { title: 'API 명세 마감', time: '16:00' }, + { title: '회고 준비', time: '17:00' }, +]; diff --git a/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts b/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts index 399f0f0..0883a20 100644 --- a/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts +++ b/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts @@ -10,11 +10,16 @@ import type { Layout } from 'react-grid-layout'; const STORAGE_KEY = 'syncly-dashboard-layout'; // 저장된 레이아웃을 읽어온다. SSR(window 없음)이나 파싱 실패 시 기본 레이아웃 사용. +// 저장본에 없는 신규 위젯은 기본 배치로 채워 넣어, 위젯을 추가해도 유실되지 않게 한다. function loadLayout(defaultLayout: Layout): Layout { if (typeof window === 'undefined') return defaultLayout; try { const saved = window.localStorage.getItem(STORAGE_KEY); - return saved ? (JSON.parse(saved) as Layout) : defaultLayout; + if (!saved) return defaultLayout; + const savedLayout = JSON.parse(saved) as Layout; + const savedIds = new Set(savedLayout.map((item) => item.i)); + const added = defaultLayout.filter((item) => !savedIds.has(item.i)); + return added.length ? [...savedLayout, ...added] : savedLayout; } catch { return defaultLayout; } diff --git a/src/shared/side-project/lib/widget-size.ts b/src/shared/side-project/lib/widget-size.ts index c8aca29..ad59cca 100644 --- a/src/shared/side-project/lib/widget-size.ts +++ b/src/shared/side-project/lib/widget-size.ts @@ -4,7 +4,7 @@ export type WidgetSize = 'sm' | 'md' | 'lg'; export function getWidgetSize(w: number, h: number): WidgetSize { // 폭이 좁거나 높이가 낮으면 리스트를 담을 수 없으므로 sm(요약) - if (w <= 3 || h <= 2) return 'sm'; - if (w <= 5) return 'md'; + if (w <= 2 || h <= 2) return 'sm'; + if (w <= 3) return 'md'; return 'lg'; } diff --git a/src/views/side-project/dashboard/config/widgets.tsx b/src/views/side-project/dashboard/config/widgets.tsx index bf3710c..c7ef0ea 100644 --- a/src/views/side-project/dashboard/config/widgets.tsx +++ b/src/views/side-project/dashboard/config/widgets.tsx @@ -8,6 +8,7 @@ import { Backlog } from '@/widgets/side-project/dashboard-backlog'; import { MyTasks } from '@/widgets/side-project/dashboard-my-tasks'; import { RecentNotes } from '@/widgets/side-project/dashboard-recent-notes'; import { SprintSummary } from '@/widgets/side-project/dashboard-sprint-summary'; +import { TodaySchedule } from '@/widgets/side-project/dashboard-today-schedule'; import { Velocity } from '@/widgets/side-project/dashboard-velocity'; interface DashboardWidget { @@ -19,19 +20,29 @@ interface DashboardWidget { // 12컬럼 기준 — 상단 요약(전체 폭), 하단 위젯 2×2 export const DASHBOARD_WIDGETS: DashboardWidget[] = [ { - layout: { i: 'sprint-summary', x: 0, y: 0, w: 12, h: 4, minW: 2, minH: 2 }, + layout: { i: 'sprint-summary', x: 0, y: 0, w: 12, h: 4, minW: 6, minH: 4 }, render: () => , }, { - layout: { i: 'my-tasks', x: 0, y: 4, w: 6, h: 5, minW: 2, minH: 2 }, + layout: { i: 'my-tasks', x: 0, y: 4, w: 6, h: 5, minW: 2, minH: 3 }, render: (size) => , }, - { layout: { i: 'velocity', x: 6, y: 4, w: 6, h: 5, minW: 2, minH: 2 }, render: () => }, - { layout: { i: 'backlog', x: 0, y: 9, w: 6, h: 5, minW: 2, minH: 2 }, render: () => }, { - layout: { i: 'recent-notes', x: 6, y: 9, w: 6, h: 5, minW: 2, minH: 2 }, + layout: { i: 'velocity', x: 6, y: 4, w: 6, h: 5, minW: 4, minH: 4 }, + render: () => , + }, + { + layout: { i: 'backlog', x: 0, y: 9, w: 6, h: 5, minW: 2, minH: 3 }, + render: (size) => , + }, + { + layout: { i: 'recent-notes', x: 6, y: 9, w: 6, h: 5, minW: 2, minH: 3 }, render: (size) => , }, + { + layout: { i: 'today-schedule', x: 0, y: 14, w: 3, h: 4, minW: 2, minH: 3 }, + render: (size) => , + }, ]; /** 초기/리셋용 레이아웃 */ diff --git a/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx index 0f12d42..4dfd65e 100644 --- a/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx +++ b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx @@ -1,11 +1,30 @@ // 백로그 위젯 — backlog-item 모델을 받아 대시보드용 compact 리스트로 렌더 import { BACKLOG_PRIORITY_COLOR, mockBacklog } from '@/entities/side-project/backlog-item'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/side-project/ui/widget-card'; +import { mockMeetingNotes } from '@/entities/side-project/meeting-note'; +import { FileText } from 'lucide-react'; +import type { WidgetSize } from '@/shared/side-project/lib/widget-size'; + +const header = ( + 보드} /> +); +export default function Backlog({ size = 'md' }: { size?: WidgetSize }) { + if (size === 'sm') { + const latest = mockMeetingNotes[0]; + return ( + + {header} +
            + + {latest.title} +
            +
            + ); + } -export default function Backlog() { return ( - 보드} /> + {header}
              {mockBacklog.map((item) => (
            • diff --git a/src/widgets/side-project/dashboard-today-schedule/index.ts b/src/widgets/side-project/dashboard-today-schedule/index.ts new file mode 100644 index 0000000..eb108d1 --- /dev/null +++ b/src/widgets/side-project/dashboard-today-schedule/index.ts @@ -0,0 +1,2 @@ +// dashboard-today-schedule 위젯의 Public API +export { default as TodaySchedule } from './ui/TodaySchedule'; diff --git a/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx b/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx new file mode 100644 index 0000000..62687da --- /dev/null +++ b/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx @@ -0,0 +1,56 @@ +// 오늘 일정 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 +// · sm: 다음 일정 1건(액센트 바 + 제목 + 시간 + 외 N건) +// · md: 3건 리스트 +// · lg: 전체 리스트 +import { mockTodaySchedule } from '@/entities/side-project/schedule-event'; +import type { WidgetSize } from '@/shared/side-project/lib/widget-size'; +import { + WidgetCard, + WidgetCardAction, + WidgetCardHeader, +} from '@/shared/side-project/ui/widget-card'; + +const header = ( + 전체 보기} /> +); + +export default function TodaySchedule({ size = 'md' }: { size?: WidgetSize }) { + if (size === 'sm') { + const [next, ...rest] = mockTodaySchedule; + return ( + + {header} +
              + +
              +

              다음 일정

              +

              {next.title}

              +

              + {next.time} + {rest.length > 0 && · 외 {rest.length}건} +

              +
              +
              +
              + ); + } + + // md: 3건, lg: 전체 + const events = size === 'md' ? mockTodaySchedule.slice(0, 3) : mockTodaySchedule; + return ( + + {header} +
                + {events.map((event) => ( +
              • + +
                +

                {event.title}

                +

                {event.time}

                +
                +
              • + ))} +
              +
              + ); +} From 35bbc33f8ff40550e88cf627577cae3f79e1c0a0 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Tue, 7 Jul 2026 09:57:24 +0900 Subject: [PATCH 16/43] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=97=85=EB=AC=B4?= =?UTF-8?q?=C2=B7=ED=9A=8C=EC=9D=98=EB=A1=9D=C2=B7=EC=98=A4=EB=8A=98=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=9C=84=EC=A0=AF=20lg=20=EB=B3=80?= =?UTF-8?q?=ED=98=95=20=EB=B0=8F=20=EB=AA=A8=EB=8D=B8=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting-note/model/meeting-note.ts | 24 +++++- .../side-project/schedule-event/index.ts | 7 +- .../schedule-event/model/schedule-event.ts | 21 +++-- src/entities/side-project/task/model/task.ts | 5 +- .../dashboard-my-tasks/ui/MyTasks.tsx | 81 ++++++++++++------- .../dashboard-recent-notes/ui/RecentNotes.tsx | 26 +++++- .../ui/TodaySchedule.tsx | 19 +++-- 7 files changed, 138 insertions(+), 45 deletions(-) diff --git a/src/entities/side-project/meeting-note/model/meeting-note.ts b/src/entities/side-project/meeting-note/model/meeting-note.ts index 4311262..4aa1c2d 100644 --- a/src/entities/side-project/meeting-note/model/meeting-note.ts +++ b/src/entities/side-project/meeting-note/model/meeting-note.ts @@ -2,9 +2,29 @@ export interface MeetingNote { title: string; date: string; + /** 본문 요약(미리보기) */ + summary: string; } export const mockMeetingNotes: MeetingNote[] = [ - { title: 'Sprint 2 플래닝', date: '2025-07-01' }, - { title: 'Sprint 1 회고', date: '2025-06-28' }, + { + title: 'Sprint 2 플래닝', + date: '2025-07-01', + summary: '스프린트 목표와 백로그 우선순위를 확정했습니다.', + }, + { + title: 'Sprint 1 회고', + date: '2025-06-28', + summary: '지난 스프린트의 성과와 개선점을 논의했습니다.', + }, + { + title: '디자인 시스템 논의', + date: '2025-06-24', + summary: '공통 컴포넌트와 디자인 토큰 구조를 정리했습니다.', + }, + { + title: 'API 명세 리뷰', + date: '2025-06-20', + summary: '엔드포인트 규격과 에러 응답 형식을 검토했습니다.', + }, ]; diff --git a/src/entities/side-project/schedule-event/index.ts b/src/entities/side-project/schedule-event/index.ts index a25a83f..df82e8a 100644 --- a/src/entities/side-project/schedule-event/index.ts +++ b/src/entities/side-project/schedule-event/index.ts @@ -1,2 +1,7 @@ // schedule-event 엔티티의 Public API (모델) -export { mockTodaySchedule, type ScheduleEvent } from './model/schedule-event'; +export { + mockTodaySchedule, + SCHEDULE_TYPE_COLOR, + type ScheduleEvent, + type ScheduleEventType, +} from './model/schedule-event'; diff --git a/src/entities/side-project/schedule-event/model/schedule-event.ts b/src/entities/side-project/schedule-event/model/schedule-event.ts index e648976..8a793d1 100644 --- a/src/entities/side-project/schedule-event/model/schedule-event.ts +++ b/src/entities/side-project/schedule-event/model/schedule-event.ts @@ -1,14 +1,23 @@ // 일정(ScheduleEvent) 도메인 모델 + 오늘 일정 목데이터 +export type ScheduleEventType = 'meeting' | 'deadline'; + +/** 일정 유형별 액센트 바 색상 (회의=보라 brand-end, 마감=빨강) */ +export const SCHEDULE_TYPE_COLOR: Record = { + meeting: '#8e51ff', + deadline: '#fb2c36', +}; + export interface ScheduleEvent { title: string; time: string; + type: ScheduleEventType; } export const mockTodaySchedule: ScheduleEvent[] = [ - { title: '디자인 리뷰 회의', time: '11:00' }, - { title: '팀 점심', time: '12:30' }, - { title: '스프린트 데일리', time: '14:00' }, - { title: '코드 리뷰', time: '15:00' }, - { title: 'API 명세 마감', time: '16:00' }, - { title: '회고 준비', time: '17:00' }, + { title: '디자인 리뷰 회의', time: '11:00', type: 'meeting' }, + { title: '팀 점심', time: '12:30', type: 'meeting' }, + { title: '스프린트 데일리', time: '14:00', type: 'meeting' }, + { title: '코드 리뷰', time: '15:00', type: 'meeting' }, + { title: 'API 명세 마감', time: '16:00', type: 'deadline' }, + { title: '회고 준비', time: '17:00', type: 'meeting' }, ]; diff --git a/src/entities/side-project/task/model/task.ts b/src/entities/side-project/task/model/task.ts index 4e14388..b7b5689 100644 --- a/src/entities/side-project/task/model/task.ts +++ b/src/entities/side-project/task/model/task.ts @@ -1,5 +1,5 @@ // 업무(Task) 도메인 모델 + 상태 스타일 + 목데이터 -export type TaskStatus = 'progress' | 'todo'; +export type TaskStatus = 'progress' | 'todo' | 'done'; interface StatusStyle { label: string; @@ -11,6 +11,7 @@ interface StatusStyle { export const TASK_STATUS: Record = { progress: { label: '진행 중', dot: '#2b7fff', bg: '#e0e7ff', text: '#432dd7' }, todo: { label: '대기', dot: '#d1d5dc', bg: '#f3f4f6', text: '#6a7282' }, + done: { label: '완료', dot: '#22c55e', bg: '#dcfce7', text: '#16a34a' }, }; export interface Task { @@ -22,6 +23,8 @@ export interface Task { export const mockTasks: Task[] = [ { title: '운동 통계 차트', point: 8, status: 'progress' }, { title: '푸시 알림 설정', point: 3, status: 'progress' }, + { title: '이번 주 배포 준비', point: 5, status: 'progress' }, { title: '온보딩 플로우 개선', point: 5, status: 'todo' }, { title: '성능 최적화 (Lighthouse)', point: 5, status: 'todo' }, + { title: '스플래시 화면 개선', point: 2, status: 'done' }, ]; diff --git a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx index 95d9f44..22c389e 100644 --- a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx +++ b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx @@ -1,7 +1,8 @@ // 내 업무 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 // · sm: 진행 중 개수 헤드라인 + 대기 건수 요약 -// · md/lg: task 리스트(상태 뱃지) -import { mockTasks, TASK_STATUS } from '@/entities/side-project/task'; +// · md: task 리스트(상태 뱃지) +// · lg: 상태별 카운트 요약 + task 리스트 +import { mockTasks, TASK_STATUS, type TaskStatus } from '@/entities/side-project/task'; import type { WidgetSize } from '@/shared/side-project/lib/widget-size'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/side-project/ui/widget-card'; @@ -9,46 +10,70 @@ const header = ( 전체 보기} /> ); +const countBy = (status: TaskStatus) => mockTasks.filter((task) => task.status === status).length; + export default function MyTasks({ size = 'md' }: { size?: WidgetSize }) { if (size === 'sm') { - const inProgress = mockTasks.filter((task) => task.status === 'progress').length; - const todo = mockTasks.filter((task) => task.status === 'todo').length; return ( {header}
              -

              {inProgress}

              -

              진행 중 · 대기 {todo}건

              +

              {countBy('progress')}

              +

              진행 중 · 대기 {countBy('todo')}건

              +
              +
              + ); + } + + const list = ( +
                + {mockTasks.map((task) => { + const status = TASK_STATUS[task.status]; + return ( +
              • + + {task.title} + {task.point}pt + + {status.label} + +
              • + ); + })} +
              + ); + + if (size === 'lg') { + return ( + + {header} +
              + + 진행 중 {countBy('progress')} + + + 대기 {countBy('todo')} + + + 완료 {countBy('done')} +
              + {list}
              ); } - // md / lg — 리스트 + // md return ( {header} -
                - {mockTasks.map((task) => { - const status = TASK_STATUS[task.status]; - return ( -
              • - - {task.title} - {task.point}pt - - {status.label} - -
              • - ); - })} -
              + {list}
              ); } diff --git a/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx b/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx index 1ea9235..c49028c 100644 --- a/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx +++ b/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx @@ -1,6 +1,7 @@ // 최근 회의록 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 // · sm: 가장 최근 회의록 1건(제목만) -// · md/lg: 회의록 리스트(제목 + 작성일) +// · md: 리스트(제목 + 작성일) +// · lg: 총 개수 + 리스트(제목 + 본문 미리보기 + 작성일) import { FileText } from 'lucide-react'; import { mockMeetingNotes } from '@/entities/side-project/meeting-note'; @@ -25,7 +26,28 @@ export default function RecentNotes({ size = 'md' }: { size?: WidgetSize }) { ); } - // md / lg — 리스트 + if (size === 'lg') { + return ( + + {header} +

              총 {mockMeetingNotes.length}개의 회의록

              +
                + {mockMeetingNotes.map((note) => ( +
              • + +
                +

                {note.title}

                +

                {note.summary}

                +

                {note.date}

                +
                +
              • + ))} +
              +
              + ); + } + + // md — 리스트(제목 + 작성일) return ( {header} diff --git a/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx b/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx index 62687da..0a5a0b0 100644 --- a/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx +++ b/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx @@ -1,8 +1,11 @@ // 오늘 일정 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 // · sm: 다음 일정 1건(액센트 바 + 제목 + 시간 + 외 N건) // · md: 3건 리스트 -// · lg: 전체 리스트 -import { mockTodaySchedule } from '@/entities/side-project/schedule-event'; +// · lg: 전체 리스트 (일정 유형별 액센트 바 색상 — 마감=빨강) +import { + mockTodaySchedule, + SCHEDULE_TYPE_COLOR, +} from '@/entities/side-project/schedule-event'; import type { WidgetSize } from '@/shared/side-project/lib/widget-size'; import { WidgetCard, @@ -21,7 +24,10 @@ export default function TodaySchedule({ size = 'md' }: { size?: WidgetSize }) { {header}
              - +

              다음 일정

              {next.title}

              @@ -42,8 +48,11 @@ export default function TodaySchedule({ size = 'md' }: { size?: WidgetSize }) { {header}
                {events.map((event) => ( -
              • - +
              • +

                {event.title}

                {event.time}

                From 066fe523fee4bb9b020e9e6296d669690f6f8746 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Tue, 7 Jul 2026 10:36:41 +0900 Subject: [PATCH 17/43] =?UTF-8?q?fix:=20=EC=9C=84=EC=A0=AF=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=ED=8C=90=EC=A0=95=EC=97=90=20=EB=86=92=EC=9D=B4=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EB=B0=8F=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=ED=94=8C=EB=A1=9C=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/side-project/lib/widget-size.ts | 10 +++++--- .../dashboard-backlog/ui/Backlog.tsx | 25 +++++++++++-------- .../dashboard-my-tasks/ui/MyTasks.tsx | 2 +- .../dashboard-recent-notes/ui/RecentNotes.tsx | 4 +-- .../ui/TodaySchedule.tsx | 2 +- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/shared/side-project/lib/widget-size.ts b/src/shared/side-project/lib/widget-size.ts index ad59cca..fb342d0 100644 --- a/src/shared/side-project/lib/widget-size.ts +++ b/src/shared/side-project/lib/widget-size.ts @@ -2,9 +2,11 @@ // 타일을 리사이즈하면 이 값이 바뀌어 위젯이 밀도가 다른 변형을 렌더한다. export type WidgetSize = 'sm' | 'md' | 'lg'; +// 폭·높이 각각의 레벨을 구해 "더 작은 쪽"으로 변형을 정한다. +// → 넓지만 낮은 타일(예: w6·h3)이 lg로 잡혀 내용이 잘리는 문제를 방지한다. export function getWidgetSize(w: number, h: number): WidgetSize { - // 폭이 좁거나 높이가 낮으면 리스트를 담을 수 없으므로 sm(요약) - if (w <= 2 || h <= 2) return 'sm'; - if (w <= 3) return 'md'; - return 'lg'; + const wLevel = w <= 2 ? 0 : w <= 3 ? 1 : 2; + const hLevel = h <= 2 ? 0 : h <= 4 ? 1 : 2; + const level = Math.min(wLevel, hLevel); + return level === 0 ? 'sm' : level === 1 ? 'md' : 'lg'; } diff --git a/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx index 4dfd65e..14b70cb 100644 --- a/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx +++ b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx @@ -1,22 +1,25 @@ -// 백로그 위젯 — backlog-item 모델을 받아 대시보드용 compact 리스트로 렌더 +// 백로그 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 +// · sm: 최상위 항목 1건 + 외 N건 +// · md/lg: 우선순위 점 + 항목 + 포인트 리스트(넘치면 스크롤) import { BACKLOG_PRIORITY_COLOR, mockBacklog } from '@/entities/side-project/backlog-item'; -import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/side-project/ui/widget-card'; -import { mockMeetingNotes } from '@/entities/side-project/meeting-note'; -import { FileText } from 'lucide-react'; import type { WidgetSize } from '@/shared/side-project/lib/widget-size'; +import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/side-project/ui/widget-card'; + +const header = 보드} />; -const header = ( - 보드} /> -); export default function Backlog({ size = 'md' }: { size?: WidgetSize }) { if (size === 'sm') { - const latest = mockMeetingNotes[0]; + const [top, ...rest] = mockBacklog; return ( {header}
                - - {latest.title} + + {top.title} + {rest.length > 0 && 외 {rest.length}건}
                ); @@ -25,7 +28,7 @@ export default function Backlog({ size = 'md' }: { size?: WidgetSize }) { return ( {header} -
                  +
                    {mockBacklog.map((item) => (
                  • +
                      {mockTasks.map((task) => { const status = TASK_STATUS[task.status]; return ( diff --git a/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx b/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx index c49028c..6f4d4d0 100644 --- a/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx +++ b/src/widgets/side-project/dashboard-recent-notes/ui/RecentNotes.tsx @@ -31,7 +31,7 @@ export default function RecentNotes({ size = 'md' }: { size?: WidgetSize }) { {header}

                      총 {mockMeetingNotes.length}개의 회의록

                      -
                        +
                          {mockMeetingNotes.map((note) => (
                        • @@ -51,7 +51,7 @@ export default function RecentNotes({ size = 'md' }: { size?: WidgetSize }) { return ( {header} -
                            +
                              {mockMeetingNotes.map((note) => (
                            • diff --git a/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx b/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx index 0a5a0b0..5c1fdbe 100644 --- a/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx +++ b/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx @@ -46,7 +46,7 @@ export default function TodaySchedule({ size = 'md' }: { size?: WidgetSize }) { return ( {header} -
                                +
                                  {events.map((event) => (
                                • Date: Tue, 7 Jul 2026 11:15:26 +0900 Subject: [PATCH 18/43] =?UTF-8?q?feat:=20=EC=88=98=EC=A0=95=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edit-dashboard-layout/index.ts | 1 + .../model/useDashboardLayout.ts | 93 ++++++++++++++----- .../ui/DashboardEditToggle.tsx | 13 ++- .../ui/EditModeBanner.tsx | 14 +++ .../side-project/dashboard/config/widgets.tsx | 8 ++ .../dashboard/ui/AddWidgetBar.tsx | 54 +++++++++++ .../dashboard/ui/DashboardGrid.tsx | 75 ++++++++++++--- .../dashboard/ui/DashboardPage.tsx | 23 ++++- 8 files changed, 242 insertions(+), 39 deletions(-) create mode 100644 src/features/side-project/edit-dashboard-layout/ui/EditModeBanner.tsx create mode 100644 src/views/side-project/dashboard/ui/AddWidgetBar.tsx diff --git a/src/features/side-project/edit-dashboard-layout/index.ts b/src/features/side-project/edit-dashboard-layout/index.ts index ddec3d1..089a681 100644 --- a/src/features/side-project/edit-dashboard-layout/index.ts +++ b/src/features/side-project/edit-dashboard-layout/index.ts @@ -1,3 +1,4 @@ // edit-dashboard-layout feature의 Public API export { useDashboardLayout } from './model/useDashboardLayout'; export { default as DashboardEditToggle } from './ui/DashboardEditToggle'; +export { default as EditModeBanner } from './ui/EditModeBanner'; diff --git a/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts b/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts index 0883a20..8617d2f 100644 --- a/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts +++ b/src/features/side-project/edit-dashboard-layout/model/useDashboardLayout.ts @@ -1,44 +1,95 @@ -// 대시보드 레이아웃 편집 상태 훅 — react-grid-layout 배치 + 편집 모드를 관리하고 -// 편집한 배치를 localStorage에 저장해 새로고침에도 유지한다. +// 대시보드 레이아웃 편집 상태 훅 — 배치 + 편집 모드 + 숨긴 위젯(삭제/추가)을 관리하고 +// localStorage에 저장해 새로고침에도 유지한다. // // 기본 레이아웃은 뷰가 소유(위젯 레지스트리에서 파생)하므로 인자로 주입받는다. // → feature가 뷰에 의존하지 않아 레이어 방향(shared ← entities ← features ← widgets ← views)을 지킨다. -// DB 연동 시 localStorage 부분만 api/changeLayout 서버액션(React Query)으로 교체하면 된다. +// DB 연동 시 localStorage 부분만 api 서버액션(React Query)으로 교체하면 된다. import { useCallback, useState } from 'react'; import type { Layout } from 'react-grid-layout'; const STORAGE_KEY = 'syncly-dashboard-layout'; -// 저장된 레이아웃을 읽어온다. SSR(window 없음)이나 파싱 실패 시 기본 레이아웃 사용. +interface DashboardState { + layout: Layout; + hiddenIds: string[]; +} + // 저장본에 없는 신규 위젯은 기본 배치로 채워 넣어, 위젯을 추가해도 유실되지 않게 한다. -function loadLayout(defaultLayout: Layout): Layout { - if (typeof window === 'undefined') return defaultLayout; +function mergeMissing(layout: Layout, defaultLayout: Layout): Layout { + const ids = new Set(layout.map((item) => item.i)); + const added = defaultLayout.filter((item) => !ids.has(item.i)); + return added.length ? [...layout, ...added] : layout; +} + +function loadState(defaultLayout: Layout): DashboardState { + if (typeof window === 'undefined') return { layout: defaultLayout, hiddenIds: [] }; try { const saved = window.localStorage.getItem(STORAGE_KEY); - if (!saved) return defaultLayout; - const savedLayout = JSON.parse(saved) as Layout; - const savedIds = new Set(savedLayout.map((item) => item.i)); - const added = defaultLayout.filter((item) => !savedIds.has(item.i)); - return added.length ? [...savedLayout, ...added] : savedLayout; + if (!saved) return { layout: defaultLayout, hiddenIds: [] }; + const parsed = JSON.parse(saved); + // 구 포맷(레이아웃 배열) 호환 + if (Array.isArray(parsed)) { + return { layout: mergeMissing(parsed as Layout, defaultLayout), hiddenIds: [] }; + } + return { + layout: mergeMissing((parsed.layout ?? defaultLayout) as Layout, defaultLayout), + hiddenIds: (parsed.hiddenIds ?? []) as string[], + }; } catch { - return defaultLayout; + return { layout: defaultLayout, hiddenIds: [] }; } } export function useDashboardLayout(defaultLayout: Layout) { - const [layout, setLayout] = useState(() => loadLayout(defaultLayout)); + const [state, setState] = useState(() => loadState(defaultLayout)); const [editMode, setEditMode] = useState(false); - const handleLayoutChange = useCallback((next: Layout) => { - setLayout(next); - try { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); - } catch { - // 저장 실패 무시 (프라이빗 모드 등) - } + const update = useCallback((updater: (prev: DashboardState) => DashboardState) => { + setState((prev) => { + const next = updater(prev); + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); + } catch { + // 저장 실패 무시 (프라이빗 모드 등) + } + return next; + }); }, []); + // 그리드는 활성 위젯만 넘기므로, 숨긴 위젯의 배치는 그대로 보존한다. + const handleLayoutChange = useCallback( + (activeLayout: Layout) => + update((prev) => { + const hidden = new Set(prev.hiddenIds); + const hiddenItems = prev.layout.filter((item) => hidden.has(item.i)); + return { ...prev, layout: [...activeLayout, ...hiddenItems] }; + }), + [update], + ); + + const removeWidget = useCallback( + (id: string) => + update((prev) => + prev.hiddenIds.includes(id) ? prev : { ...prev, hiddenIds: [...prev.hiddenIds, id] }, + ), + [update], + ); + + const addWidget = useCallback( + (id: string) => + update((prev) => ({ ...prev, hiddenIds: prev.hiddenIds.filter((x) => x !== id) })), + [update], + ); + const toggleEdit = useCallback(() => setEditMode((prev) => !prev), []); - return { layout, editMode, handleLayoutChange, toggleEdit }; + return { + layout: state.layout, + hiddenIds: state.hiddenIds, + editMode, + handleLayoutChange, + removeWidget, + addWidget, + toggleEdit, + }; } diff --git a/src/features/side-project/edit-dashboard-layout/ui/DashboardEditToggle.tsx b/src/features/side-project/edit-dashboard-layout/ui/DashboardEditToggle.tsx index fa0314a..0feb518 100644 --- a/src/features/side-project/edit-dashboard-layout/ui/DashboardEditToggle.tsx +++ b/src/features/side-project/edit-dashboard-layout/ui/DashboardEditToggle.tsx @@ -1,6 +1,10 @@ // 레이아웃 편집 토글 — 우하단 플로팅 버튼. 상태는 갖지 않고 props로만 제어되는 순수 컴포넌트. +// · 보기 모드: 흰 버튼 "레이아웃 편집" +// · 편집 모드: 인디고 채움 버튼 "편집 완료" import { Check, Pencil } from 'lucide-react'; +import { cn } from '@/shared/lib/utils'; + interface DashboardEditToggleProps { editing: boolean; onToggle: () => void; @@ -12,9 +16,14 @@ export default function DashboardEditToggle({ editing, onToggle }: DashboardEdit type="button" onClick={onToggle} aria-pressed={editing} - className="text-brand-ink hover:bg-brand-surface fixed right-6 bottom-6 z-20 flex items-center gap-2 rounded-2xl border border-brand/10 bg-white px-4 py-2.5 text-sm font-semibold shadow-lg transition-colors" + className={cn( + 'fixed right-6 bottom-6 z-20 flex items-center gap-2 rounded-2xl px-4 py-2.5 text-sm font-semibold shadow-lg transition-colors', + editing + ? 'bg-[#4f39f6] text-white hover:bg-[#4530d9]' + : 'text-brand-ink hover:bg-brand-surface border border-brand/10 bg-white', + )} > - {editing ? : } + {editing ? : } {editing ? '편집 완료' : '레이아웃 편집'} ); diff --git a/src/features/side-project/edit-dashboard-layout/ui/EditModeBanner.tsx b/src/features/side-project/edit-dashboard-layout/ui/EditModeBanner.tsx new file mode 100644 index 0000000..7c1c1e3 --- /dev/null +++ b/src/features/side-project/edit-dashboard-layout/ui/EditModeBanner.tsx @@ -0,0 +1,14 @@ +// 편집 모드 안내 배너 — 편집 모드일 때 대시보드 상단에 노출 +import { GripVertical } from 'lucide-react'; + +export default function EditModeBanner() { + return ( +
                                  + +

                                  + 편집 모드 — 좌상단 ⠿ 이동 · 우하단 ↘ 크기 조절 · 우상단 + 🗑 삭제 · 아래 위젯 추가 버튼으로 카드 추가 +

                                  +
                                  + ); +} diff --git a/src/views/side-project/dashboard/config/widgets.tsx b/src/views/side-project/dashboard/config/widgets.tsx index c7ef0ea..51e6672 100644 --- a/src/views/side-project/dashboard/config/widgets.tsx +++ b/src/views/side-project/dashboard/config/widgets.tsx @@ -14,6 +14,8 @@ import { Velocity } from '@/widgets/side-project/dashboard-velocity'; interface DashboardWidget { /** 그리드 기본 배치 (i는 위젯 식별자) */ layout: LayoutItem; + /** 위젯 추가 목록에 표시할 이름 */ + title: string; render: (size: WidgetSize) => ReactNode; } @@ -21,26 +23,32 @@ interface DashboardWidget { export const DASHBOARD_WIDGETS: DashboardWidget[] = [ { layout: { i: 'sprint-summary', x: 0, y: 0, w: 12, h: 4, minW: 6, minH: 4 }, + title: '스프린트 요약', render: () => , }, { layout: { i: 'my-tasks', x: 0, y: 4, w: 6, h: 5, minW: 2, minH: 3 }, + title: '내 업무', render: (size) => , }, { layout: { i: 'velocity', x: 6, y: 4, w: 6, h: 5, minW: 4, minH: 4 }, + title: '벨로시티', render: () => , }, { layout: { i: 'backlog', x: 0, y: 9, w: 6, h: 5, minW: 2, minH: 3 }, + title: '백로그', render: (size) => , }, { layout: { i: 'recent-notes', x: 6, y: 9, w: 6, h: 5, minW: 2, minH: 3 }, + title: '최근 회의록', render: (size) => , }, { layout: { i: 'today-schedule', x: 0, y: 14, w: 3, h: 4, minW: 2, minH: 3 }, + title: '오늘 일정', render: (size) => , }, ]; diff --git a/src/views/side-project/dashboard/ui/AddWidgetBar.tsx b/src/views/side-project/dashboard/ui/AddWidgetBar.tsx new file mode 100644 index 0000000..e806af3 --- /dev/null +++ b/src/views/side-project/dashboard/ui/AddWidgetBar.tsx @@ -0,0 +1,54 @@ +// 위젯 추가 바 — 편집 모드에서 그리드 아래 노출. 숨긴 위젯을 다시 추가한다. +'use client'; + +import { useState } from 'react'; +import { Plus } from 'lucide-react'; + +interface HiddenWidget { + id: string; + title: string; +} + +interface AddWidgetBarProps { + hidden: HiddenWidget[]; + onAdd: (id: string) => void; +} + +export default function AddWidgetBar({ hidden, onAdd }: AddWidgetBarProps) { + const [open, setOpen] = useState(false); + const hasHidden = hidden.length > 0; + + return ( +
                                  + + + {open && hasHidden && ( +
                                  + {hidden.map((widget) => ( + + ))} +
                                  + )} +
                                  + ); +} diff --git a/src/views/side-project/dashboard/ui/DashboardGrid.tsx b/src/views/side-project/dashboard/ui/DashboardGrid.tsx index 0987243..be3a073 100644 --- a/src/views/side-project/dashboard/ui/DashboardGrid.tsx +++ b/src/views/side-project/dashboard/ui/DashboardGrid.tsx @@ -1,14 +1,15 @@ // 대시보드 그리드 — react-grid-layout(v2)로 위젯 타일을 배치/드래그/리사이즈한다. -// 편집 모드일 때만 드래그·리사이즈가 활성화된다. 각 타일의 현재 크기(w/h)로 -// sm/md/lg를 판정해 위젯에 전달하므로, 리사이즈하면 위젯 내용 밀도가 바뀐다. +// 편집 모드에서는 카드별 편집 chrome(이동 핸들·크기 뱃지·삭제)과 인디고 테두리가 노출되고, +// 드래그는 좌상단 핸들(.rgl-drag-handle)로만 시작된다. 숨긴 위젯은 렌더에서 제외된다. // // 그리드는 컨테이너 실측 width와 localStorage에 의존하므로 SSR/hydration 시점에는 // 렌더하지 않고 클라이언트 마운트 이후에만 렌더한다(useSyncExternalStore로 SSR-안전하게 게이팅). 'use client'; -import { useSyncExternalStore } from 'react'; +import { useSyncExternalStore, type Ref } from 'react'; import ReactGridLayout, { useContainerWidth } from 'react-grid-layout'; -import type { Layout } from 'react-grid-layout'; +import type { Layout, ResizeHandleAxis } from 'react-grid-layout'; +import { Maximize2, GripVertical, Trash2 } from 'lucide-react'; import { getWidgetSize } from '@/shared/side-project/lib/widget-size'; @@ -24,33 +25,83 @@ function useIsClient() { ); } +// 우하단 리사이즈 핸들 커스텀(원형). react-resizable 기본 클래스로 위치를 잡고 배경 삼각형은 제거한다. +const renderResizeHandle = (axis: ResizeHandleAxis, ref: Ref) => ( +
                                  } + className={`react-resizable-handle react-resizable-handle-${axis} border-brand/10 text-brand-muted -right-2! -bottom-2! flex! size-6! items-center justify-center rounded-full! border bg-white! bg-none! p-0! shadow-md transition-opacity [&::after]:hidden!`} + > + +
                                  +); + interface DashboardGridProps { layout: Layout; editMode: boolean; + hiddenIds: string[]; onLayoutChange: (layout: Layout) => void; + onRemove: (id: string) => void; } -export default function DashboardGrid({ layout, editMode, onLayoutChange }: DashboardGridProps) { +export default function DashboardGrid({ + layout, + editMode, + hiddenIds, + onLayoutChange, + onRemove, +}: DashboardGridProps) { const { width, containerRef } = useContainerWidth(); const isClient = useIsClient(); + const activeWidgets = DASHBOARD_WIDGETS.filter((widget) => !hiddenIds.includes(widget.layout.i)); + const activeLayout = layout.filter((item) => !hiddenIds.includes(item.i)); + return (
                                  {isClient && width > 0 && ( - {DASHBOARD_WIDGETS.map((widget) => { - // 현재 레이아웃에서 이 위젯의 실제 크기를 찾아 sm/md/lg 판정 - const item = layout.find((entry) => entry.i === widget.layout.i) ?? widget.layout; + {activeWidgets.map((widget) => { + const id = widget.layout.i; + const item = activeLayout.find((entry) => entry.i === id) ?? widget.layout; const size = getWidgetSize(item.w, item.h); return ( -
                                  +
                                  + {editMode && ( + <> + {/* 좌상단 이동 핸들 */} +
                                  + +
                                  + {/* 상단 크기 뱃지 */} + + {item.w}열×{item.h}행 + + {/* 우상단 삭제 */} + + + )} {widget.render(size)}
                                  ); diff --git a/src/views/side-project/dashboard/ui/DashboardPage.tsx b/src/views/side-project/dashboard/ui/DashboardPage.tsx index 8a46e56..03fea0e 100644 --- a/src/views/side-project/dashboard/ui/DashboardPage.tsx +++ b/src/views/side-project/dashboard/ui/DashboardPage.tsx @@ -1,22 +1,37 @@ // 사이드 프로젝트 대시보드 페이지 — 위젯 그리드와 레이아웃 편집 기능을 조립한다. -// 편집 상태/토글은 edit-dashboard-layout feature가 담당하고, 뷰는 기본 레이아웃만 주입한다. +// 편집 상태/토글/추가·삭제는 edit-dashboard-layout feature가 담당하고, 뷰는 위젯 목록/기본 배치를 주입한다. // AppShell(사이드바/탑바 슬롯)과 폰트는 상위 워크스페이스 layout이 담당한다. 'use client'; import { DashboardEditToggle, + EditModeBanner, useDashboardLayout, } from '@/features/side-project/edit-dashboard-layout'; -import { DEFAULT_LAYOUT } from '../config/widgets'; +import { DASHBOARD_WIDGETS, DEFAULT_LAYOUT } from '../config/widgets'; +import AddWidgetBar from './AddWidgetBar'; import DashboardGrid from './DashboardGrid'; export default function DashboardPage() { - const { layout, editMode, handleLayoutChange, toggleEdit } = useDashboardLayout(DEFAULT_LAYOUT); + const { layout, hiddenIds, editMode, handleLayoutChange, removeWidget, addWidget, toggleEdit } = + useDashboardLayout(DEFAULT_LAYOUT); + + const hiddenWidgets = DASHBOARD_WIDGETS.filter((widget) => + hiddenIds.includes(widget.layout.i), + ).map((widget) => ({ id: widget.layout.i, title: widget.title })); return ( <> - + {editMode && } + + {editMode &&