From 65d60275b701c9c14ad3ea8a5f68042dc9bef88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Tue, 7 Jul 2026 11:15:49 +0900 Subject: [PATCH 1/6] feat: add project management board page (#14) --- .../[workspaceId]/project-management/page.tsx | 5 + src/entities/project-column/index.ts | 2 + src/entities/project-column/model/types.ts | 10 + .../project-column/ui/ProjectColumn.tsx | 70 +++++++ src/entities/project-task/index.ts | 2 + src/entities/project-task/model/types.ts | 9 + .../project-task/ui/ProjectTaskCard.tsx | 59 ++++++ src/entities/workspace/index.ts | 2 +- .../workspace/model/mock-workspace.ts | 21 +- src/features/project-board/index.ts | 1 + .../project-board/model/mockProjectBoard.ts | 88 ++++++++ .../project-board/ui/ProjectBoard.tsx | 188 ++++++++++++++++++ src/views/project-management/index.ts | 1 + .../ui/ProjectManagementPage.tsx | 15 ++ .../model/workspace-navigation.ts | 10 +- .../workspace-shell/ui/WorkspaceHeader.tsx | 4 +- .../workspace-shell/ui/WorkspaceShell.tsx | 4 +- .../workspace-shell/ui/WorkspaceSidebar.tsx | 2 +- 18 files changed, 478 insertions(+), 15 deletions(-) create mode 100644 src/app/workspaces/[workspaceId]/project-management/page.tsx create mode 100644 src/entities/project-column/index.ts create mode 100644 src/entities/project-column/model/types.ts create mode 100644 src/entities/project-column/ui/ProjectColumn.tsx create mode 100644 src/entities/project-task/index.ts create mode 100644 src/entities/project-task/model/types.ts create mode 100644 src/entities/project-task/ui/ProjectTaskCard.tsx create mode 100644 src/features/project-board/index.ts create mode 100644 src/features/project-board/model/mockProjectBoard.ts create mode 100644 src/features/project-board/ui/ProjectBoard.tsx create mode 100644 src/views/project-management/index.ts create mode 100644 src/views/project-management/ui/ProjectManagementPage.tsx diff --git a/src/app/workspaces/[workspaceId]/project-management/page.tsx b/src/app/workspaces/[workspaceId]/project-management/page.tsx new file mode 100644 index 0000000..2cf7c93 --- /dev/null +++ b/src/app/workspaces/[workspaceId]/project-management/page.tsx @@ -0,0 +1,5 @@ +import { ProjectManagementPage } from '@/views/project-management'; + +export default function WorkspaceProjectManagementPage() { + return ; +} diff --git a/src/entities/project-column/index.ts b/src/entities/project-column/index.ts new file mode 100644 index 0000000..28c8778 --- /dev/null +++ b/src/entities/project-column/index.ts @@ -0,0 +1,2 @@ +export type { ProjectBoardColumn } from './model/types'; +export { ProjectColumn } from './ui/ProjectColumn'; diff --git a/src/entities/project-column/model/types.ts b/src/entities/project-column/model/types.ts new file mode 100644 index 0000000..97a9444 --- /dev/null +++ b/src/entities/project-column/model/types.ts @@ -0,0 +1,10 @@ +import type { ProjectTask } from '@/entities/project-task'; + +export type ProjectColumnTone = 'slate' | 'brand' | 'green'; + +export type ProjectBoardColumn = { + id: string; + title: string; + tone: ProjectColumnTone; + tasks: ProjectTask[]; +}; diff --git a/src/entities/project-column/ui/ProjectColumn.tsx b/src/entities/project-column/ui/ProjectColumn.tsx new file mode 100644 index 0000000..013c4eb --- /dev/null +++ b/src/entities/project-column/ui/ProjectColumn.tsx @@ -0,0 +1,70 @@ +import type { DragEvent } from 'react'; +import { ProjectTaskCard } from '@/entities/project-task'; +import { cn } from '@/shared/lib/utils'; +import type { ProjectBoardColumn } from '../model/types'; + +type ProjectColumnProps = { + column: ProjectBoardColumn; + onDeleteTask: (taskId: string) => void; + onDropTask: (columnId: string, taskId?: string) => void; + onDragStartTask: (event: DragEvent, taskId: string) => void; + onDragEndTask: () => void; + draggingTaskId: string | null; +}; + +const toneStyles = { + slate: 'bg-[#f1f3f999] text-[#9aa3b2]', + brand: 'bg-[#f1f3f999] text-brand-start', + green: 'bg-[#f1f3f999] text-[#00c950]', +} as const; + +export function ProjectColumn({ + column, + onDeleteTask, + onDropTask, + onDragStartTask, + onDragEndTask, + draggingTaskId, +}: ProjectColumnProps) { + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + const taskId = event.dataTransfer.getData('text/plain') || undefined; + onDropTask(column.id, taskId); + }; + + return ( +
event.preventDefault()} + onDragEnter={(event) => event.preventDefault()} + onDrop={handleDrop} + > +
+
+ +

{column.title}

