Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 12 additions & 1 deletion src/app/workspaces/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// 워크스페이스 공통 사이드바와 헤더를 적용하는 라우트 레이아웃입니다.
import { notFound } from 'next/navigation';
import { getMockWorkspaceById } from '@/entities/workspace';
import { WorkspaceShell } from '@/widgets/workspace-shell';

interface WorkspaceLayoutProps {
Expand All @@ -10,6 +12,15 @@ interface WorkspaceLayoutProps {

export default async function WorkspaceLayout({ children, params }: WorkspaceLayoutProps) {
const { workspaceId } = await params;
const workspace = getMockWorkspaceById(workspaceId);

return <WorkspaceShell workspaceId={workspaceId}>{children}</WorkspaceShell>;
if (!workspace) {
notFound();
}

return (
<WorkspaceShell workspace={workspace} workspaceId={workspaceId}>
{children}
</WorkspaceShell>
);
}
25 changes: 25 additions & 0 deletions src/app/workspaces/[workspaceId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { notFound, 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);
Comment thread
JiWoongE marked this conversation as resolved.

if (!workspace) {
notFound();
}

if (workspace.purpose === 'store-operation') {
redirect(`/workspaces/${workspaceId}/work-schedule`);
}

redirect(`/workspaces/${workspaceId}/project-management`);
}
15 changes: 15 additions & 0 deletions src/app/workspaces/[workspaceId]/project-management/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ProjectManagementPage } from '@/views/project-management';

interface WorkspaceProjectManagementPageProps {
params: Promise<{
workspaceId: string;
}>;
}

export default async function WorkspaceProjectManagementPage({
params,
}: WorkspaceProjectManagementPageProps) {
const { workspaceId } = await params;

return <ProjectManagementPage workspaceId={workspaceId} />;
}
2 changes: 2 additions & 0 deletions src/entities/project-column/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { ProjectBoardColumn } from './model/types';
export { ProjectColumn } from './ui/ProjectColumn';
10 changes: 10 additions & 0 deletions src/entities/project-column/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Task, TaskStatus } from '@/entities/task';

export type ProjectColumnTone = 'slate' | 'brand' | 'green';

export type ProjectBoardColumn = {
id: TaskStatus;
title: string;
tone: ProjectColumnTone;
tasks: Task[];
};
98 changes: 98 additions & 0 deletions src/entities/project-column/ui/ProjectColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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: TaskStatus, targetIndex: number, taskId?: string) => void;
onDragStartTask: (event: DragEvent<HTMLElement>, taskId: string) => void;
onDragEndTask: () => void;
draggingTaskId: string | null;
dragOverIndex: number | null;
onDragOverTask: (columnId: TaskStatus, targetIndex: number) => void;
onDragLeaveColumn: (columnId: TaskStatus) => void;
};

const toneStyles = {
slate: 'bg-brand-panel-soft text-[#9aa3b2]',
brand: 'bg-brand-panel-soft text-brand-start',
green: 'bg-brand-panel-soft text-[#00c950]',
} as const;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export function ProjectColumn({
column,
onDeleteTask,
onDropTask,
onDragStartTask,
onDragEndTask,
draggingTaskId,
dragOverIndex,
onDragOverTask,
onDragLeaveColumn,
}: ProjectColumnProps) {
const handleDrop = (event: DragEvent<HTMLElement>, targetIndex: number) => {
event.preventDefault();
event.stopPropagation();
const taskId = event.dataTransfer.getData('text/plain') || undefined;
onDropTask(column.id, targetIndex, taskId);
};

return (
<section
className="bg-brand-panel-soft rounded-[20px] p-5"
onDragLeave={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
onDragLeaveColumn(column.id);
}
}}
>
<header className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<span className={cn('size-2.5 rounded-full', toneStyles[column.tone])} />
<h3 className="text-[17px] font-bold text-brand-ink">{column.title}</h3>
</div>
<span className="text-[15px] font-semibold text-brand-muted">{column.tasks.length}</span>
</header>

