124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { Plus, ClipboardList } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { WorkItemCard } from "./WorkItemCard";
|
|
import { WorkItemDetailList } from "./WorkItemDetailList";
|
|
import {
|
|
WorkItem,
|
|
WorkItemDetail,
|
|
WorkPhaseDefinition,
|
|
DetailTypeDefinition,
|
|
} from "../types";
|
|
|
|
interface WorkPhaseSectionProps {
|
|
phase: WorkPhaseDefinition;
|
|
items: WorkItem[];
|
|
selectedWorkItemId: string | null;
|
|
selectedWorkItemDetails: WorkItemDetail[];
|
|
detailTypes: DetailTypeDefinition[];
|
|
readonly?: boolean;
|
|
onSelectWorkItem: (workItemId: string) => void;
|
|
onAddWorkItem: (phase: string) => void;
|
|
onEditWorkItem: (item: WorkItem) => void;
|
|
onDeleteWorkItem: (id: string) => void;
|
|
onCreateDetail: (workItemId: string, data: Partial<WorkItemDetail>) => void;
|
|
onUpdateDetail: (id: string, data: Partial<WorkItemDetail>) => void;
|
|
onDeleteDetail: (id: string) => void;
|
|
}
|
|
|
|
export function WorkPhaseSection({
|
|
phase,
|
|
items,
|
|
selectedWorkItemId,
|
|
selectedWorkItemDetails,
|
|
detailTypes,
|
|
readonly,
|
|
onSelectWorkItem,
|
|
onAddWorkItem,
|
|
onEditWorkItem,
|
|
onDeleteWorkItem,
|
|
onCreateDetail,
|
|
onUpdateDetail,
|
|
onDeleteDetail,
|
|
}: WorkPhaseSectionProps) {
|
|
const selectedItem = items.find((i) => i.id === selectedWorkItemId) || null;
|
|
const isThisSectionSelected = items.some(
|
|
(i) => i.id === selectedWorkItemId
|
|
);
|
|
|
|
return (
|
|
<div className="rounded-lg border bg-card">
|
|
{/* 섹션 헤더 */}
|
|
<div className="flex items-center justify-between border-b px-4 py-2.5">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="text-sm font-semibold">{phase.label}</h3>
|
|
<Badge
|
|
variant="secondary"
|
|
className="h-5 rounded-full px-2 text-[10px]"
|
|
>
|
|
{items.length}개 항목
|
|
</Badge>
|
|
</div>
|
|
{!readonly && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 gap-1 text-xs"
|
|
onClick={() => onAddWorkItem(phase.key)}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
작업항목 추가
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 콘텐츠 영역 */}
|
|
<div className="flex min-h-[140px]">
|
|
{/* 좌측: 작업 항목 카드 목록 */}
|
|
<div className="w-[240px] shrink-0 border-r p-2">
|
|
{items.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-6 text-center">
|
|
<ClipboardList className="mb-1 h-6 w-6 text-muted-foreground/40" />
|
|
<p className="text-[11px] text-muted-foreground">
|
|
등록된 항목이 없습니다
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1.5">
|
|
{items.map((item) => (
|
|
<WorkItemCard
|
|
key={item.id}
|
|
item={item}
|
|
isSelected={selectedWorkItemId === item.id}
|
|
readonly={readonly}
|
|
onClick={() => onSelectWorkItem(item.id)}
|
|
onEdit={() => onEditWorkItem(item)}
|
|
onDelete={() => onDeleteWorkItem(item.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 우측: 상세 리스트 */}
|
|
<div className="flex-1">
|
|
<WorkItemDetailList
|
|
workItem={isThisSectionSelected ? selectedItem : null}
|
|
details={isThisSectionSelected ? selectedWorkItemDetails : []}
|
|
detailTypes={detailTypes}
|
|
readonly={readonly}
|
|
onCreateDetail={(data) =>
|
|
selectedWorkItemId && onCreateDetail(selectedWorkItemId, data)
|
|
}
|
|
onUpdateDetail={onUpdateDetail}
|
|
onDeleteDetail={onDeleteDetail}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|