+
+ {column.tasks.length} +
+ +
event.preventDefault()} + onDragEnter={(event) => event.preventDefault()} + onDrop={handleDrop} + > + {column.tasks.map((task) => ( + + ))} +
+
+ ); +} diff --git a/src/entities/project-task/index.ts b/src/entities/project-task/index.ts new file mode 100644 index 0000000..8b3c9ca --- /dev/null +++ b/src/entities/project-task/index.ts @@ -0,0 +1,2 @@ +export type { ProjectTask } from './model/types'; +export { ProjectTaskCard } from './ui/ProjectTaskCard'; diff --git a/src/entities/project-task/model/types.ts b/src/entities/project-task/model/types.ts new file mode 100644 index 0000000..116d0cd --- /dev/null +++ b/src/entities/project-task/model/types.ts @@ -0,0 +1,9 @@ +export type ProjectTask = { + id: string; + title: string; + assignee: string; + assigneeInitial: string; + assigneeColor: string; + dueDate: string; + columnId: string; +}; diff --git a/src/entities/project-task/ui/ProjectTaskCard.tsx b/src/entities/project-task/ui/ProjectTaskCard.tsx new file mode 100644 index 0000000..ec53e8f --- /dev/null +++ b/src/entities/project-task/ui/ProjectTaskCard.tsx @@ -0,0 +1,59 @@ +import type { DragEvent } from 'react'; +import { Clock3, X } from 'lucide-react'; +import type { ProjectTask } from '../model/types'; + +type ProjectTaskCardProps = { + task: ProjectTask; + onDelete: (taskId: string) => void; + onDragStart: (event: DragEvent, taskId: string) => void; + onDragEnd: () => void; + isDragging?: boolean; +}; + +export function ProjectTaskCard({ + task, + onDelete, + onDragStart, + onDragEnd, + isDragging = false, +}: ProjectTaskCardProps) { + return ( +
onDragStart(event, task.id)} + onDragEnd={onDragEnd} + className="group rounded-[22px] border border-brand/10 bg-white p-[18px] shadow-[0_1px_2px_rgba(0,0,0,0.08),0_2px_6px_rgba(0,0,0,0.04)] transition-opacity" + style={{ opacity: isDragging ? 0.55 : 1 }} + > +
+

+ {task.title} +

+ +
+
+
+
+ {task.assigneeInitial} +
+ {task.assignee} +
+ +
+ + {task.dueDate} +
+
+
+ ); +} diff --git a/src/entities/workspace/index.ts b/src/entities/workspace/index.ts index 659fd91..f6d636d 100644 --- a/src/entities/workspace/index.ts +++ b/src/entities/workspace/index.ts @@ -1,5 +1,5 @@ // workspace 엔티티 Public API export type { Workspace, WorkspacePurpose, WorkspaceSummary } from './model/workspace.types'; -export { mockWorkspace } from './model/mock-workspace'; +export { getMockWorkspaceById, mockWorkspace } from './model/mock-workspace'; export { WORKSPACE_PURPOSE_META, FALLBACK_PURPOSE_META } from './config/purpose'; export { getMyWorkspaces } from './api/get-my-workspaces'; diff --git a/src/entities/workspace/model/mock-workspace.ts b/src/entities/workspace/model/mock-workspace.ts index e482c6e..5ceb752 100644 --- a/src/entities/workspace/model/mock-workspace.ts +++ b/src/entities/workspace/model/mock-workspace.ts @@ -1,8 +1,21 @@ // 워크스페이스 API 연결 전 사이드바에 표시하는 현재 워크스페이스 목업입니다. import type { Workspace } from './workspace.types'; -export const mockWorkspace: Workspace = { - id: 'test', - name: '카페 그레이 운영', - purpose: 'store-operation', +const mockWorkspacesById: Record = { + test: { + id: 'test', + name: '캡스톤 디자인 팀', + purpose: 'team-project', + }, + 'store-test': { + id: 'store-test', + name: '카페 그레이 운영', + purpose: 'store-operation', + }, }; + +export const mockWorkspace: Workspace = mockWorkspacesById['store-test']; + +export function getMockWorkspaceById(workspaceId: string): Workspace { + return mockWorkspacesById[workspaceId] ?? mockWorkspace; +} diff --git a/src/features/project-board/index.ts b/src/features/project-board/index.ts new file mode 100644 index 0000000..7e0b190 --- /dev/null +++ b/src/features/project-board/index.ts @@ -0,0 +1 @@ +export { ProjectBoard } from './ui/ProjectBoard'; diff --git a/src/features/project-board/model/mockProjectBoard.ts b/src/features/project-board/model/mockProjectBoard.ts new file mode 100644 index 0000000..c0909e2 --- /dev/null +++ b/src/features/project-board/model/mockProjectBoard.ts @@ -0,0 +1,88 @@ +import type { ProjectBoardColumn } from '@/entities/project-column'; + +export const initialProjectBoard: ProjectBoardColumn[] = [ + { + id: 'todo', + title: '대기', + tone: 'slate', + tasks: [ + { + id: 'task-1', + title: '사용자 인터뷰 설문지 제작', + assignee: '박서준', + assigneeInitial: '박', + assigneeColor: '#00C950', + dueDate: '7/5', + columnId: 'todo', + }, + { + id: 'task-2', + title: 'DB 스키마 설계', + assignee: '김지은', + assigneeInitial: '김', + assigneeColor: '#FE9A00', + dueDate: '7/6', + columnId: 'todo', + }, + { + id: 'task-3', + title: '스프린트 1 회고 준비', + assignee: '이하은', + assigneeInitial: '이', + assigneeColor: '#615FFF', + dueDate: '7/10', + columnId: 'todo', + }, + ], + }, + { + id: 'in-progress', + title: '진행 중', + tone: 'brand', + tasks: [ + { + id: 'task-4', + title: '와이어프레임 초안 작성', + assignee: '김지은', + assigneeInitial: '김', + assigneeColor: '#00B8DB', + dueDate: '7/3', + columnId: 'in-progress', + }, + { + id: 'task-5', + title: '랜딩 페이지 디자인', + assignee: '최민준', + assigneeInitial: '최', + assigneeColor: '#2B7FFF', + dueDate: '7/8', + columnId: 'in-progress', + }, + ], + }, + { + id: 'done', + title: '완료', + tone: 'green', + tasks: [ + { + id: 'task-6', + title: 'API 명세서 문서화', + assignee: '이하은', + assigneeInitial: '이', + assigneeColor: '#615FFF', + dueDate: '7/2', + columnId: 'done', + }, + { + id: 'task-7', + title: '로고 시안 3종 작성', + assignee: '박서준', + assigneeInitial: '박', + assigneeColor: '#FE9A00', + dueDate: '6/30', + columnId: 'done', + }, + ], + }, +]; diff --git a/src/features/project-board/ui/ProjectBoard.tsx b/src/features/project-board/ui/ProjectBoard.tsx new file mode 100644 index 0000000..c5f916f --- /dev/null +++ b/src/features/project-board/ui/ProjectBoard.tsx @@ -0,0 +1,188 @@ +'use client'; + +import type { DragEvent, KeyboardEvent } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { Plus, X } from 'lucide-react'; +import { ProjectColumn } from '@/entities/project-column'; +import type { ProjectBoardColumn } from '@/entities/project-column'; +import type { ProjectTask } from '@/entities/project-task'; +import { initialProjectBoard } from '../model/mockProjectBoard'; + +const DEFAULT_ASSIGNEE = { + name: '김지은', + initial: '김', + color: '#1BB6DB', +}; + +export function ProjectBoard() { + const [isComposerOpen, setIsComposerOpen] = useState(false); + const [taskTitle, setTaskTitle] = useState(''); + const [columns, setColumns] = useState(initialProjectBoard); + const [draggingTaskId, setDraggingTaskId] = useState(null); + const inputRef = useRef(null); + + useEffect(() => { + if (!isComposerOpen) { + return; + } + + inputRef.current?.focus(); + }, [isComposerOpen]); + + const taskCount = useMemo( + () => columns.reduce((count, column) => count + column.tasks.length, 0), + [columns], + ); + + const createTask = () => { + const trimmedTitle = taskTitle.trim(); + + if (!trimmedTitle) { + return; + } + + const today = new Date(); + const formattedDate = `${today.getMonth() + 1}/${today.getDate()}`; + + const newTask: ProjectTask = { + id: `task-${taskCount + 1}-${Date.now()}`, + title: trimmedTitle, + assignee: DEFAULT_ASSIGNEE.name, + assigneeInitial: DEFAULT_ASSIGNEE.initial, + assigneeColor: DEFAULT_ASSIGNEE.color, + dueDate: formattedDate, + columnId: 'todo', + }; + + setColumns((currentColumns) => + currentColumns.map((column) => + column.id === 'todo' + ? { ...column, tasks: [...column.tasks, newTask] } + : column, + ), + ); + setTaskTitle(''); + setIsComposerOpen(false); + }; + + const handleComposerKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Enter') { + return; + } + + event.preventDefault(); + createTask(); + }; + + const handleDeleteTask = (taskId: string) => { + setColumns((currentColumns) => + currentColumns.map((column) => ({ + ...column, + tasks: column.tasks.filter((task) => task.id !== taskId), + })), + ); + }; + + const handleDropTask = (targetColumnId: string, droppedTaskId?: string) => { + const activeTaskId = droppedTaskId ?? draggingTaskId; + + if (!activeTaskId) { + return; + } + + let movedTask: ProjectTask | null = null; + + const nextColumns = columns.map((column) => { + const remainingTasks = column.tasks.filter((task) => { + const isDraggingTask = task.id === activeTaskId; + + if (isDraggingTask) { + movedTask = { ...task, columnId: targetColumnId }; + } + + return !isDraggingTask; + }); + + return { ...column, tasks: remainingTasks }; + }); + + if (!movedTask) { + setDraggingTaskId(null); + return; + } + + setColumns( + nextColumns.map((column) => + column.id === targetColumnId + ? { ...column, tasks: [...column.tasks, movedTask] } + : column, + ), + ); + setDraggingTaskId(null); + }; + + const handleDragStartTask = (event: DragEvent, taskId: string) => { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', taskId); + setDraggingTaskId(taskId); + }; + + return ( +
+
+