<div
className="mt-4 min-h-[220px] space-y-3.5 rounded-[16px]"
onDragOver={(event) => {
event.preventDefault();
onDragOverTask(column.id, column.tasks.length);
}}
onDrop={(event) => handleDrop(event, column.tasks.length)}
>
{column.tasks.map((task, index) => (
<div
key={task.id}
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)}
>
<TaskCard
task={task}
onDelete={onDeleteTask}
onDragStart={onDragStartTask}
onDragEnd={onDragEndTask}
isDragging={draggingTaskId === task.id}
/>
</div>
))}

{dragOverIndex === column.tasks.length ? (
<div className="h-1 w-full rounded-full bg-brand" />
) : null}
</div>
</section>
);
}
3 changes: 3 additions & 0 deletions src/entities/task/index.ts
Original file line number Diff line number Diff line change
@@ -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';
80 changes: 80 additions & 0 deletions src/entities/task/model/mock-tasks-by-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Task } from './task.types';

const mockTasksByWorkspaceId: Record<string, Task[]> = {
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 })) ?? [];
}
12 changes: 12 additions & 0 deletions src/entities/task/model/task.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type TaskStatus = 'todo' | 'in-progress' | 'done';

export type Task = {
id: string;
workspaceId: string;
title: string;
assignee: string;
assigneeInitial: string;
assigneeColor: string;
dueDate: string;
status: TaskStatus;
};
59 changes: 59 additions & 0 deletions src/entities/task/ui/TaskCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { DragEvent } from 'react';
import { Clock3, X } from 'lucide-react';
import type { Task } from '../model/task.types';

type TaskCardProps = {
task: Task;
onDelete: (taskId: string) => void;
onDragStart: (event: DragEvent<HTMLElement>, taskId: string) => void;
onDragEnd: () => void;
isDragging?: boolean;
};

export function TaskCard({
task,
onDelete,
onDragStart,
onDragEnd,
isDragging = false,
}: TaskCardProps) {
return (
<article
draggable
onDragStart={(event) => onDragStart(event, task.id)}
onDragEnd={onDragEnd}
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 }}
>
<div className="flex items-start justify-between gap-3">
<h3 className="text-brand-ink pr-2 text-[17px] leading-tight font-semibold tracking-[-0.02em]">
{task.title}
</h3>
<button
type="button"
onClick={() => onDelete(task.id)}
className="text-brand-muted hover:bg-brand-soft hover:text-brand-ink rounded-full p-1 opacity-0 transition group-hover:opacity-100"
aria-label={`${task.title} 삭제`}
>
<X className="size-4" />
</button>
Comment thread
JiWoongE marked this conversation as resolved.
</div>
<div className="mt-4 flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div
className="flex size-9 items-center justify-center rounded-full text-[15px] font-semibold text-white"
style={{ backgroundColor: task.assigneeColor }}
>
{task.assigneeInitial}
</div>
<span className="text-brand-muted text-[15px]">{task.assignee}</span>
</div>

<div className="text-brand-muted flex items-center gap-1.5 text-[15px]">
<Clock3 className="size-4" />
<span>{task.dueDate}</span>
</div>
</div>
</article>
);
}
2 changes: 1 addition & 1 deletion src/entities/workspace/index.ts
Original file line number Diff line number Diff line change
@@ -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';
22 changes: 18 additions & 4 deletions src/entities/workspace/model/mock-workspace.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
// 워크스페이스 API 연결 전 사이드바에 표시하는 현재 워크스페이스 목업입니다.
import { cache } from 'react';
import type { Workspace } from './workspace.types';

export const mockWorkspace: Workspace = {
id: 'test',
name: '카페 그레이 운영',
purpose: 'store-operation',
const mockWorkspacesById: Record<string, Workspace> = {
test: {
id: 'test',
name: '캡스톤 디자인 팀',
purpose: 'team-project',
},
'store-test': {
id: 'store-test',
name: '카페 그레이 운영',
purpose: 'store-operation',
},
};

export const mockWorkspace: Workspace = mockWorkspacesById['store-test'];

export const getMockWorkspaceById = cache(
(workspaceId: string): Workspace | null => mockWorkspacesById[workspaceId] ?? null,
);
1 change: 1 addition & 0 deletions src/features/project-board/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ProjectBoard } from './ui/ProjectBoard';
Loading