351 lines
11 KiB
TypeScript
351 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Plus, Trash2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { DetailTypeDefinition, WorkItem } from "../types";
|
|
|
|
interface ModalDetail {
|
|
id: string;
|
|
detail_type: string;
|
|
content: string;
|
|
is_required: string;
|
|
sort_order: number;
|
|
}
|
|
|
|
interface WorkItemAddModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSave: (data: {
|
|
work_phase: string;
|
|
title: string;
|
|
is_required: string;
|
|
description?: string;
|
|
details?: Array<{
|
|
detail_type?: string;
|
|
content: string;
|
|
is_required: string;
|
|
sort_order: number;
|
|
}>;
|
|
}) => void;
|
|
phaseKey: string;
|
|
phaseLabel: string;
|
|
detailTypes: DetailTypeDefinition[];
|
|
editItem?: WorkItem | null;
|
|
}
|
|
|
|
export function WorkItemAddModal({
|
|
open,
|
|
onClose,
|
|
onSave,
|
|
phaseKey,
|
|
phaseLabel,
|
|
detailTypes,
|
|
editItem,
|
|
}: WorkItemAddModalProps) {
|
|
const [title, setTitle] = useState("");
|
|
const [isRequired, setIsRequired] = useState("Y");
|
|
const [description, setDescription] = useState("");
|
|
const [details, setDetails] = useState<ModalDetail[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (open && editItem) {
|
|
setTitle(editItem.title || "");
|
|
setIsRequired(editItem.is_required || "Y");
|
|
setDescription(editItem.description || "");
|
|
} else if (open && !editItem) {
|
|
setTitle("");
|
|
setIsRequired("Y");
|
|
setDescription("");
|
|
setDetails([]);
|
|
}
|
|
}, [open, editItem]);
|
|
|
|
const resetForm = () => {
|
|
setTitle("");
|
|
setIsRequired("Y");
|
|
setDescription("");
|
|
setDetails([]);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (!title.trim()) return;
|
|
onSave({
|
|
work_phase: phaseKey,
|
|
title: title.trim(),
|
|
is_required: isRequired,
|
|
description: description.trim() || undefined,
|
|
details: details
|
|
.filter((d) => d.content.trim())
|
|
.map((d, idx) => ({
|
|
detail_type: d.detail_type || undefined,
|
|
content: d.content.trim(),
|
|
is_required: d.is_required,
|
|
sort_order: idx + 1,
|
|
})),
|
|
});
|
|
resetForm();
|
|
onClose();
|
|
};
|
|
|
|
const addDetail = () => {
|
|
setDetails((prev) => [
|
|
...prev,
|
|
{
|
|
id: crypto.randomUUID(),
|
|
detail_type: detailTypes[0]?.value || "",
|
|
content: "",
|
|
is_required: "N",
|
|
sort_order: prev.length + 1,
|
|
},
|
|
]);
|
|
};
|
|
|
|
const removeDetail = (id: string) => {
|
|
setDetails((prev) => prev.filter((d) => d.id !== id));
|
|
};
|
|
|
|
const updateDetailField = (
|
|
id: string,
|
|
field: keyof ModalDetail,
|
|
value: string | number
|
|
) => {
|
|
setDetails((prev) =>
|
|
prev.map((d) => (d.id === id ? { ...d, [field]: value } : d))
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(v) => {
|
|
if (!v) {
|
|
resetForm();
|
|
onClose();
|
|
}
|
|
}}
|
|
>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">
|
|
작업 항목 {editItem ? "수정" : "추가"}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
{phaseLabel} 단계에 {editItem ? "항목을 수정" : "새 항목을 추가"}합니다.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* 기본 정보 */}
|
|
<div className="space-y-3 rounded-lg border p-3">
|
|
<p className="text-xs font-medium text-muted-foreground">
|
|
기본 정보
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">
|
|
항목 제목 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="예: 장비 점검, 품질 검사"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">필수 여부</Label>
|
|
<Select value={isRequired} onValueChange={setIsRequired}>
|
|
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Y" className="text-xs sm:text-sm">
|
|
필수
|
|
</SelectItem>
|
|
<SelectItem value="N" className="text-xs sm:text-sm">
|
|
선택
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">비고</Label>
|
|
<Textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="추가 설명이나 주의사항"
|
|
className="mt-1 min-h-[60px] text-xs sm:text-sm"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 상세 항목 (신규 추가 시에만) */}
|
|
{!editItem && (
|
|
<div className="space-y-2 rounded-lg border p-3">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-xs font-medium text-muted-foreground">
|
|
상세 항목
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-6 gap-1 text-[10px]"
|
|
onClick={addDetail}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
상세 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{details.length > 0 ? (
|
|
<table className="w-full text-xs">
|
|
<thead>
|
|
<tr className="border-b">
|
|
<th className="w-10 py-1.5 text-center font-medium text-muted-foreground">
|
|
순서
|
|
</th>
|
|
<th className="w-20 px-1 py-1.5 text-left font-medium text-muted-foreground">
|
|
유형
|
|
</th>
|
|
<th className="px-1 py-1.5 text-left font-medium text-muted-foreground">
|
|
내용
|
|
</th>
|
|
<th className="w-16 px-1 py-1.5 text-center font-medium text-muted-foreground">
|
|
필수
|
|
</th>
|
|
<th className="w-10 py-1.5 text-center font-medium text-muted-foreground">
|
|
관리
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{details.map((detail, idx) => (
|
|
<tr key={detail.id} className="border-b">
|
|
<td className="py-1 text-center text-muted-foreground">
|
|
{idx + 1}
|
|
</td>
|
|
<td className="px-1 py-1">
|
|
<Select
|
|
value={detail.detail_type}
|
|
onValueChange={(v) =>
|
|
updateDetailField(detail.id, "detail_type", v)
|
|
}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{detailTypes.map((t) => (
|
|
<SelectItem
|
|
key={t.value}
|
|
value={t.value}
|
|
className="text-xs"
|
|
>
|
|
{t.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</td>
|
|
<td className="px-1 py-1">
|
|
<Input
|
|
value={detail.content}
|
|
onChange={(e) =>
|
|
updateDetailField(
|
|
detail.id,
|
|
"content",
|
|
e.target.value
|
|
)
|
|
}
|
|
placeholder="상세 내용"
|
|
className="h-7 text-[10px]"
|
|
/>
|
|
</td>
|
|
<td className="px-1 py-1">
|
|
<Select
|
|
value={detail.is_required}
|
|
onValueChange={(v) =>
|
|
updateDetailField(detail.id, "is_required", v)
|
|
}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Y" className="text-xs">
|
|
필수
|
|
</SelectItem>
|
|
<SelectItem value="N" className="text-xs">
|
|
선택
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</td>
|
|
<td className="py-1 text-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 text-destructive hover:text-destructive"
|
|
onClick={() => removeDetail(detail.id)}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
) : (
|
|
<p className="py-3 text-center text-[10px] text-muted-foreground">
|
|
상세 항목이 없습니다. "상세 추가" 버튼을 클릭하여 추가하세요.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
resetForm();
|
|
onClose();
|
|
}}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={!title.trim()}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|