+ 프로젝트 관리 +

+ +
+ + {isComposerOpen ? ( +
+
+ setTaskTitle(event.target.value)} + onKeyDown={handleComposerKeyDown} + /> + +
+
+ ) : null} + +
+ {columns.map((column: ProjectBoardColumn) => ( + setDraggingTaskId(null)} + draggingTaskId={draggingTaskId} + /> + ))} +
+
+ ); +} diff --git a/src/views/project-management/index.ts b/src/views/project-management/index.ts new file mode 100644 index 0000000..918e057 --- /dev/null +++ b/src/views/project-management/index.ts @@ -0,0 +1 @@ +export { default as ProjectManagementPage } from './ui/ProjectManagementPage'; diff --git a/src/views/project-management/ui/ProjectManagementPage.tsx b/src/views/project-management/ui/ProjectManagementPage.tsx new file mode 100644 index 0000000..afaec59 --- /dev/null +++ b/src/views/project-management/ui/ProjectManagementPage.tsx @@ -0,0 +1,15 @@ +import { Plus_Jakarta_Sans } from 'next/font/google'; +import { ProjectBoard } from '@/features/project-board'; + +const jakarta = Plus_Jakarta_Sans({ + subsets: ['latin'], + weight: ['400', '500', '600', '700', '800'], +}); + +export default function ProjectManagementPage() { + return ( +
+ +
+ ); +} diff --git a/src/widgets/workspace-shell/model/workspace-navigation.ts b/src/widgets/workspace-shell/model/workspace-navigation.ts index cb56ede..d36b57f 100644 --- a/src/widgets/workspace-shell/model/workspace-navigation.ts +++ b/src/widgets/workspace-shell/model/workspace-navigation.ts @@ -1,16 +1,16 @@ // 워크스페이스 목적별 사이드바 메뉴 항목을 정의합니다. import type { WorkspacePurpose } from '@/entities/workspace'; import { + BarChart3, Bell, Calendar, ClipboardList, FileBox, + FileText, LayoutDashboard, MessageSquare, - Settings, Rocket, - FileText, - BarChart3, + Settings, type LucideIcon, } from 'lucide-react'; @@ -30,7 +30,6 @@ export const storeOperationNavigationItems: WorkspaceNavigationItem[] = [ { label: '설정', href: 'settings', icon: Settings }, ]; -// TODO: 사이드 프로젝트 도메인을 만들 때 아래 메뉴를 별도 purpose 전용 사이드바로 연결합니다. export const sideProjectNavigationItems: WorkspaceNavigationItem[] = [ { label: '대시보드', href: 'dashboard', icon: LayoutDashboard }, { label: '스프린트 보드', href: 'sprint-board', icon: Rocket }, @@ -42,10 +41,9 @@ export const sideProjectNavigationItems: WorkspaceNavigationItem[] = [ { label: '설정', href: 'settings', icon: Settings }, ]; -// TODO: 팀 프로젝트 도메인을 만들 때 아래 메뉴를 별도 purpose 전용 사이드바로 연결합니다. export const teamProjectNavigationItems: WorkspaceNavigationItem[] = [ { label: '대시보드', href: 'dashboard', icon: LayoutDashboard }, - { label: '프로젝트 관리', href: 'sprint-board', icon: Rocket }, + { label: '프로젝트 관리', href: 'project-management', icon: Rocket }, { label: '캘린더', href: 'calendar', icon: Calendar }, { label: '공지', href: 'notices', icon: Bell }, { label: '회의록', href: 'meeting-notes', icon: FileText }, diff --git a/src/widgets/workspace-shell/ui/WorkspaceHeader.tsx b/src/widgets/workspace-shell/ui/WorkspaceHeader.tsx index 1e15ae5..f9e0c51 100644 --- a/src/widgets/workspace-shell/ui/WorkspaceHeader.tsx +++ b/src/widgets/workspace-shell/ui/WorkspaceHeader.tsx @@ -11,7 +11,9 @@ interface WorkspaceHeaderProps { } function getCurrentPageTitle(pathname: string, navigationItems: WorkspaceNavigationItem[]): string { - const currentNavigationItem = navigationItems.find((item) => pathname.endsWith(`/${item.href}`)); + const currentNavigationItem = navigationItems.find((item) => + pathname.endsWith(`/${item.href}`), + ); return currentNavigationItem?.label ?? '대시보드'; } diff --git a/src/widgets/workspace-shell/ui/WorkspaceShell.tsx b/src/widgets/workspace-shell/ui/WorkspaceShell.tsx index bf8189e..f3111bb 100644 --- a/src/widgets/workspace-shell/ui/WorkspaceShell.tsx +++ b/src/widgets/workspace-shell/ui/WorkspaceShell.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { WorkspaceHeader } from './WorkspaceHeader'; import { WorkspaceSidebar } from './WorkspaceSidebar'; -import { mockWorkspace } from '@/entities/workspace'; +import { getMockWorkspaceById } from '@/entities/workspace'; import { getWorkspaceNavigation } from '@/widgets/workspace-shell/lib/get-workspace-navigation'; interface WorkspaceShellProps { @@ -14,7 +14,7 @@ interface WorkspaceShellProps { export function WorkspaceShell({ workspaceId, children }: WorkspaceShellProps) { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); - const workspace = mockWorkspace; // 나중에는 workspaceId로 Supabase 조회 + const workspace = getMockWorkspaceById(workspaceId); // 나중에는 workspaceId로 Supabase 조회 const navigationItems = getWorkspaceNavigation(workspace.purpose); return ( diff --git a/src/widgets/workspace-shell/ui/WorkspaceSidebar.tsx b/src/widgets/workspace-shell/ui/WorkspaceSidebar.tsx index 249f4a4..626031b 100644 --- a/src/widgets/workspace-shell/ui/WorkspaceSidebar.tsx +++ b/src/widgets/workspace-shell/ui/WorkspaceSidebar.tsx @@ -1,6 +1,6 @@ 'use client'; -// 매장 운영 워크스페이스 페이지에서 공통으로 사용하는 좌측 사이드바입니다. +// 워크스페이스 페이지에서 공통으로 사용하는 좌측 사이드바입니다. import Image from 'next/image'; import Link from 'next/link'; import { ChevronRight, LogOut, Menu, Store } from 'lucide-react'; From 52836ae48d733ecd1cafdec0763691e1d9da5aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Tue, 7 Jul 2026 15:16:57 +0900 Subject: [PATCH 2/6] refactor: align project board with workspace task model (#14) --- .../[workspaceId]/project-management/page.tsx | 14 +- src/entities/project-column/model/types.ts | 6 +- .../project-column/ui/ProjectColumn.tsx | 63 ++++++--- src/entities/project-task/index.ts | 2 - src/entities/task/index.ts | 3 + .../task/model/mock-tasks-by-workspace.ts | 80 ++++++++++++ .../types.ts => task/model/task.types.ts} | 7 +- .../ui/TaskCard.tsx} | 20 +-- .../project-board/model/mockProjectBoard.ts | 88 ------------- .../model/project-board-columns.ts | 23 ++++ .../project-board/ui/ProjectBoard.tsx | 122 ++++++++++++------ .../ui/ProjectManagementPage.tsx | 12 +- 12 files changed, 273 insertions(+), 167 deletions(-) delete mode 100644 src/entities/project-task/index.ts create mode 100644 src/entities/task/index.ts create mode 100644 src/entities/task/model/mock-tasks-by-workspace.ts rename src/entities/{project-task/model/types.ts => task/model/task.types.ts} (50%) rename src/entities/{project-task/ui/ProjectTaskCard.tsx => task/ui/TaskCard.tsx} (65%) delete mode 100644 src/features/project-board/model/mockProjectBoard.ts create mode 100644 src/features/project-board/model/project-board-columns.ts diff --git a/src/app/workspaces/[workspaceId]/project-management/page.tsx b/src/app/workspaces/[workspaceId]/project-management/page.tsx index 2cf7c93..3b86497 100644 --- a/src/app/workspaces/[workspaceId]/project-management/page.tsx +++ b/src/app/workspaces/[workspaceId]/project-management/page.tsx @@ -1,5 +1,15 @@ import { ProjectManagementPage } from '@/views/project-management'; -export default function WorkspaceProjectManagementPage() { - return ; +interface WorkspaceProjectManagementPageProps { + params: Promise<{ + workspaceId: string; + }>; +} + +export default async function WorkspaceProjectManagementPage({ + params, +}: WorkspaceProjectManagementPageProps) { + const { workspaceId } = await params; + + return ; } diff --git a/src/entities/project-column/model/types.ts b/src/entities/project-column/model/types.ts index 97a9444..f9519ab 100644 --- a/src/entities/project-column/model/types.ts +++ b/src/entities/project-column/model/types.ts @@ -1,10 +1,10 @@ -import type { ProjectTask } from '@/entities/project-task'; +import type { Task, TaskStatus } from '@/entities/task'; export type ProjectColumnTone = 'slate' | 'brand' | 'green'; export type ProjectBoardColumn = { - id: string; + id: TaskStatus; title: string; tone: ProjectColumnTone; - tasks: ProjectTask[]; + tasks: Task[]; }; diff --git a/src/entities/project-column/ui/ProjectColumn.tsx b/src/entities/project-column/ui/ProjectColumn.tsx index 013c4eb..0dbe180 100644 --- a/src/entities/project-column/ui/ProjectColumn.tsx +++ b/src/entities/project-column/ui/ProjectColumn.tsx @@ -1,15 +1,18 @@ import type { DragEvent } from 'react'; -import { ProjectTaskCard } from '@/entities/project-task'; +import { TaskCard } from '@/entities/task'; import { cn } from '@/shared/lib/utils'; import type { ProjectBoardColumn } from '../model/types'; type ProjectColumnProps = { column: ProjectBoardColumn; onDeleteTask: (taskId: string) => void; - onDropTask: (columnId: string, taskId?: string) => void; + onDropTask: (columnId: string, targetIndex: number, taskId?: string) => void; onDragStartTask: (event: DragEvent, taskId: string) => void; onDragEndTask: () => void; draggingTaskId: string | null; + dragOverIndex: number | null; + onDragOverTask: (columnId: string, targetIndex: number) => void; + onDragLeaveColumn: (columnId: string) => void; }; const toneStyles = { @@ -25,20 +28,25 @@ export function ProjectColumn({ onDragStartTask, onDragEndTask, draggingTaskId, + dragOverIndex, + onDragOverTask, + onDragLeaveColumn, }: ProjectColumnProps) { - const handleDrop = (event: DragEvent) => { + const handleDrop = (event: DragEvent, targetIndex: number) => { event.preventDefault(); event.stopPropagation(); const taskId = event.dataTransfer.getData('text/plain') || undefined; - onDropTask(column.id, taskId); + onDropTask(column.id, targetIndex, taskId); }; return (
event.preventDefault()} - onDragEnter={(event) => event.preventDefault()} - onDrop={handleDrop} + onDragLeave={(event) => { + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { + onDragLeaveColumn(column.id); + } + }} >
@@ -50,20 +58,39 @@ export function ProjectColumn({
event.preventDefault()} - onDragEnter={(event) => event.preventDefault()} - onDrop={handleDrop} + onDragOver={(event) => { + event.preventDefault(); + onDragOverTask(column.id, column.tasks.length); + }} + onDrop={(event) => handleDrop(event, column.tasks.length)} > - {column.tasks.map((task) => ( - ( +
+ className={cn( + 'rounded-[18px] transition-all', + dragOverIndex === index && 'relative before:absolute before:-top-2 before:left-0 before:h-1 before:w-full before:rounded-full before:bg-brand', + )} + onDragOver={(event) => { + event.preventDefault(); + event.stopPropagation(); + onDragOverTask(column.id, index); + }} + onDrop={(event) => handleDrop(event, index)} + > + +
))} + + {dragOverIndex === column.tasks.length ? ( +
+ ) : null}
); diff --git a/src/entities/project-task/index.ts b/src/entities/project-task/index.ts deleted file mode 100644 index 8b3c9ca..0000000 --- a/src/entities/project-task/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { ProjectTask } from './model/types'; -export { ProjectTaskCard } from './ui/ProjectTaskCard'; diff --git a/src/entities/task/index.ts b/src/entities/task/index.ts new file mode 100644 index 0000000..ec1c76b --- /dev/null +++ b/src/entities/task/index.ts @@ -0,0 +1,3 @@ +export { getMockTasksByWorkspaceId } from './model/mock-tasks-by-workspace'; +export type { Task, TaskStatus } from './model/task.types'; +export { TaskCard } from './ui/TaskCard'; diff --git a/src/entities/task/model/mock-tasks-by-workspace.ts b/src/entities/task/model/mock-tasks-by-workspace.ts new file mode 100644 index 0000000..ed0eaa0 --- /dev/null +++ b/src/entities/task/model/mock-tasks-by-workspace.ts @@ -0,0 +1,80 @@ +import type { Task } from './task.types'; + +const mockTasksByWorkspaceId: Record = { + test: [ + { + id: 'task-1', + workspaceId: 'test', + title: '사용자 인터뷰 설문지 제작', + assignee: '박서준', + assigneeInitial: '박', + assigneeColor: '#00C950', + dueDate: '7/5', + status: 'todo', + }, + { + id: 'task-2', + workspaceId: 'test', + title: 'DB 스키마 설계', + assignee: '김지은', + assigneeInitial: '김', + assigneeColor: '#FE9A00', + dueDate: '7/6', + status: 'todo', + }, + { + id: 'task-3', + workspaceId: 'test', + title: '스프린트 1 회고 준비', + assignee: '이하은', + assigneeInitial: '이', + assigneeColor: '#615FFF', + dueDate: '7/10', + status: 'todo', + }, + { + id: 'task-4', + workspaceId: 'test', + title: '와이어프레임 초안 작성', + assignee: '김지은', + assigneeInitial: '김', + assigneeColor: '#00B8DB', + dueDate: '7/3', + status: 'in-progress', + }, + { + id: 'task-5', + workspaceId: 'test', + title: '랜딩 페이지 디자인', + assignee: '최민준', + assigneeInitial: '최', + assigneeColor: '#2B7FFF', + dueDate: '7/8', + status: 'in-progress', + }, + { + id: 'task-6', + workspaceId: 'test', + title: 'API 명세서 문서화', + assignee: '이하은', + assigneeInitial: '이', + assigneeColor: '#615FFF', + dueDate: '7/2', + status: 'done', + }, + { + id: 'task-7', + workspaceId: 'test', + title: '로고 시안 3종 작성', + assignee: '박서준', + assigneeInitial: '박', + assigneeColor: '#FE9A00', + dueDate: '6/30', + status: 'done', + }, + ], +}; + +export function getMockTasksByWorkspaceId(workspaceId: string): Task[] { + return mockTasksByWorkspaceId[workspaceId]?.map((task) => ({ ...task })) ?? []; +} diff --git a/src/entities/project-task/model/types.ts b/src/entities/task/model/task.types.ts similarity index 50% rename from src/entities/project-task/model/types.ts rename to src/entities/task/model/task.types.ts index 116d0cd..ba506e6 100644 --- a/src/entities/project-task/model/types.ts +++ b/src/entities/task/model/task.types.ts @@ -1,9 +1,12 @@ -export type ProjectTask = { +export type TaskStatus = 'todo' | 'in-progress' | 'done'; + +export type Task = { id: string; + workspaceId: string; title: string; assignee: string; assigneeInitial: string; assigneeColor: string; dueDate: string; - columnId: string; + status: TaskStatus; }; diff --git a/src/entities/project-task/ui/ProjectTaskCard.tsx b/src/entities/task/ui/TaskCard.tsx similarity index 65% rename from src/entities/project-task/ui/ProjectTaskCard.tsx rename to src/entities/task/ui/TaskCard.tsx index ec53e8f..a622225 100644 --- a/src/entities/project-task/ui/ProjectTaskCard.tsx +++ b/src/entities/task/ui/TaskCard.tsx @@ -1,38 +1,38 @@ import type { DragEvent } from 'react'; import { Clock3, X } from 'lucide-react'; -import type { ProjectTask } from '../model/types'; +import type { Task } from '../model/task.types'; -type ProjectTaskCardProps = { - task: ProjectTask; +type TaskCardProps = { + task: Task; onDelete: (taskId: string) => void; onDragStart: (event: DragEvent, taskId: string) => void; onDragEnd: () => void; isDragging?: boolean; }; -export function ProjectTaskCard({ +export function TaskCard({ task, onDelete, onDragStart, onDragEnd, isDragging = false, -}: ProjectTaskCardProps) { +}: TaskCardProps) { return (
onDragStart(event, task.id)} onDragEnd={onDragEnd} - className="group rounded-[22px] border border-brand/10 bg-white p-[18px] shadow-[0_1px_2px_rgba(0,0,0,0.08),0_2px_6px_rgba(0,0,0,0.04)] transition-opacity" + className="group border-brand/10 rounded-[22px] border bg-white p-4.5 shadow-[0_1px_2px_rgba(0,0,0,0.08),0_2px_6px_rgba(0,0,0,0.04)] transition-opacity" style={{ opacity: isDragging ? 0.55 : 1 }} >
-

+

{task.title}

- {task.assignee} + {task.assignee} -
+
{task.dueDate}
diff --git a/src/features/project-board/model/mockProjectBoard.ts b/src/features/project-board/model/mockProjectBoard.ts deleted file mode 100644 index c0909e2..0000000 --- a/src/features/project-board/model/mockProjectBoard.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { ProjectBoardColumn } from '@/entities/project-column'; - -export const initialProjectBoard: ProjectBoardColumn[] = [ - { - id: 'todo', - title: '대기', - tone: 'slate', - tasks: [ - { - id: 'task-1', - title: '사용자 인터뷰 설문지 제작', - assignee: '박서준', - assigneeInitial: '박', - assigneeColor: '#00C950', - dueDate: '7/5', - columnId: 'todo', - }, - { - id: 'task-2', - title: 'DB 스키마 설계', - assignee: '김지은', - assigneeInitial: '김', - assigneeColor: '#FE9A00', - dueDate: '7/6', - columnId: 'todo', - }, - { - id: 'task-3', - title: '스프린트 1 회고 준비', - assignee: '이하은', - assigneeInitial: '이', - assigneeColor: '#615FFF', - dueDate: '7/10', - columnId: 'todo', - }, - ], - }, - { - id: 'in-progress', - title: '진행 중', - tone: 'brand', - tasks: [ - { - id: 'task-4', - title: '와이어프레임 초안 작성', - assignee: '김지은', - assigneeInitial: '김', - assigneeColor: '#00B8DB', - dueDate: '7/3', - columnId: 'in-progress', - }, - { - id: 'task-5', - title: '랜딩 페이지 디자인', - assignee: '최민준', - assigneeInitial: '최', - assigneeColor: '#2B7FFF', - dueDate: '7/8', - columnId: 'in-progress', - }, - ], - }, - { - id: 'done', - title: '완료', - tone: 'green', - tasks: [ - { - id: 'task-6', - title: 'API 명세서 문서화', - assignee: '이하은', - assigneeInitial: '이', - assigneeColor: '#615FFF', - dueDate: '7/2', - columnId: 'done', - }, - { - id: 'task-7', - title: '로고 시안 3종 작성', - assignee: '박서준', - assigneeInitial: '박', - assigneeColor: '#FE9A00', - dueDate: '6/30', - columnId: 'done', - }, - ], - }, -]; diff --git a/src/features/project-board/model/project-board-columns.ts b/src/features/project-board/model/project-board-columns.ts new file mode 100644 index 0000000..ae2509b --- /dev/null +++ b/src/features/project-board/model/project-board-columns.ts @@ -0,0 +1,23 @@ +import type { ProjectBoardColumn } from '@/entities/project-column'; +import type { Task, TaskStatus } from '@/entities/task'; + +const projectBoardColumnConfig: Array<{ + id: TaskStatus; + title: string; + tone: ProjectBoardColumn['tone']; +}> = [ + { id: 'todo', title: '대기', tone: 'slate' }, + { id: 'in-progress', title: '진행 중', tone: 'brand' }, + { id: 'done', title: '완료', tone: 'green' }, +]; + +export function createProjectBoardColumns(tasks: Task[]): ProjectBoardColumn[] { + return projectBoardColumnConfig.map((column) => ({ + ...column, + tasks: tasks.filter((task) => task.status === column.id), + })); +} + +export function flattenProjectBoardColumns(columns: ProjectBoardColumn[]): Task[] { + return columns.flatMap((column) => column.tasks); +} diff --git a/src/features/project-board/ui/ProjectBoard.tsx b/src/features/project-board/ui/ProjectBoard.tsx index c5f916f..60d6f75 100644 --- a/src/features/project-board/ui/ProjectBoard.tsx +++ b/src/features/project-board/ui/ProjectBoard.tsx @@ -5,8 +5,12 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Plus, X } from 'lucide-react'; import { ProjectColumn } from '@/entities/project-column'; import type { ProjectBoardColumn } from '@/entities/project-column'; -import type { ProjectTask } from '@/entities/project-task'; -import { initialProjectBoard } from '../model/mockProjectBoard'; +import { getMockTasksByWorkspaceId } from '@/entities/task'; +import type { Task, TaskStatus } from '@/entities/task'; +import { + createProjectBoardColumns, + flattenProjectBoardColumns, +} from '../model/project-board-columns'; const DEFAULT_ASSIGNEE = { name: '김지은', @@ -14,11 +18,19 @@ const DEFAULT_ASSIGNEE = { color: '#1BB6DB', }; -export function ProjectBoard() { +type ProjectBoardProps = { + workspaceId: string; +}; + +export function ProjectBoard({ workspaceId }: ProjectBoardProps) { const [isComposerOpen, setIsComposerOpen] = useState(false); const [taskTitle, setTaskTitle] = useState(''); - const [columns, setColumns] = useState(initialProjectBoard); + const [tasks, setTasks] = useState(() => getMockTasksByWorkspaceId(workspaceId)); const [draggingTaskId, setDraggingTaskId] = useState(null); + const [dragOverState, setDragOverState] = useState<{ + columnId: TaskStatus; + index: number; + } | null>(null); const inputRef = useRef(null); useEffect(() => { @@ -30,9 +42,10 @@ export function ProjectBoard() { }, [isComposerOpen]); const taskCount = useMemo( - () => columns.reduce((count, column) => count + column.tasks.length, 0), - [columns], + () => tasks.length, + [tasks], ); + const columns = useMemo(() => createProjectBoardColumns(tasks), [tasks]); const createTask = () => { const trimmedTitle = taskTitle.trim(); @@ -44,23 +57,18 @@ export function ProjectBoard() { const today = new Date(); const formattedDate = `${today.getMonth() + 1}/${today.getDate()}`; - const newTask: ProjectTask = { + const newTask: Task = { id: `task-${taskCount + 1}-${Date.now()}`, + workspaceId, title: trimmedTitle, assignee: DEFAULT_ASSIGNEE.name, assigneeInitial: DEFAULT_ASSIGNEE.initial, assigneeColor: DEFAULT_ASSIGNEE.color, dueDate: formattedDate, - columnId: 'todo', + status: 'todo', }; - setColumns((currentColumns) => - currentColumns.map((column) => - column.id === 'todo' - ? { ...column, tasks: [...column.tasks, newTask] } - : column, - ), - ); + setTasks((currentTasks) => [...currentTasks, newTask]); setTaskTitle(''); setIsComposerOpen(false); }; @@ -75,50 +83,70 @@ export function ProjectBoard() { }; const handleDeleteTask = (taskId: string) => { - setColumns((currentColumns) => - currentColumns.map((column) => ({ - ...column, - tasks: column.tasks.filter((task) => task.id !== taskId), - })), - ); + setTasks((currentTasks) => currentTasks.filter((task) => task.id !== taskId)); }; - const handleDropTask = (targetColumnId: string, droppedTaskId?: string) => { + const handleDropTask = ( + targetColumnId: TaskStatus, + targetIndex: number, + droppedTaskId?: string, + ) => { const activeTaskId = droppedTaskId ?? draggingTaskId; if (!activeTaskId) { return; } - let movedTask: ProjectTask | null = null; + let movedTask: Task | null = null; + let sourceColumnId: TaskStatus | null = null; const nextColumns = columns.map((column) => { - const remainingTasks = column.tasks.filter((task) => { - const isDraggingTask = task.id === activeTaskId; + const taskIndex = column.tasks.findIndex((task) => task.id === activeTaskId); - if (isDraggingTask) { - movedTask = { ...task, columnId: targetColumnId }; - } + if (taskIndex === -1) { + return column; + } - return !isDraggingTask; - }); + sourceColumnId = column.id; + movedTask = { ...column.tasks[taskIndex], status: targetColumnId }; - return { ...column, tasks: remainingTasks }; + return { + ...column, + tasks: column.tasks.filter((task) => task.id !== activeTaskId), + }; }); if (!movedTask) { setDraggingTaskId(null); + setDragOverState(null); return; } - setColumns( - nextColumns.map((column) => - column.id === targetColumnId - ? { ...column, tasks: [...column.tasks, movedTask] } - : column, - ), - ); + const nextTask = movedTask; + + const reorderedColumns = nextColumns.map((column) => { + if (column.id !== targetColumnId) { + return column; + } + + const insertionIndex = + sourceColumnId === targetColumnId + ? Math.min(targetIndex, column.tasks.length) + : Math.min(targetIndex, column.tasks.length); + + return { + ...column, + tasks: [ + ...column.tasks.slice(0, insertionIndex), + nextTask, + ...column.tasks.slice(insertionIndex), + ], + }; + }); + + setTasks(flattenProjectBoardColumns(reorderedColumns)); setDraggingTaskId(null); + setDragOverState(null); }; const handleDragStartTask = (event: DragEvent, taskId: string) => { @@ -127,6 +155,11 @@ export function ProjectBoard() { setDraggingTaskId(taskId); }; + const handleDragEndTask = () => { + setDraggingTaskId(null); + setDragOverState(null); + }; + return (
@@ -178,8 +211,19 @@ export function ProjectBoard() { onDeleteTask={handleDeleteTask} onDropTask={handleDropTask} onDragStartTask={handleDragStartTask} - onDragEndTask={() => setDraggingTaskId(null)} + onDragEndTask={handleDragEndTask} draggingTaskId={draggingTaskId} + dragOverIndex={ + dragOverState?.columnId === column.id ? dragOverState.index : null + } + onDragOverTask={(columnId, index) => + setDragOverState({ columnId, index }) + } + onDragLeaveColumn={(columnId) => { + setDragOverState((current) => + current?.columnId === columnId ? null : current, + ); + }} /> ))}
diff --git a/src/views/project-management/ui/ProjectManagementPage.tsx b/src/views/project-management/ui/ProjectManagementPage.tsx index afaec59..15838f9 100644 --- a/src/views/project-management/ui/ProjectManagementPage.tsx +++ b/src/views/project-management/ui/ProjectManagementPage.tsx @@ -6,10 +6,16 @@ const jakarta = Plus_Jakarta_Sans({ weight: ['400', '500', '600', '700', '800'], }); -export default function ProjectManagementPage() { +type ProjectManagementPageProps = { + workspaceId: string; +}; + +export default function ProjectManagementPage({ + workspaceId, +}: ProjectManagementPageProps) { return ( -
- +
+
); } From 1382322b1b63d203bb7f17100a1ecf6de3a7392a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Tue, 7 Jul 2026 16:25:49 +0900 Subject: [PATCH 3/6] fix: resolve workspace project board type issues (#14) --- src/app/workspaces/[workspaceId]/page.tsx | 21 +++++++++++++++++++ .../project-column/ui/ProjectColumn.tsx | 7 ++++--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/app/workspaces/[workspaceId]/page.tsx diff --git a/src/app/workspaces/[workspaceId]/page.tsx b/src/app/workspaces/[workspaceId]/page.tsx new file mode 100644 index 0000000..c8257b7 --- /dev/null +++ b/src/app/workspaces/[workspaceId]/page.tsx @@ -0,0 +1,21 @@ +import { redirect } from 'next/navigation'; +import { getMockWorkspaceById } from '@/entities/workspace'; + +interface WorkspaceHomePageProps { + params: Promise<{ + workspaceId: string; + }>; +} + +export default async function WorkspaceHomePage({ + params, +}: WorkspaceHomePageProps) { + const { workspaceId } = await params; + const workspace = getMockWorkspaceById(workspaceId); + + if (workspace.purpose === 'store-operation') { + redirect(`/workspaces/${workspaceId}/work-schedule`); + } + + redirect(`/workspaces/${workspaceId}/project-management`); +} diff --git a/src/entities/project-column/ui/ProjectColumn.tsx b/src/entities/project-column/ui/ProjectColumn.tsx index 0dbe180..9988bd6 100644 --- a/src/entities/project-column/ui/ProjectColumn.tsx +++ b/src/entities/project-column/ui/ProjectColumn.tsx @@ -1,18 +1,19 @@ import type { DragEvent } from 'react'; import { TaskCard } from '@/entities/task'; +import type { TaskStatus } from '@/entities/task'; import { cn } from '@/shared/lib/utils'; import type { ProjectBoardColumn } from '../model/types'; type ProjectColumnProps = { column: ProjectBoardColumn; onDeleteTask: (taskId: string) => void; - onDropTask: (columnId: string, targetIndex: number, taskId?: string) => void; + onDropTask: (columnId: TaskStatus, targetIndex: number, taskId?: string) => void; onDragStartTask: (event: DragEvent, taskId: string) => void; onDragEndTask: () => void; draggingTaskId: string | null; dragOverIndex: number | null; - onDragOverTask: (columnId: string, targetIndex: number) => void; - onDragLeaveColumn: (columnId: string) => void; + onDragOverTask: (columnId: TaskStatus, targetIndex: number) => void; + onDragLeaveColumn: (columnId: TaskStatus) => void; }; const toneStyles = { From 7c97cd0d891e98b545c6ba78724c20a560a6f881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Tue, 7 Jul 2026 16:45:18 +0900 Subject: [PATCH 4/6] fix: address project board review feedback (#14) --- src/app/globals.css | 1 + src/app/workspaces/[workspaceId]/layout.tsx | 13 ++++++++++++- src/app/workspaces/[workspaceId]/page.tsx | 6 +++++- src/entities/project-column/ui/ProjectColumn.tsx | 8 ++++---- src/entities/workspace/model/mock-workspace.ts | 4 ++-- src/features/project-board/ui/ProjectBoard.tsx | 9 ++++++++- src/widgets/workspace-shell/ui/WorkspaceShell.tsx | 6 +++--- 7 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index a33a764..330df6e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -13,6 +13,7 @@ --color-brand-surface: #f7f8fc; --color-brand-soft: #eef0fb; --color-brand-secondary: #f1f3f9; + --color-brand-panel-soft: #f1f3f999; --color-brand-start: #615fff; --color-brand-end: #8e51ff; --color-brand-deep: #7f22fe; diff --git a/src/app/workspaces/[workspaceId]/layout.tsx b/src/app/workspaces/[workspaceId]/layout.tsx index a7cc4a9..bd7f5fb 100644 --- a/src/app/workspaces/[workspaceId]/layout.tsx +++ b/src/app/workspaces/[workspaceId]/layout.tsx @@ -1,4 +1,6 @@ // 워크스페이스 공통 사이드바와 헤더를 적용하는 라우트 레이아웃입니다. +import { notFound } from 'next/navigation'; +import { getMockWorkspaceById } from '@/entities/workspace'; import { WorkspaceShell } from '@/widgets/workspace-shell'; interface WorkspaceLayoutProps { @@ -10,6 +12,15 @@ interface WorkspaceLayoutProps { export default async function WorkspaceLayout({ children, params }: WorkspaceLayoutProps) { const { workspaceId } = await params; + const workspace = getMockWorkspaceById(workspaceId); - return {children}; + if (!workspace) { + notFound(); + } + + return ( + + {children} + + ); } diff --git a/src/app/workspaces/[workspaceId]/page.tsx b/src/app/workspaces/[workspaceId]/page.tsx index c8257b7..2d00c26 100644 --- a/src/app/workspaces/[workspaceId]/page.tsx +++ b/src/app/workspaces/[workspaceId]/page.tsx @@ -1,4 +1,4 @@ -import { redirect } from 'next/navigation'; +import { notFound, redirect } from 'next/navigation'; import { getMockWorkspaceById } from '@/entities/workspace'; interface WorkspaceHomePageProps { @@ -13,6 +13,10 @@ export default async function WorkspaceHomePage({ const { workspaceId } = await params; const workspace = getMockWorkspaceById(workspaceId); + if (!workspace) { + notFound(); + } + if (workspace.purpose === 'store-operation') { redirect(`/workspaces/${workspaceId}/work-schedule`); } diff --git a/src/entities/project-column/ui/ProjectColumn.tsx b/src/entities/project-column/ui/ProjectColumn.tsx index 9988bd6..279fc5f 100644 --- a/src/entities/project-column/ui/ProjectColumn.tsx +++ b/src/entities/project-column/ui/ProjectColumn.tsx @@ -17,9 +17,9 @@ type ProjectColumnProps = { }; const toneStyles = { - slate: 'bg-[#f1f3f999] text-[#9aa3b2]', - brand: 'bg-[#f1f3f999] text-brand-start', - green: 'bg-[#f1f3f999] text-[#00c950]', + slate: 'bg-brand-panel-soft text-[#9aa3b2]', + brand: 'bg-brand-panel-soft text-brand-start', + green: 'bg-brand-panel-soft text-[#00c950]', } as const; export function ProjectColumn({ @@ -42,7 +42,7 @@ export function ProjectColumn({ return (
{ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { onDragLeaveColumn(column.id); diff --git a/src/entities/workspace/model/mock-workspace.ts b/src/entities/workspace/model/mock-workspace.ts index 5ceb752..cc6fcc1 100644 --- a/src/entities/workspace/model/mock-workspace.ts +++ b/src/entities/workspace/model/mock-workspace.ts @@ -16,6 +16,6 @@ const mockWorkspacesById: Record = { export const mockWorkspace: Workspace = mockWorkspacesById['store-test']; -export function getMockWorkspaceById(workspaceId: string): Workspace { - return mockWorkspacesById[workspaceId] ?? mockWorkspace; +export function getMockWorkspaceById(workspaceId: string): Workspace | null { + return mockWorkspacesById[workspaceId] ?? null; } diff --git a/src/features/project-board/ui/ProjectBoard.tsx b/src/features/project-board/ui/ProjectBoard.tsx index 60d6f75..cff0529 100644 --- a/src/features/project-board/ui/ProjectBoard.tsx +++ b/src/features/project-board/ui/ProjectBoard.tsx @@ -99,6 +99,7 @@ export function ProjectBoard({ workspaceId }: ProjectBoardProps) { let movedTask: Task | null = null; let sourceColumnId: TaskStatus | null = null; + let sourceTaskIndex: number | null = null; const nextColumns = columns.map((column) => { const taskIndex = column.tasks.findIndex((task) => task.id === activeTaskId); @@ -108,6 +109,7 @@ export function ProjectBoard({ workspaceId }: ProjectBoardProps) { } sourceColumnId = column.id; + sourceTaskIndex = taskIndex; movedTask = { ...column.tasks[taskIndex], status: targetColumnId }; return { @@ -131,7 +133,12 @@ export function ProjectBoard({ workspaceId }: ProjectBoardProps) { const insertionIndex = sourceColumnId === targetColumnId - ? Math.min(targetIndex, column.tasks.length) + ? Math.min( + sourceTaskIndex !== null && sourceTaskIndex < targetIndex + ? targetIndex - 1 + : targetIndex, + column.tasks.length, + ) : Math.min(targetIndex, column.tasks.length); return { diff --git a/src/widgets/workspace-shell/ui/WorkspaceShell.tsx b/src/widgets/workspace-shell/ui/WorkspaceShell.tsx index f3111bb..5ae01ff 100644 --- a/src/widgets/workspace-shell/ui/WorkspaceShell.tsx +++ b/src/widgets/workspace-shell/ui/WorkspaceShell.tsx @@ -2,19 +2,19 @@ // 공통 워크스페이스 사이드바, 헤더, 페이지 콘텐츠 프레임을 조합합니다. import { useState } from 'react'; +import type { Workspace } from '@/entities/workspace'; import { WorkspaceHeader } from './WorkspaceHeader'; import { WorkspaceSidebar } from './WorkspaceSidebar'; -import { getMockWorkspaceById } from '@/entities/workspace'; import { getWorkspaceNavigation } from '@/widgets/workspace-shell/lib/get-workspace-navigation'; interface WorkspaceShellProps { + workspace: Workspace; workspaceId: string; children: React.ReactNode; } -export function WorkspaceShell({ workspaceId, children }: WorkspaceShellProps) { +export function WorkspaceShell({ workspace, workspaceId, children }: WorkspaceShellProps) { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); - const workspace = getMockWorkspaceById(workspaceId); // 나중에는 workspaceId로 Supabase 조회 const navigationItems = getWorkspaceNavigation(workspace.purpose); return ( From 7986618ae24d2068659a7cbe55882e7a2043b28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Tue, 7 Jul 2026 17:02:41 +0900 Subject: [PATCH 5/6] refactor: cache workspace mock lookup (#14) --- src/entities/workspace/model/mock-workspace.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/entities/workspace/model/mock-workspace.ts b/src/entities/workspace/model/mock-workspace.ts index cc6fcc1..b352c01 100644 --- a/src/entities/workspace/model/mock-workspace.ts +++ b/src/entities/workspace/model/mock-workspace.ts @@ -1,4 +1,5 @@ // 워크스페이스 API 연결 전 사이드바에 표시하는 현재 워크스페이스 목업입니다. +import { cache } from 'react'; import type { Workspace } from './workspace.types'; const mockWorkspacesById: Record = { @@ -16,6 +17,6 @@ const mockWorkspacesById: Record = { export const mockWorkspace: Workspace = mockWorkspacesById['store-test']; -export function getMockWorkspaceById(workspaceId: string): Workspace | null { - return mockWorkspacesById[workspaceId] ?? null; -} +export const getMockWorkspaceById = cache( + (workspaceId: string): Workspace | null => mockWorkspacesById[workspaceId] ?? null, +); From 7494f2db99cb1b74a7f842f9ae7220bc70dde9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Wed, 8 Jul 2026 11:50:18 +0900 Subject: [PATCH 6/6] refactor: remove redundant useMemo from project board (#14) --- src/features/project-board/ui/ProjectBoard.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/features/project-board/ui/ProjectBoard.tsx b/src/features/project-board/ui/ProjectBoard.tsx index cff0529..d8c2ee1 100644 --- a/src/features/project-board/ui/ProjectBoard.tsx +++ b/src/features/project-board/ui/ProjectBoard.tsx @@ -1,7 +1,7 @@ 'use client'; import type { DragEvent, KeyboardEvent } from 'react'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Plus, X } from 'lucide-react'; import { ProjectColumn } from '@/entities/project-column'; import type { ProjectBoardColumn } from '@/entities/project-column'; @@ -32,6 +32,8 @@ export function ProjectBoard({ workspaceId }: ProjectBoardProps) { index: number; } | null>(null); const inputRef = useRef(null); + const taskCount = tasks.length; + const columns = createProjectBoardColumns(tasks); useEffect(() => { if (!isComposerOpen) { @@ -41,12 +43,6 @@ export function ProjectBoard({ workspaceId }: ProjectBoardProps) { inputRef.current?.focus(); }, [isComposerOpen]); - const taskCount = useMemo( - () => tasks.length, - [tasks], - ); - const columns = useMemo(() => createProjectBoardColumns(tasks), [tasks]); - const createTask = () => { const trimmedTitle = taskTitle.trim();