diff --git a/frontend/app/(main)/design/design-request/page.tsx b/frontend/app/(main)/design/design-request/page.tsx new file mode 100644 index 00000000..29ec9df9 --- /dev/null +++ b/frontend/app/(main)/design/design-request/page.tsx @@ -0,0 +1,839 @@ +"use client"; + +import React, { useState, useMemo, useCallback, useEffect } from "react"; +import { + Search, + RotateCcw, + Plus, + Pencil, + Trash2, + Calendar, + Upload, + PointerIcon, + Ruler, + ClipboardList, + FileText, + Loader2, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from "@/components/ui/resizable"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + getDesignRequestList, + createDesignRequest, + updateDesignRequest, + deleteDesignRequest, +} from "@/lib/api/design"; + +// ========== 타입 ========== +interface HistoryItem { + id?: string; + step: string; + history_date: string; + user_name: string; + description: string; +} + +interface DesignRequest { + id: string; + request_no: string; + source_type: string; + request_date: string; + due_date: string; + design_type: string; + priority: string; + status: string; + approval_step: string; + target_name: string; + customer: string; + req_dept: string; + requester: string; + designer: string; + order_no: string; + spec: string; + change_type: string; + drawing_no: string; + urgency: string; + reason: string; + content: string; + apply_timing: string; + review_memo: string; + project_id: string; + ecn_no: string; + created_date: string; + updated_date: string; + writer: string; + company_code: string; + history: HistoryItem[]; + impact: string[]; +} + +// ========== 스타일 맵 ========== +const STATUS_STYLES: Record = { + 신규접수: "bg-muted text-foreground", + 접수대기: "bg-muted text-foreground", + 검토중: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300", + 설계진행: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300", + 설계검토: "bg-violet-100 text-violet-800 dark:bg-violet-900/30 dark:text-violet-300", + 출도완료: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300", + 반려: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300", + 종료: "bg-muted text-muted-foreground", +}; + +const TYPE_STYLES: Record = { + 신규설계: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300", + 유사설계: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300", + 개조설계: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300", +}; + +const PRIORITY_STYLES: Record = { + 긴급: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300", + 높음: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300", + 보통: "bg-muted text-foreground", + 낮음: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300", +}; + +const STATUS_PROGRESS: Record = { + 신규접수: 0, + 접수대기: 0, + 검토중: 20, + 설계진행: 50, + 설계검토: 80, + 출도완료: 100, + 반려: 0, + 종료: 100, +}; + +function getProgressColor(p: number) { + if (p >= 100) return "bg-emerald-500"; + if (p >= 60) return "bg-amber-500"; + if (p >= 20) return "bg-blue-500"; + return "bg-muted"; +} + +function getProgressTextColor(p: number) { + if (p >= 100) return "text-emerald-500"; + if (p >= 60) return "text-amber-500"; + if (p >= 20) return "text-blue-500"; + return "text-muted-foreground"; +} + +const INITIAL_FORM = { + request_no: "", + request_date: "", + due_date: "", + design_type: "", + priority: "보통", + target_name: "", + customer: "", + req_dept: "", + requester: "", + designer: "", + order_no: "", + spec: "", + drawing_no: "", + content: "", +}; + +// ========== 메인 컴포넌트 ========== +export default function DesignRequestPage() { + const [requests, setRequests] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const [filterStatus, setFilterStatus] = useState(""); + const [filterType, setFilterType] = useState(""); + const [filterPriority, setFilterPriority] = useState(""); + const [filterKeyword, setFilterKeyword] = useState(""); + + const [modalOpen, setModalOpen] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); + const [editingId, setEditingId] = useState(null); + const [form, setForm] = useState(INITIAL_FORM); + + const today = useMemo(() => new Date(), []); + + // 데이터 조회 + const fetchRequests = useCallback(async () => { + setLoading(true); + try { + const params: Record = { source_type: "dr" }; + if (filterStatus && filterStatus !== "__all__") params.status = filterStatus; + if (filterType && filterType !== "__all__") { + // design_type은 서버에서 직접 필터링하지 않으므로 클라이언트에서 처리 + } + if (filterPriority && filterPriority !== "__all__") params.priority = filterPriority; + if (filterKeyword) params.search = filterKeyword; + + const res = await getDesignRequestList(params); + if (res.success && res.data) { + setRequests(res.data); + } else { + setRequests([]); + } + } catch { + setRequests([]); + } finally { + setLoading(false); + } + }, [filterStatus, filterPriority, filterKeyword]); + + useEffect(() => { + fetchRequests(); + }, [fetchRequests]); + + // 클라이언트 사이드 필터링 (design_type은 서버에서 지원하지 않으므로) + const filteredRequests = useMemo(() => { + let list = requests; + if (filterType && filterType !== "__all__") { + list = list.filter((item) => item.design_type === filterType); + } + return list; + }, [requests, filterType]); + + const selectedItem = useMemo(() => { + if (!selectedId) return null; + return requests.find((r) => r.id === selectedId) || null; + }, [selectedId, requests]); + + const statusCounts = useMemo(() => { + return { + 접수대기: requests.filter((r) => r.status === "접수대기" || r.status === "신규접수").length, + 설계진행: requests.filter((r) => r.status === "설계진행").length, + 출도완료: requests.filter((r) => r.status === "출도완료").length, + }; + }, [requests]); + + const handleResetFilter = useCallback(() => { + setFilterStatus(""); + setFilterType(""); + setFilterPriority(""); + setFilterKeyword(""); + }, []); + + // 채번: 기존 데이터 기반으로 다음 번호 생성 + const generateNextNo = useCallback(() => { + const year = new Date().getFullYear(); + const existing = requests.filter((r) => r.request_no?.startsWith(`DR-${year}-`)); + const maxNum = existing.reduce((max, r) => { + const parts = r.request_no?.split("-"); + const num = parts?.length >= 3 ? parseInt(parts[2]) : 0; + return num > max ? num : max; + }, 0); + return `DR-${year}-${String(maxNum + 1).padStart(4, "0")}`; + }, [requests]); + + const handleOpenRegister = useCallback(() => { + setIsEditMode(false); + setEditingId(null); + setForm({ + ...INITIAL_FORM, + request_no: generateNextNo(), + request_date: new Date().toISOString().split("T")[0], + }); + setModalOpen(true); + }, [generateNextNo]); + + const handleOpenEdit = useCallback(() => { + if (!selectedItem) return; + setIsEditMode(true); + setEditingId(selectedItem.id); + setForm({ + request_no: selectedItem.request_no || "", + request_date: selectedItem.request_date || "", + due_date: selectedItem.due_date || "", + design_type: selectedItem.design_type || "", + priority: selectedItem.priority || "보통", + target_name: selectedItem.target_name || "", + customer: selectedItem.customer || "", + req_dept: selectedItem.req_dept || "", + requester: selectedItem.requester || "", + designer: selectedItem.designer || "", + order_no: selectedItem.order_no || "", + spec: selectedItem.spec || "", + drawing_no: selectedItem.drawing_no || "", + content: selectedItem.content || "", + }); + setModalOpen(true); + }, [selectedItem]); + + const handleSave = useCallback(async () => { + if (!form.target_name.trim()) { alert("설비/제품명을 입력하세요."); return; } + if (!form.design_type) { alert("의뢰 유형을 선택하세요."); return; } + if (!form.due_date) { alert("납기를 입력하세요."); return; } + if (!form.spec.trim()) { alert("요구사양을 입력하세요."); return; } + + setSaving(true); + try { + const payload = { + request_no: form.request_no, + source_type: "dr", + request_date: form.request_date, + due_date: form.due_date, + design_type: form.design_type, + priority: form.priority, + target_name: form.target_name, + customer: form.customer, + req_dept: form.req_dept, + requester: form.requester, + designer: form.designer, + order_no: form.order_no, + spec: form.spec, + drawing_no: form.drawing_no, + content: form.content, + }; + + let res; + if (isEditMode && editingId) { + res = await updateDesignRequest(editingId, payload); + } else { + res = await createDesignRequest({ + ...payload, + status: "신규접수", + history: [{ + step: "신규접수", + history_date: form.request_date || new Date().toISOString().split("T")[0], + user_name: form.requester || "시스템", + description: `${form.req_dept || ""}에서 설계의뢰 등록`, + }], + }); + } + + if (res.success) { + setModalOpen(false); + await fetchRequests(); + if (isEditMode && editingId) { + setSelectedId(editingId); + } else if (res.data?.id) { + setSelectedId(res.data.id); + } + } else { + alert(`저장 실패: ${res.message || "알 수 없는 오류"}`); + } + } catch (err: any) { + alert(`저장 중 오류가 발생했습니다: ${err.message}`); + } finally { + setSaving(false); + } + }, [form, isEditMode, editingId, fetchRequests]); + + const handleDelete = useCallback(async () => { + if (!selectedId || !selectedItem) return; + const displayNo = selectedItem.request_no || selectedId; + if (!confirm(`${displayNo} 설계의뢰를 삭제하시겠습니까?`)) return; + + try { + const res = await deleteDesignRequest(selectedId); + if (res.success) { + setSelectedId(null); + await fetchRequests(); + } else { + alert(`삭제 실패: ${res.message || "알 수 없는 오류"}`); + } + } catch (err: any) { + alert(`삭제 중 오류가 발생했습니다: ${err.message}`); + } + }, [selectedId, selectedItem, fetchRequests]); + + const getDueDateInfo = useCallback( + (dueDate: string) => { + if (!dueDate) return { text: "-", color: "text-muted-foreground" }; + const due = new Date(dueDate); + const diff = Math.ceil((due.getTime() - today.getTime()) / 86400000); + if (diff < 0) return { text: `${Math.abs(diff)}일 초과`, color: "text-destructive" }; + if (diff === 0) return { text: "오늘", color: "text-amber-500" }; + if (diff <= 7) return { text: `${diff}일 남음`, color: "text-amber-500" }; + return { text: `${diff}일 남음`, color: "text-emerald-500" }; + }, + [today] + ); + + const getProgress = useCallback((status: string) => { + return STATUS_PROGRESS[status] ?? 0; + }, []); + + return ( +
+ {/* 검색 섹션 */} +
+ + + +
+ + setFilterKeyword(e.target.value)} + placeholder="의뢰번호 / 설비명 / 고객명 검색" + className="h-7 w-[240px] pl-7 text-xs" + /> +
+ + +
+ + {/* 메인 영역 */} + + {/* 왼쪽: 목록 */} + +
+
+ + + 설계의뢰 목록 ({filteredRequests.length}건) + + +
+ + {loading ? ( +
+ + 불러오는 중... +
+ ) : ( + + + + 의뢰번호 + 유형 + 상태 + 우선순위 + 설비/제품명 + 고객명 + 설계담당 + 납기 + 진행률 + + + + {filteredRequests.length === 0 && ( + + +
+ + 등록된 설계의뢰가 없습니다 +
+
+
+ )} + {filteredRequests.map((item) => { + const progress = getProgress(item.status); + return ( + setSelectedId(item.id)} + > + {item.request_no || "-"} + + {item.design_type ? ( + {item.design_type} + ) : "-"} + + + {item.status} + + + {item.priority} + + {item.target_name || "-"} + {item.customer || "-"} + {item.designer || "-"} + {item.due_date || "-"} + +
+
+
+
+ {progress}% +
+ + + ); + })} + +
+ )} +
+
+
+ + + + {/* 오른쪽: 상세 */} + +
+
+ + + 상세 정보 + + {selectedItem && ( +
+ + +
+ )} +
+ +
+ {/* 상태 카드 */} +
+ setFilterStatus("접수대기")} + > +
접수대기
+
{statusCounts.접수대기}
+
+ setFilterStatus("설계진행")} + > +
설계진행
+
{statusCounts.설계진행}
+
+ setFilterStatus("출도완료")} + > +
출도완료
+
{statusCounts.출도완료}
+
+
+ + {/* 상세 내용 */} + {!selectedItem ? ( +
+ + 좌측 목록에서 설계의뢰를 선택하세요 +
+ ) : ( +
+ {/* 기본 정보 */} +
+
+ 기본 정보 +
+
+ {selectedItem.request_no || "-"}} /> + {selectedItem.status}} /> + {selectedItem.design_type} : "-"} /> + {selectedItem.priority}} /> + + + + + + + {selectedItem.due_date}{" "} + + ({getDueDateInfo(selectedItem.due_date).text}) + + + ) : "-" + } + /> + + { + const progress = getProgress(selectedItem.status); + return ( +
+
+
+
+ {progress}% +
+ ); + })() + } + /> +
+
+ + {/* 요구사양 */} +
+
+ 요구사양 +
+
+
{selectedItem.spec || "-"}
+ {selectedItem.drawing_no && ( +
+ 참조 도면: + {selectedItem.drawing_no} +
+ )} + {selectedItem.content && ( +
+ 비고: {selectedItem.content} +
+ )} +
+
+ + {/* 진행 이력 */} + {selectedItem.history && selectedItem.history.length > 0 && ( +
+
+ 진행 이력 +
+
+ {selectedItem.history.map((h, idx) => { + const isLast = idx === selectedItem.history.length - 1; + const isDone = h.step === "출도완료" || h.step === "종료"; + return ( +
+
+
+ {!isLast &&
} +
+
+ {h.step} +
{h.description}
+
{h.history_date} · {h.user_name}
+
+
+ ); + })} +
+
+ )} +
+ )} +
+ +
+ + + + {/* 등록/수정 모달 */} + + + + + {isEditMode ? <>설계의뢰 수정 : <>설계의뢰 등록} + + + {isEditMode ? "설계의뢰 정보를 수정합니다." : "새 설계의뢰를 등록합니다."} + + +
+ {/* 좌측: 기본 정보 */} +
+
+ 의뢰 기본 정보 +
+
+ + +
+
+
+ + setForm((p) => ({ ...p, request_date: e.target.value }))} className="h-9 text-sm" /> +
+
+ + setForm((p) => ({ ...p, due_date: e.target.value }))} className="h-9 text-sm" /> +
+
+
+
+ + +
+
+ + +
+
+
+ + setForm((p) => ({ ...p, target_name: e.target.value }))} placeholder="설비 또는 제품명 입력" className="h-9 text-sm" /> +
+
+
+ + +
+
+ + setForm((p) => ({ ...p, requester: e.target.value }))} placeholder="의뢰자명" className="h-9 text-sm" /> +
+
+
+
+ + setForm((p) => ({ ...p, customer: e.target.value }))} placeholder="고객/거래처명" className="h-9 text-sm" /> +
+
+ + setForm((p) => ({ ...p, order_no: e.target.value }))} placeholder="관련 수주번호" className="h-9 text-sm" /> +
+
+
+ + +
+
+ + {/* 우측: 상세 내용 */} +
+
+ 요구사양 및 설명 +
+
+ +