From c0e9122429c6f3b5d3e6b4f6f84f37198e977935 Mon Sep 17 00:00:00 2001 From: nelee Date: Tue, 12 May 2026 13:34:58 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat(kanban):=20=EC=B9=B8=EB=B0=98=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - console.log 제거 (#46) - KanbanHeader: 틸 배경 → bg-card, SVG → lucide-react 아이콘 (#49) - KanbanColumn: 디자인 토큰 적용, '여기에 놓기' 레이아웃 밀림 제거 (#47) - KanbanBoard: 컨테이너 디자인 토큰 적용 (#48) - TaskCard: 아코디언 제거, 항상 마감일 표시, 서브태스크 진행률 바 추가, 디자인 토큰 적용 (#45, #48) Closes #45 Closes #46 Closes #47 Closes #48 Closes #49 Co-Authored-By: Claude Sonnet 4.6 --- .../features/kanban/KanbanBoard.tsx | 32 +-- .../features/kanban/KanbanColumn.tsx | 57 +----- .../kanban/components/KanbanHeader.tsx | 175 ++++------------ src/features/task/ui/card/TaskCard.tsx | 189 ++++-------------- 4 files changed, 90 insertions(+), 363 deletions(-) diff --git a/src/components/features/kanban/KanbanBoard.tsx b/src/components/features/kanban/KanbanBoard.tsx index 845a7a8..7a72eba 100644 --- a/src/components/features/kanban/KanbanBoard.tsx +++ b/src/components/features/kanban/KanbanBoard.tsx @@ -64,15 +64,6 @@ const KanbanBoard = ({ const projectStartedAt = project?.started_at; const projectEndedAt = project?.ended_at; - console.log("KanbanBoard - Project Info:", { - project, - projectName, - projectStartedAt, - projectEndedAt, - boardId, - projectId, - }); - const [selectedTask, setSelectedTask] = useState(null); const [showTaskAddModal, setShowTaskAddModal] = useState(false); const [activeTask, setActiveTask] = useState(null); @@ -120,20 +111,6 @@ const KanbanBoard = ({ return []; } - if (filter.assignee === "me") { - console.log("내 작업 필터 활성화"); - console.log("세션 상태:", status); - console.log("현재 사용자 ID:", session?.user?.user_id); - console.log( - "전체 태스크:", - tasks.map((t) => ({ - id: t.id, - title: t.title, - assigned_user_id: t.assigned_user_id, - })) - ); - } - return tasks.filter((task) => { if (filter.priority !== "all" && task.priority !== filter.priority) { return false; @@ -150,11 +127,6 @@ const KanbanBoard = ({ const currentUserId = session?.user?.user_id; const taskUserId = task.assigned_user_id; - console.log( - `태스크 '${task.title}' 체크 - 할당된 사용자: ${taskUserId}, 현재 사용자: ${currentUserId}` - ); - console.log("타입 체크:", typeof taskUserId, typeof currentUserId); - const taskUserIdStr = taskUserId ? String(taskUserId) : null; const currentUserIdStr = currentUserId ? String(currentUserId) : null; @@ -339,7 +311,7 @@ const KanbanBoard = ({ return ( {/* 전체 컨테이너 - 캘린더와 동일한 구조 */} -
+
{/* 칸반 헤더 */} -
+
{KANBAN_COLUMNS.map((column) => ( diff --git a/src/components/features/kanban/KanbanColumn.tsx b/src/components/features/kanban/KanbanColumn.tsx index 96bc7b2..459959a 100644 --- a/src/components/features/kanban/KanbanColumn.tsx +++ b/src/components/features/kanban/KanbanColumn.tsx @@ -37,25 +37,16 @@ const KanbanColumn = ({ const overdueCount = id !== "done" ? tasks.filter((task) => isTaskOverdue(task)).length : 0; - // 드래그 오버 시 컬럼 스타일 const getColumnStyle = () => { if (isOver) { - // 드롭 대상 컬럼 하이라이트 switch (id) { - case "todo": - return "ring-2 ring-gray-400 dark:ring-gray-500 bg-gray-100 dark:bg-gray-600"; - case "inprogress": - return "ring-2 ring-blue-400 dark:ring-blue-500 bg-blue-50 dark:bg-blue-900/30"; - case "done": - return "ring-2 ring-green-400 dark:ring-green-500 bg-green-50 dark:bg-green-900/30"; - default: - return ""; + case "todo": return "ring-2 ring-border bg-muted/60"; + case "inprogress": return "ring-2 ring-main-400 dark:ring-main-500 bg-main-500/5"; + case "done": return "ring-2 ring-emerald-400 dark:ring-emerald-500 bg-emerald-50 dark:bg-emerald-900/20"; + default: return ""; } } - if (isDragging) { - // 드래그 중일 때 모든 컬럼 약간 강조 - return "border-dashed"; - } + if (isDragging) return "border-dashed"; return ""; }; @@ -63,17 +54,17 @@ const KanbanColumn = ({
{/* 컬럼 헤더 */} -
+
)} {/* 전체 개수 */} - + {tasks.length}
- {/* 드래그 오버 시 안내 메시지 */} - {isOver && ( -
- 여기에 놓기 -
- )} - {/* Task Cards */} task.id)} diff --git a/src/components/features/kanban/components/KanbanHeader.tsx b/src/components/features/kanban/components/KanbanHeader.tsx index 83507ef..72e0b4d 100644 --- a/src/components/features/kanban/components/KanbanHeader.tsx +++ b/src/components/features/kanban/components/KanbanHeader.tsx @@ -1,9 +1,9 @@ -// src/components/features/kanban/components/KanbanHeader.tsx "use client"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { showToast } from "@/lib/utils/toast"; +import { Info, SlidersHorizontal, HelpCircle, Plus } from "lucide-react"; interface KanbanHeaderProps { projectName: string; @@ -31,7 +31,6 @@ export default function KanbanHeader({ project, onProjectInfoClick, }: KanbanHeaderProps) { - // 프로젝트 기간 정보 계산 const getProjectPeriodInfo = () => { if (!project?.started_at || !project?.ended_at) return null; @@ -51,64 +50,45 @@ export default function KanbanHeader({ day: "numeric", }); - // 남은 일수 계산 const timeDiff = endDate.getTime() - today.getTime(); const remainingDays = Math.ceil(timeDiff / (1000 * 3600 * 24)); - - // 시작까지 남은 일수 const timeToStart = startDate.getTime() - today.getTime(); const daysToStart = Math.ceil(timeToStart / (1000 * 3600 * 24)); - // 프로젝트 상태 판단 - let status: "before" | "active" | "warning" | "ended"; let badgeText: string; - let badgeColor: string; + let badgeClass: string; + let status: "before" | "active" | "warning" | "ended"; if (today < startDate) { - // 시작 전 status = "before"; badgeText = `시작 D-${daysToStart}`; - badgeColor = "bg-blue-500 text-white"; + badgeClass = "bg-main-500/10 text-main-600 dark:bg-main-400/10 dark:text-main-300"; } else if (today > endDate) { - // 종료 status = "ended"; badgeText = "종료"; - badgeColor = "bg-red-500 text-white"; + badgeClass = "bg-red-500/10 text-red-600 dark:bg-red-400/10 dark:text-red-400"; } else if (remainingDays <= 3) { - // 마감 임박 (3일 이하) status = "warning"; badgeText = `D-${remainingDays} ⚠️`; - badgeColor = "bg-orange-500 text-white"; + badgeClass = "bg-amber-500/10 text-amber-600 dark:bg-amber-400/10 dark:text-amber-400"; } else { - // 진행 중 status = "active"; badgeText = `D-${remainingDays}`; - badgeColor = "bg-white/20 text-white"; + badgeClass = "bg-main-500/10 text-main-600 dark:bg-main-400/10 dark:text-main-300"; } - return { - startStr, - endStr, - remainingDays, - status, - badgeText, - badgeColor, - }; + return { startStr, endStr, remainingDays, status, badgeText, badgeClass }; }; const projectPeriod = getProjectPeriodInfo(); - // 프로젝트 종료 체크 const handleAddClick = () => { if (project?.ended_at) { const today = new Date().toISOString().split("T")[0]; - const startDate = project.started_at; - - if (today < startDate!) { + if (today < project.started_at!) { showToast("아직 프로젝트가 시작되지 않았습니다.", "warning"); return; } - if (today > project.ended_at) { showToast("종료된 프로젝트입니다.", "warning"); return; @@ -117,140 +97,57 @@ export default function KanbanHeader({ onAddClick(); }; + const iconBtn = "p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors"; + return ( -
-
- {/* 왼쪽: 프로젝트명 + 정보 버튼 + 기간 정보 */} -
+
+
+ {/* 왼쪽: 프로젝트명 + 기간 */} +
-

+

{projectName}

- {/* 프로젝트 정보 버튼 */} -
{projectPeriod && ( -
- | - - {projectPeriod.startStr} ~ {projectPeriod.endStr} - - +
+ {projectPeriod.startStr} ~ {projectPeriod.endStr} + {projectPeriod.badgeText}
)} - {/* 종료된 프로젝트 안내 문구 */} {projectPeriod?.status === "ended" && ( -
- - - - - 이 프로젝트는 종료되었습니다. 일정 추가/수정이 제한됩니다. - -
+

+ 종료된 프로젝트입니다. 일정 추가/수정이 제한됩니다. +

)}
- {/* 오른쪽: 칸반보드 표시 + 작업 개수 + 오늘 날짜 + 버튼들 */} -
- {/* 칸반보드 뷰 표시 */} -
-
- 칸반보드 -
-
- - {/* 작업 개수 및 오늘 날짜 */} -
-
{tasksCount}개 작업
-
- {format(new Date(), "M월 d일 (E)", { locale: ko })} -
+ {/* 오른쪽: 날짜 + 작업 수 + 버튼 */} +
+
+
{tasksCount}개 작업
+
{format(new Date(), "M월 d일 (E)", { locale: ko })}
- {/* 새 작업 버튼 */} - {/* 필터 버튼 */} - - {/* 도움말 버튼 */} -
diff --git a/src/features/task/ui/card/TaskCard.tsx b/src/features/task/ui/card/TaskCard.tsx index 2217679..66edd86 100644 --- a/src/features/task/ui/card/TaskCard.tsx +++ b/src/features/task/ui/card/TaskCard.tsx @@ -1,61 +1,45 @@ -import { useState, useRef, useEffect, useCallback, useMemo } from "react"; +import { useMemo, useEffect, useState } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { Task } from "@/types"; import { CSS } from "@dnd-kit/utilities"; import { Check } from "lucide-react"; import PriorityBadge from "@/features/task/ui/fields/PriorityBadge"; -import AssigneeInfo from "@/features/task/ui/fields/AssigneeInfo"; -import SubtaskList from "@/features/task/ui/fields/SubtaskList"; import DateInfo from "@/features/task/ui/fields/DateInfo"; interface TaskCardProps { task: Task; projectId: string; onClick?: () => void; - isOverlay?: boolean; // ✨ DragOverlay 모드 + isOverlay?: boolean; } const TaskCard = ({ task, - projectId, + projectId: _projectId, onClick, isOverlay = false, }: TaskCardProps) => { - const [isExpanded, setIsExpanded] = useState(false); - const [contentHeight, setContentHeight] = useState(0); const [isNew, setIsNew] = useState(true); - const contentRef = useRef(null); - // 지연 여부 체크 (마감일+시간이 지났고 완료되지 않은 경우) const isOverdue = useMemo(() => { if (!task.ended_at || task.status === "done") return false; - const now = new Date(); - - // 시간 정보가 있는 경우 (use_time && end_time) if (task.use_time && task.end_time) { const endDateStr = task.ended_at.includes("T") ? task.ended_at.split("T")[0] : task.ended_at; - const deadlineDateTime = new Date(`${endDateStr}T${task.end_time}`); - return now > deadlineDateTime; + return now > new Date(`${endDateStr}T${task.end_time}`); } - - // 날짜만 있는 경우 - 오늘 자정 기준 const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const endDateStr = task.ended_at.includes("T") ? task.ended_at.split("T")[0] : task.ended_at; const [year, month, day] = endDateStr.split("-").map(Number); - const endDate = new Date(year, month - 1, day); - - return endDate < today; + return new Date(year, month - 1, day) < today; }, [task.ended_at, task.end_time, task.use_time, task.status]); - // 완료 여부 const isCompleted = task.status === "done"; - // 새 카드 애니메이션 (마운트 후 해제) useEffect(() => { const timer = setTimeout(() => setIsNew(false), 500); return () => clearTimeout(timer); @@ -67,7 +51,6 @@ const TaskCard = ({ animateLayoutChanges: () => false, }); - // --- 스타일 처리 --- const dragStyle = useMemo( () => ({ transform: transform ? CSS.Transform.toString(transform) : undefined, @@ -78,32 +61,6 @@ const TaskCard = ({ [transform, isDragging, isOverlay] ); - const toggleExpanded = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - setIsExpanded((prev) => !prev); - }, []); - - const hasAdditionalInfo = useMemo(() => { - return Boolean( - task.description || - task.assigned_user_id || - task.started_at || - task.ended_at || - (task.subtasks && task.subtasks.length > 0) || - task.memo - ); - }, [task]); - - // --- Height 계산 (아코디언) --- - useEffect(() => { - if (!contentRef.current) return; - - const calc = () => setContentHeight(contentRef.current!.scrollHeight); - calc(); - const timer = setTimeout(calc, 80); - return () => clearTimeout(timer); - }, [task, isExpanded]); - return (
- {/* 헤더 */} -
-
- {/* 완료 체크 아이콘 */} + {/* 제목 행 */} +
+
{isCompleted && ( -
- +
+
)}

{task.title}

- - {/* 우선순위 + 펼치기 버튼 */} -
- {task.priority && } - {hasAdditionalInfo && !isOverlay && ( - - )} -
+ {task.priority && }
- {/* 접혔을 때 날짜 표시 */} - {!isExpanded && !isOverlay && (task.started_at || task.ended_at) && ( -
+ {/* 마감일 */} + {(task.started_at || task.ended_at) && !isOverlay && ( +
)} - {/* 펼쳐진 내용 */} - {!isOverlay && ( -
-
- {task.description && ( -

- {task.description} -

- )} - - {task.assigned_user_id && ( -
- -
- )} - - {(task.started_at || task.ended_at) && ( - - )} - - {Array.isArray(task.subtasks) && task.subtasks.length > 0 && ( -
- -
- )} - - {task.memo && ( -
- {task.memo} -
- )} + {/* 서브태스크 진행률 */} + {Array.isArray(task.subtasks) && task.subtasks.length > 0 && !isOverlay && ( +
+
+
s.completed).length / + task.subtasks.length) * + 100 + )}%`, + }} + />
+ + {task.subtasks.filter((s) => s.completed).length}/{task.subtasks.length} +
)}
From 75637c8b4e29ba525ed99152a96d8df3b316775a Mon Sep 17 00:00:00 2001 From: nelee Date: Tue, 12 May 2026 13:38:16 +0900 Subject: [PATCH 2/7] =?UTF-8?q?design(kanban):=20=EC=8B=9C=EA=B0=81?= =?UTF-8?q?=EC=A0=81=20=EA=B3=84=EC=B8=B5=20=EA=B0=9C=EC=84=A0=20=E2=80=94?= =?UTF-8?q?=20=EB=B0=B0=EA=B2=BD=20=EB=AA=85=EC=95=94=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헤더: bg-main-500/5 (연한 틸 tint) - 컬럼 영역: bg-muted/40 (회색 배경) - 컬럼: bg-card (흰색, 회색 위에서 부각) - 컬럼 헤더: bg-muted/40 (컬럼 본문과 구분) - 카드: shadow-sm 추가 Co-Authored-By: Claude Sonnet 4.6 --- src/components/features/kanban/KanbanBoard.tsx | 4 ++-- src/components/features/kanban/KanbanColumn.tsx | 4 ++-- src/components/features/kanban/components/KanbanHeader.tsx | 2 +- src/features/task/ui/card/TaskCard.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/features/kanban/KanbanBoard.tsx b/src/components/features/kanban/KanbanBoard.tsx index 7a72eba..aef20ae 100644 --- a/src/components/features/kanban/KanbanBoard.tsx +++ b/src/components/features/kanban/KanbanBoard.tsx @@ -423,9 +423,9 @@ function ColumnGrid({ }) { return (
-
+
-
+
{KANBAN_COLUMNS.map((column) => ( {/* 컬럼 헤더 */} -
+
+
{/* 왼쪽: 프로젝트명 + 기간 */}
diff --git a/src/features/task/ui/card/TaskCard.tsx b/src/features/task/ui/card/TaskCard.tsx index 66edd86..cfb2cff 100644 --- a/src/features/task/ui/card/TaskCard.tsx +++ b/src/features/task/ui/card/TaskCard.tsx @@ -70,7 +70,7 @@ const TaskCard = ({ onClick={onClick} className={` bg-card text-foreground - p-4 rounded-[10px] border + p-4 rounded-[10px] border shadow-sm cursor-grab active:cursor-grabbing ${ isCompleted From 5c014382b87ed97b754e637fcb72ed4864a9b779 Mon Sep 17 00:00:00 2001 From: nelee Date: Tue, 12 May 2026 14:06:42 +0900 Subject: [PATCH 3/7] =?UTF-8?q?design(kanban):=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=8A=A4=ED=8E=99=20=EB=B0=98=EC=98=81=20=E2=80=94?= =?UTF-8?q?=20=EC=BB=AC=EB=9F=AC=C2=B7=EA=B0=84=EA=B2=A9=C2=B7=EA=B7=B8?= =?UTF-8?q?=EB=A6=BC=EC=9E=90=20=EC=A0=95=EA=B5=90=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체 컨테이너 rounded-[12px] 및 shadow 수치 스펙 적용 - 컬럼 영역 배경 #F0F4F5, padding/gap 16px 통일 - 컬럼 헤더 배경 #F0F4F5, 상태 dot 6px, 상태명 14px 고정 - 드래그 하이라이트 ring 색상 스펙 정확히 반영 (main-500 / emerald-500) - TaskCard shadow 0 1px 3px rgba(0,0,0,0.08) - 상태 컬러 gray-500 / blue-500 / emerald-500 로 정비 - KanbanHeader min-h-[60px], 프로젝트명 text-lg 고정 Co-Authored-By: Claude Sonnet 4.6 --- src/components/features/kanban/KanbanBoard.tsx | 6 +++--- src/components/features/kanban/KanbanColumn.tsx | 12 ++++++------ .../features/kanban/components/KanbanHeader.tsx | 4 ++-- src/features/task/ui/card/TaskCard.tsx | 2 +- src/lib/utils/taskUtils.ts | 14 +++++++------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/features/kanban/KanbanBoard.tsx b/src/components/features/kanban/KanbanBoard.tsx index aef20ae..f1ff94e 100644 --- a/src/components/features/kanban/KanbanBoard.tsx +++ b/src/components/features/kanban/KanbanBoard.tsx @@ -311,7 +311,7 @@ const KanbanBoard = ({ return ( {/* 전체 컨테이너 - 캘린더와 동일한 구조 */} -
+
{/* 칸반 헤더 */} -
+
-
+
{KANBAN_COLUMNS.map((column) => ( {/* 컬럼 헤더 */} -
+

{title}

@@ -100,7 +100,7 @@ const KanbanColumn = ({
diff --git a/src/components/features/kanban/components/KanbanHeader.tsx b/src/components/features/kanban/components/KanbanHeader.tsx index ca23267..a1b9dc8 100644 --- a/src/components/features/kanban/components/KanbanHeader.tsx +++ b/src/components/features/kanban/components/KanbanHeader.tsx @@ -100,12 +100,12 @@ export default function KanbanHeader({ const iconBtn = "p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors"; return ( -
+
{/* 왼쪽: 프로젝트명 + 기간 */}
-

+

{projectName}

- {selectedTask && ( - setSelectedTask(null)}> + setSelectedTask(null)}> + {selectedTask && ( setSelectedTask(null)} /> - - )} + )} + {showTaskAddModal && ( - setShowTaskAddModal(false)}> + { setShowTaskAddModal(false); setAddTaskDefaultStatus(undefined); }}> setShowTaskAddModal(false)} + onCancel={() => { setShowTaskAddModal(false); setAddTaskDefaultStatus(undefined); }} /> )} @@ -414,11 +425,15 @@ function ColumnGrid({ groupedTasks, projectId, onTaskClick, + onColumnAddClick, + onTitleUpdate, isDragging, }: { groupedTasks: Record; projectId: string; onTaskClick: (task: Task) => void; + onColumnAddClick: (status: TaskStatus) => void; + onTitleUpdate: (taskId: string, updates: Partial) => void; isDragging: boolean; }) { return ( @@ -434,6 +449,8 @@ function ColumnGrid({ tasks={groupedTasks[column.id] || []} projectId={projectId} onTaskClick={onTaskClick} + onAddClick={() => onColumnAddClick(column.id as TaskStatus)} + onTitleUpdate={(taskId, title) => onTitleUpdate(taskId, { title })} isDragging={isDragging} /> ))} diff --git a/src/components/features/kanban/KanbanColumn.tsx b/src/components/features/kanban/KanbanColumn.tsx index 9b2d4ab..9513aba 100644 --- a/src/components/features/kanban/KanbanColumn.tsx +++ b/src/components/features/kanban/KanbanColumn.tsx @@ -3,6 +3,7 @@ import { SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable"; +import { Plus } from "lucide-react"; import { TaskCard } from "@/features/task"; import { Task, TaskStatus } from "@/types"; @@ -15,7 +16,9 @@ interface KanbanColumnProps { tasks: Task[]; projectId: string; onTaskClick: (task: Task) => void; - isDragging?: boolean; // 현재 드래그 중인지 + onAddClick?: () => void; + onTitleUpdate?: (taskId: string, title: string) => void; + isDragging?: boolean; } const KanbanColumn = ({ @@ -24,6 +27,8 @@ const KanbanColumn = ({ tasks, projectId, onTaskClick, + onAddClick, + onTitleUpdate, isDragging = false, }: KanbanColumnProps) => { const { setNodeRef, isOver } = useDroppable({ @@ -111,6 +116,11 @@ const KanbanColumn = ({ task={task} projectId={projectId} onClick={() => onTaskClick(task)} + onTitleUpdate={ + onTitleUpdate + ? (title) => onTitleUpdate(task.id, title) + : undefined + } /> )) : !isOver && ( @@ -118,9 +128,19 @@ const KanbanColumn = ({ icon="clipboard" title="작업이 없어요" variant="minimal" - className="py-10" + className="py-6" /> )} + + {onAddClick && ( + + )}
diff --git a/src/components/ui/SidePanel.tsx b/src/components/ui/SidePanel.tsx new file mode 100644 index 0000000..7b6d885 --- /dev/null +++ b/src/components/ui/SidePanel.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useEffect } from "react"; + +interface SidePanelProps { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; +} + +export default function SidePanel({ isOpen, onClose, children }: SidePanelProps) { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + if (isOpen) document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose]); + + useEffect(() => { + document.body.style.overflow = isOpen ? "hidden" : ""; + return () => { document.body.style.overflow = ""; }; + }, [isOpen]); + + return ( + <> + {/* Backdrop */} +
+ + {/* Panel */} +
+ {children} +
+ + ); +} diff --git a/src/features/task/ui/add/TaskAdd.tsx b/src/features/task/ui/add/TaskAdd.tsx index bc3748e..66de70a 100644 --- a/src/features/task/ui/add/TaskAdd.tsx +++ b/src/features/task/ui/add/TaskAdd.tsx @@ -24,6 +24,7 @@ interface TaskAddProps { projectEndedAt?: string; onSuccess?: (task: Omit) => void; onCancel: () => void; + initialStatus?: TaskStatus; initialStartDate?: string; initialEndDate?: string; initialStartTime?: string; @@ -80,6 +81,7 @@ export default function TaskAdd({ projectEndedAt, onSuccess, onCancel, + initialStatus, initialStartDate, initialEndDate, initialStartTime, @@ -88,6 +90,7 @@ export default function TaskAdd({ }: TaskAddProps) { const [formData, setFormData] = useState({ ...INITIAL_FORM_DATA, + status: initialStatus || "todo", started_at: initialStartDate || "", ended_at: initialEndDate || "", start_time: initialStartTime || "", diff --git a/src/features/task/ui/card/TaskCard.tsx b/src/features/task/ui/card/TaskCard.tsx index 7bf2e48..58d4e75 100644 --- a/src/features/task/ui/card/TaskCard.tsx +++ b/src/features/task/ui/card/TaskCard.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect, useState } from "react"; +import { useMemo, useEffect, useState, useRef, useCallback } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { Task } from "@/types"; import { CSS } from "@dnd-kit/utilities"; @@ -11,6 +11,7 @@ interface TaskCardProps { projectId: string; onClick?: () => void; isOverlay?: boolean; + onTitleUpdate?: (title: string) => void; } const TaskCard = ({ @@ -18,8 +19,12 @@ const TaskCard = ({ projectId: _projectId, onClick, isOverlay = false, + onTitleUpdate, }: TaskCardProps) => { const [isNew, setIsNew] = useState(true); + const [isEditingTitle, setIsEditingTitle] = useState(false); + const [editedTitle, setEditedTitle] = useState(task.title); + const titleInputRef = useRef(null); const isOverdue = useMemo(() => { if (!task.ended_at || task.status === "done") return false; @@ -45,10 +50,50 @@ const TaskCard = ({ return () => clearTimeout(timer); }, []); + useEffect(() => { + if (isEditingTitle && titleInputRef.current) { + titleInputRef.current.focus(); + titleInputRef.current.select(); + } + }, [isEditingTitle]); + + const handleTitleSave = useCallback(() => { + const trimmed = editedTitle.trim(); + if (trimmed && trimmed !== task.title) { + onTitleUpdate?.(trimmed); + } + setIsEditingTitle(false); + }, [editedTitle, task.title, onTitleUpdate]); + + const handleTitleDoubleClick = useCallback( + (e: React.MouseEvent) => { + if (isOverlay || !onTitleUpdate) return; + e.stopPropagation(); + setEditedTitle(task.title); + setIsEditingTitle(true); + }, + [isOverlay, onTitleUpdate, task.title] + ); + + const handleTitleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleTitleSave(); + } + if (e.key === "Escape") { + setIsEditingTitle(false); + setEditedTitle(task.title); + } + }, + [handleTitleSave, task.title] + ); + const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({ id: task.id, animateLayoutChanges: () => false, + disabled: isEditingTitle, }); const dragStyle = useMemo( @@ -88,20 +133,36 @@ const TaskCard = ({ {/* 제목 행 */}
- {isCompleted && ( + {isCompleted && !isEditingTitle && (
)} -

- {task.title} -

+ {isEditingTitle ? ( +