You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
// frontend/components/tasks/KanbanView.tsx
|
|
"use client";
|
|
import { useState } from "react";
|
|
import { Icon } from "@/components/Icon";
|
|
import { cx } from "@/lib/cx";
|
|
import { COLUMNS } from "@/lib/tasks/tree";
|
|
import type { Person, Project, Status, Task } from "@/lib/types";
|
|
import { KanbanCard } from "./KanbanCard";
|
|
|
|
export function KanbanView({
|
|
tasks,
|
|
people,
|
|
projects,
|
|
onOpen,
|
|
onMove,
|
|
onAdd,
|
|
}: {
|
|
tasks: Task[];
|
|
people: Record<string, Person>;
|
|
projects: Record<string, Project>;
|
|
onOpen: (id: string) => void;
|
|
onMove: (id: string, status: Status) => void;
|
|
onAdd: (status: Status, title: string) => void;
|
|
}) {
|
|
const [dragId, setDragId] = useState<string | null>(null);
|
|
const [overCol, setOverCol] = useState<string | null>(null);
|
|
const [addCol, setAddCol] = useState<string | null>(null);
|
|
const [text, setText] = useState("");
|
|
const submit = (status: Status) => {
|
|
if (text.trim()) onAdd(status, text.trim());
|
|
setText("");
|
|
setAddCol(null);
|
|
};
|
|
return (
|
|
<div className="kboard">
|
|
{COLUMNS.map((col) => {
|
|
const items = tasks.filter((t) => t.status === col.id);
|
|
return (
|
|
<div
|
|
key={col.id}
|
|
className={cx("kcol", overCol === col.id && "over")}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
if (overCol !== col.id) setOverCol(col.id);
|
|
}}
|
|
onDragLeave={(e) => {
|
|
if (e.currentTarget === e.target) setOverCol(null);
|
|
}}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
if (dragId) onMove(dragId, col.id as Status);
|
|
setDragId(null);
|
|
setOverCol(null);
|
|
}}
|
|
>
|
|
<div className="kcol-head">
|
|
<span className="kcol-dot" style={{ background: col.accent }} />
|
|
<span className="kcol-label">{col.label}</span>
|
|
<span className="kcol-count">{items.length}</span>
|
|
<button
|
|
className="kcol-add"
|
|
onClick={() => {
|
|
setAddCol(col.id);
|
|
setText("");
|
|
}}
|
|
aria-label="작업 추가"
|
|
>
|
|
<Icon name="plus" />
|
|
</button>
|
|
</div>
|
|
<div className="kcol-body">
|
|
{addCol === col.id && (
|
|
<div className="quickadd">
|
|
<input
|
|
autoFocus
|
|
value={text}
|
|
placeholder="작업 입력 후 Enter…"
|
|
onChange={(e) => setText(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") submit(col.id as Status);
|
|
if (e.key === "Escape") setAddCol(null);
|
|
}}
|
|
onBlur={() => submit(col.id as Status)}
|
|
/>
|
|
</div>
|
|
)}
|
|
{items.map((t) => (
|
|
<KanbanCard
|
|
key={t.id}
|
|
task={t}
|
|
people={people}
|
|
projects={projects}
|
|
onOpen={onOpen}
|
|
dragging={dragId === t.id}
|
|
onDragStart={(e) => {
|
|
setDragId(t.id);
|
|
e.dataTransfer.effectAllowed = "move";
|
|
}}
|
|
onDragEnd={() => {
|
|
setDragId(null);
|
|
setOverCol(null);
|
|
}}
|
|
/>
|
|
))}
|
|
{items.length === 0 && addCol !== col.id && (
|
|
<div className="kcol-empty">여기로 작업을 옮겨보세요</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|