Files
wace_rps/frontend/app/(main)/COMPANY_7/design/change-management/page.tsx
T
kjs f32861df8b feat: add new design management pages and session files
- Introduced multiple new pages for design management, including change management, design requests, my work, project management, and task management.
- Added session files to track design sessions with relevant details such as session ID, end time, and reason.
- Enhanced the overall structure and organization of the design management features, improving user experience and functionality.

This commit expands the design management capabilities within the application, allowing for better tracking and handling of design-related tasks.
2026-03-27 14:48:15 +09:00

1656 lines
70 KiB
TypeScript

"use client";
import React, { useState, useMemo, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import {
Search,
RotateCcw,
Plus,
Save,
ClipboardList,
Inbox,
Pencil,
FileText,
XCircle,
ArrowRight,
Paperclip,
Upload,
Loader2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import {
getDesignRequestList,
createDesignRequest,
updateDesignRequest,
addRequestHistory,
getEcnList,
createEcn,
updateEcn,
} from "@/lib/api/design";
// --- Types ---
type ChangeType = "설계오류" | "원가절감" | "고객요청" | "공정개선" | "법규대응";
type EcrStatus = "요청접수" | "영향도분석" | "ECN발행" | "기각";
type EcnStatus = "ECN발행" | "도면변경" | "통보완료" | "적용완료";
type TabType = "ecr" | "ecn";
interface EcrHistory {
status: string;
date: string;
user: string;
desc: string;
}
interface EcrItem {
id: string;
_id?: string;
date: string;
changeType: ChangeType;
urgency: "보통" | "긴급";
status: EcrStatus;
target: string;
drawingNo: string;
reqDept: string;
requester: string;
reason: string;
content: string;
impact: string[];
applyTiming: string;
ecnNo: string;
history: EcrHistory[];
}
interface EcnItem {
id: string;
_id?: string;
ecrNo: string;
ecrId?: string;
date: string;
applyDate: string;
status: EcnStatus;
target: string;
drawingBefore: string;
drawingAfter: string;
designer: string;
before: string;
after: string;
reason: string;
notifyDepts: string[];
remark: string;
history: EcrHistory[];
}
// --- Style Helpers ---
const getChangeTypeStyle = (type: ChangeType) => {
switch (type) {
case "설계오류":
return "bg-rose-100 text-rose-800 border-rose-200";
case "원가절감":
return "bg-emerald-100 text-emerald-800 border-emerald-200";
case "고객요청":
return "bg-blue-100 text-blue-800 border-blue-200";
case "공정개선":
return "bg-amber-100 text-amber-800 border-amber-200";
case "법규대응":
return "bg-purple-100 text-purple-800 border-purple-200";
default:
return "bg-gray-100 text-gray-800 border-gray-200";
}
};
const getEcrStatusStyle = (status: EcrStatus) => {
switch (status) {
case "요청접수":
return "bg-blue-100 text-blue-800 border-blue-200";
case "영향도분석":
return "bg-amber-100 text-amber-800 border-amber-200";
case "ECN발행":
return "bg-emerald-100 text-emerald-800 border-emerald-200";
case "기각":
return "bg-slate-100 text-slate-800 border-slate-200";
default:
return "bg-gray-100 text-gray-800 border-gray-200";
}
};
const getEcnStatusStyle = (status: EcnStatus) => {
switch (status) {
case "ECN발행":
return "bg-blue-100 text-blue-800 border-blue-200";
case "도면변경":
return "bg-purple-100 text-purple-800 border-purple-200";
case "통보완료":
return "bg-teal-100 text-teal-800 border-teal-200";
case "적용완료":
return "bg-emerald-100 text-emerald-800 border-emerald-200";
default:
return "bg-gray-100 text-gray-800 border-gray-200";
}
};
const getImpactBadgeStyle = (impact: string) => {
switch (impact) {
case "BOM":
return "bg-blue-100 text-blue-800 border-blue-200";
case "공정":
return "bg-amber-100 text-amber-800 border-amber-200";
case "금형":
return "bg-rose-100 text-rose-800 border-rose-200";
case "검사기준":
return "bg-purple-100 text-purple-800 border-purple-200";
case "구매":
case "원가":
return "bg-emerald-100 text-emerald-800 border-emerald-200";
default:
return "bg-gray-100 text-gray-800 border-gray-200";
}
};
// --- Constants ---
const CHANGE_TYPES: ChangeType[] = ["설계오류", "원가절감", "고객요청", "공정개선", "법규대응"];
const ECR_STATUSES: EcrStatus[] = ["요청접수", "영향도분석", "ECN발행", "기각"];
const ECN_STATUSES: EcnStatus[] = ["ECN발행", "도면변경", "통보완료", "적용완료"];
const DEPARTMENTS = ["품질팀", "생산팀", "영업팀", "구매팀", "설계팀"];
const DESIGNERS = ["이설계", "박도면", "최기구", "김전장"];
const IMPACT_OPTIONS = [
{ key: "BOM", label: "BOM 변경" },
{ key: "공정", label: "공정 변경" },
{ key: "금형", label: "금형 변경" },
{ key: "검사기준", label: "검사기준 변경" },
{ key: "구매", label: "구매 변경" },
{ key: "원가", label: "원가 영향" },
];
const NOTIFY_DEPTS = [
{ key: "생산팀", label: "생산팀" },
{ key: "품질팀", label: "품질팀" },
{ key: "구매팀", label: "구매팀" },
{ key: "영업팀", label: "영업팀" },
{ key: "물류팀", label: "물류팀" },
{ key: "금형팀", label: "금형팀" },
];
// --- API Response Mapping ---
function mapEcrFromApi(raw: any): EcrItem {
const history = (raw.history || []).map((h: any) => ({
status: h.step || h.status || "",
date: h.history_date || "",
user: h.user_name || "",
desc: h.description || "",
}));
return {
id: raw.request_no || raw.id || "",
_id: raw.id,
date: raw.request_date || "",
changeType: (raw.change_type as ChangeType) || "설계오류",
urgency: (raw.urgency as "보통" | "긴급") || "보통",
status: (raw.status as EcrStatus) || "요청접수",
target: raw.target_name || "",
drawingNo: raw.drawing_no || "",
reqDept: raw.req_dept || "",
requester: raw.requester || "",
reason: raw.reason || "",
content: raw.content || "",
impact: Array.isArray(raw.impact) ? raw.impact : [],
applyTiming: raw.apply_timing || "",
ecnNo: raw.ecn_no || "",
history,
};
}
function mapEcnFromApi(raw: any, ecrData: EcrItem[]): EcnItem {
const history = (raw.history || []).map((h: any) => ({
status: h.status || "",
date: h.history_date || "",
user: h.user_name || "",
desc: h.description || "",
}));
const ecrNo = raw.ecr_id
? ecrData.find((e) => e._id === raw.ecr_id)?.id ?? raw.ecr_id
: "";
return {
id: raw.ecn_no || raw.id || "",
_id: raw.id,
ecrNo,
ecrId: raw.ecr_id,
date: raw.ecn_date || "",
applyDate: raw.apply_date || "",
status: (raw.status as EcnStatus) || "ECN발행",
target: raw.target || "",
drawingBefore: raw.drawing_before || "",
drawingAfter: raw.drawing_after || "",
designer: raw.designer || "",
before: raw.before_content || "",
after: raw.after_content || "",
reason: raw.reason || "",
notifyDepts: Array.isArray(raw.notify_depts) ? raw.notify_depts : [],
remark: raw.remark || "",
history,
};
}
// --- Timeline Component ---
function Timeline({ history }: { history: EcrHistory[] }) {
return (
<div className="space-y-0">
{history.map((h, idx) => {
const isLast = idx === history.length - 1;
const isRejected = h.status === "기각";
const isCompleted = h.status === "적용완료";
return (
<div key={idx} className="flex gap-3 relative">
<div className="flex flex-col items-center">
<div
className={cn(
"w-3 h-3 rounded-full border-2 mt-1.5 shrink-0",
isLast && isRejected
? "bg-rose-500 border-rose-300"
: isLast && isCompleted
? "bg-emerald-500 border-emerald-300"
: isLast
? "bg-primary border-primary/50 ring-4 ring-primary/10"
: "bg-emerald-500 border-emerald-300"
)}
/>
{!isLast && (
<div className="w-px flex-1 bg-border min-h-[24px]" />
)}
</div>
<div className="pb-4 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span
className={cn(
"px-2 py-0.5 rounded-full text-[10px] font-medium border",
h.status === "기각"
? "bg-slate-100 text-slate-800 border-slate-200"
: h.status === "적용완료"
? "bg-emerald-100 text-emerald-800 border-emerald-200"
: h.status === "ECN발행"
? "bg-emerald-100 text-emerald-800 border-emerald-200"
: h.status === "영향도분석"
? "bg-amber-100 text-amber-800 border-amber-200"
: h.status === "도면변경"
? "bg-purple-100 text-purple-800 border-purple-200"
: h.status === "통보완료"
? "bg-teal-100 text-teal-800 border-teal-200"
: "bg-blue-100 text-blue-800 border-blue-200"
)}
>
{h.status}
</span>
</div>
<p className="text-xs text-foreground">{h.desc}</p>
<p className="text-[11px] text-muted-foreground mt-0.5">
{h.date} · {h.user}
</p>
</div>
</div>
);
})}
</div>
);
}
// --- Main Component ---
export default function DesignChangeManagementPage() {
const [currentTab, setCurrentTab] = useState<TabType>("ecr");
const [ecrData, setEcrData] = useState<EcrItem[]>([]);
const [ecnData, setEcnData] = useState<EcnItem[]>([]);
const [loading, setLoading] = useState(true);
const [selectedId, setSelectedId] = useState<string | null>(null);
// 검색 상태
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
const [searchStatus, setSearchStatus] = useState<string>("all");
const [searchChangeType, setSearchChangeType] = useState<string>("all");
const [searchKeyword, setSearchKeyword] = useState("");
// ECR 모달
const [isEcrModalOpen, setIsEcrModalOpen] = useState(false);
const [isEcrEditMode, setIsEcrEditMode] = useState(false);
const [ecrForm, setEcrForm] = useState<Partial<EcrItem>>({});
const [ecrImpactChecks, setEcrImpactChecks] = useState<Record<string, boolean>>({});
// ECN 모달
const [isEcnModalOpen, setIsEcnModalOpen] = useState(false);
const [ecnForm, setEcnForm] = useState<Partial<EcnItem>>({});
const [ecnNotifyChecks, setEcnNotifyChecks] = useState<Record<string, boolean>>({});
// 기각 모달
const [isRejectModalOpen, setIsRejectModalOpen] = useState(false);
const [rejectReason, setRejectReason] = useState("");
const [rejectTargetId, setRejectTargetId] = useState("");
useEffect(() => {
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(today.getMonth() - 3);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
}, []);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const [ecrRes, ecnRes] = await Promise.all([
getDesignRequestList({ source_type: "ecr" }),
getEcnList(),
]);
if (ecrRes.success && ecrRes.data) {
setEcrData((ecrRes.data as any[]).map(mapEcrFromApi));
}
if (ecnRes.success && ecnRes.data) {
const ecrList = ecrRes.success && ecrRes.data ? (ecrRes.data as any[]).map(mapEcrFromApi) : [];
setEcnData((ecnRes.data as any[]).map((r) => mapEcnFromApi(r, ecrList)));
}
} catch {
toast.error("데이터를 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
// --- Filtered Data ---
const filteredEcr = useMemo(() => {
return ecrData
.filter((item) => {
if (searchDateFrom && item.date < searchDateFrom) return false;
if (searchDateTo && item.date > searchDateTo) return false;
if (searchStatus !== "all" && item.status !== searchStatus) return false;
if (searchChangeType !== "all" && item.changeType !== searchChangeType) return false;
if (searchKeyword) {
const kw = searchKeyword.toLowerCase();
const str = [item.id, item.target, item.requester, item.drawingNo].join(" ").toLowerCase();
if (!str.includes(kw)) return false;
}
return true;
})
.sort((a, b) => b.date.localeCompare(a.date));
}, [ecrData, searchDateFrom, searchDateTo, searchStatus, searchChangeType, searchKeyword]);
const filteredEcn = useMemo(() => {
return ecnData
.filter((item) => {
if (searchDateFrom && item.date < searchDateFrom) return false;
if (searchDateTo && item.date > searchDateTo) return false;
if (searchStatus !== "all" && item.status !== searchStatus) return false;
if (searchKeyword) {
const kw = searchKeyword.toLowerCase();
const str = [item.id, item.target, item.designer, item.ecrNo].join(" ").toLowerCase();
if (!str.includes(kw)) return false;
}
return true;
})
.sort((a, b) => b.date.localeCompare(a.date));
}, [ecnData, searchDateFrom, searchDateTo, searchStatus, searchKeyword]);
// --- Status Counts ---
const ecrStatusCounts = useMemo(() => {
const counts: Record<string, number> = {};
ECR_STATUSES.forEach((s) => (counts[s] = ecrData.filter((r) => r.status === s).length));
return counts;
}, [ecrData]);
const ecnStatusCounts = useMemo(() => {
const counts: Record<string, number> = {};
ECN_STATUSES.forEach((s) => (counts[s] = ecnData.filter((r) => r.status === s).length));
return counts;
}, [ecnData]);
// --- Selected Items ---
const selectedEcr = useMemo(
() => (currentTab === "ecr" ? ecrData.find((r) => r.id === selectedId) : null),
[ecrData, selectedId, currentTab]
);
const selectedEcn = useMemo(
() => (currentTab === "ecn" ? ecnData.find((r) => r.id === selectedId) : null),
[ecnData, selectedId, currentTab]
);
// --- Tab Switch ---
const handleTabSwitch = (tab: TabType) => {
setCurrentTab(tab);
setSelectedId(null);
setSearchStatus("all");
};
// --- Search ---
const handleResetSearch = () => {
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(today.getMonth() - 3);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
setSearchStatus("all");
setSearchChangeType("all");
setSearchKeyword("");
};
const handleFilterByStatus = (status: string) => {
setSearchStatus(status);
};
// --- ECR/ECN Navigation ---
const navigateToLink = (targetId: string) => {
if (targetId.startsWith("ECN")) {
setCurrentTab("ecn");
setSelectedId(targetId);
setSearchStatus("all");
} else if (targetId.startsWith("ECR")) {
setCurrentTab("ecr");
setSelectedId(targetId);
setSearchStatus("all");
}
};
// --- ECR Number Generator ---
const generateEcrNo = useCallback(() => {
const year = new Date().getFullYear();
const prefix = `ECR-${year}-`;
const existing = ecrData.filter((r) => r.id.startsWith(prefix));
const maxNum = existing.reduce((max, r) => {
const num = parseInt(r.id.split("-")[2]);
return num > max ? num : max;
}, 0);
return `${prefix}${String(maxNum + 1).padStart(4, "0")}`;
}, [ecrData]);
const generateEcnNo = useCallback(() => {
const year = new Date().getFullYear();
const prefix = `ECN-${year}-`;
const existing = ecnData.filter((r) => r.id.startsWith(prefix));
const maxNum = existing.reduce((max, r) => {
const num = parseInt(r.id.split("-")[2]);
return num > max ? num : max;
}, 0);
return `${prefix}${String(maxNum + 1).padStart(4, "0")}`;
}, [ecnData]);
// --- ECR Modal ---
const openEcrRegisterModal = () => {
setIsEcrEditMode(false);
setEcrForm({
id: generateEcrNo(),
date: new Date().toISOString().split("T")[0],
changeType: undefined,
urgency: "보통",
target: "",
drawingNo: "",
reqDept: "",
requester: "",
reason: "",
content: "",
applyTiming: "즉시",
});
setEcrImpactChecks({});
setIsEcrModalOpen(true);
};
const openEcrEditModal = (id: string) => {
const item = ecrData.find((r) => r.id === id);
if (!item) return;
setIsEcrEditMode(true);
setEcrForm({ ...item });
const checks: Record<string, boolean> = {};
IMPACT_OPTIONS.forEach((opt) => {
checks[opt.key] = item.impact.includes(opt.key);
});
setEcrImpactChecks(checks);
setIsEcrModalOpen(true);
};
const handleSaveEcr = async () => {
if (!ecrForm.changeType) {
toast.error("변경 유형을 선택하세요.");
return;
}
if (!ecrForm.target?.trim()) {
toast.error("대상 품목/설비를 입력하세요.");
return;
}
if (!ecrForm.reason?.trim()) {
toast.error("변경 사유를 입력하세요.");
return;
}
if (!ecrForm.content?.trim()) {
toast.error("변경 요구 내용을 입력하세요.");
return;
}
const impact = IMPACT_OPTIONS.filter((opt) => ecrImpactChecks[opt.key]).map((opt) => opt.key);
const reqDate = ecrForm.date || new Date().toISOString().split("T")[0];
const historyEntry = {
step: "요청접수",
history_date: reqDate,
user_name: ecrForm.requester || "시스템",
description: `${ecrForm.reqDept || ""}에서 ECR 등록`,
};
if (isEcrEditMode && ecrForm._id) {
const res = await updateDesignRequest(ecrForm._id, {
request_no: ecrForm.id,
request_date: reqDate,
change_type: ecrForm.changeType,
urgency: ecrForm.urgency || "보통",
target_name: ecrForm.target,
drawing_no: ecrForm.drawingNo || "",
req_dept: ecrForm.reqDept || "",
requester: ecrForm.requester || "",
reason: ecrForm.reason,
content: ecrForm.content,
impact,
apply_timing: ecrForm.applyTiming || "즉시",
});
if (res.success) {
toast.success("ECR이 수정되었습니다.");
setIsEcrModalOpen(false);
fetchData();
} else {
toast.error(res.message || "ECR 수정에 실패했습니다.");
}
} else {
const res = await createDesignRequest({
request_no: ecrForm.id || generateEcrNo(),
source_type: "ecr",
request_date: reqDate,
change_type: ecrForm.changeType,
urgency: ecrForm.urgency || "보통",
status: "요청접수",
target_name: ecrForm.target,
drawing_no: ecrForm.drawingNo || "",
req_dept: ecrForm.reqDept || "",
requester: ecrForm.requester || "",
reason: ecrForm.reason,
content: ecrForm.content,
impact,
apply_timing: ecrForm.applyTiming || "즉시",
history: [historyEntry],
});
if (res.success) {
toast.success("ECR이 등록되었습니다.");
setIsEcrModalOpen(false);
fetchData();
} else {
toast.error(res.message || "ECR 등록에 실패했습니다.");
}
}
};
// --- ECN Modal ---
const openEcnIssueModal = (ecrId: string) => {
const ecr = ecrData.find((r) => r.id === ecrId);
if (!ecr) return;
setEcnForm({
id: generateEcnNo(),
ecrNo: ecrId,
ecrId: ecr._id,
date: new Date().toISOString().split("T")[0],
target: ecr.target,
reason: ecr.reason,
drawingBefore: ecr.drawingNo,
drawingAfter: "",
designer: "",
before: "",
after: "",
applyDate: "",
remark: "",
});
setEcnNotifyChecks({});
setIsEcnModalOpen(true);
};
const handleSaveEcn = async () => {
if (!ecnForm.after?.trim()) {
toast.error("변경 후(TO-BE) 내용을 입력하세요.");
return;
}
if (!ecnForm.applyDate) {
toast.error("적용일자를 입력하세요.");
return;
}
if (!ecnForm.ecrId) {
toast.error("관련 ECR 정보가 없습니다.");
return;
}
const notifyDepts = NOTIFY_DEPTS.filter((d) => ecnNotifyChecks[d.key]).map((d) => d.key);
const ecnDate = ecnForm.date || new Date().toISOString().split("T")[0];
const historyEntry = {
status: "ECN발행",
history_date: ecnDate,
user_name: ecnForm.designer || "시스템",
description: "ECN 발행",
};
const ecnNo = ecnForm.id || generateEcnNo();
const res = await createEcn({
ecn_no: ecnNo,
ecr_id: ecnForm.ecrId,
ecn_date: ecnDate,
apply_date: ecnForm.applyDate,
status: "ECN발행",
target: ecnForm.target || "",
drawing_before: ecnForm.drawingBefore || "",
drawing_after: ecnForm.drawingAfter || "(미정)",
designer: ecnForm.designer || "",
before_content: ecnForm.before || "",
after_content: ecnForm.after || "",
reason: ecnForm.reason || "",
remark: ecnForm.remark || "",
notify_depts: notifyDepts,
history: [historyEntry],
});
if (res.success) {
await updateDesignRequest(ecnForm.ecrId!, {
status: "ECN발행",
ecn_no: ecnNo,
});
await addRequestHistory(ecnForm.ecrId!, {
step: "ECN발행",
history_date: ecnDate,
user_name: ecnForm.designer || "시스템",
description: `${ecnNo} 발행`,
});
toast.success("ECN이 발행되었습니다.");
setIsEcnModalOpen(false);
fetchData();
} else {
toast.error(res.message || "ECN 발행에 실패했습니다.");
}
};
// --- ECR Reject ---
const openRejectModal = (id: string) => {
setRejectTargetId(id);
setRejectReason("");
setIsRejectModalOpen(true);
};
const handleRejectSubmit = async () => {
if (!rejectReason.trim()) {
toast.error("기각 사유를 입력하세요.");
return;
}
const ecr = ecrData.find((r) => r.id === rejectTargetId);
if (!ecr?._id) {
toast.error("ECR 정보를 찾을 수 없습니다.");
return;
}
const updateRes = await updateDesignRequest(ecr._id, { status: "기각", review_memo: rejectReason });
if (!updateRes.success) {
toast.error(updateRes.message || "ECR 기각에 실패했습니다.");
return;
}
await addRequestHistory(ecr._id, {
step: "기각",
history_date: new Date().toISOString().split("T")[0],
user_name: "설계팀",
description: rejectReason,
});
toast.success("ECR이 기각되었습니다.");
setIsRejectModalOpen(false);
fetchData();
};
// --- Stat Cards ---
const ecrStatCards = [
{ label: "요청접수", value: ecrStatusCounts["요청접수"] || 0, gradient: "from-indigo-500 to-blue-600", textColor: "text-white" },
{ label: "영향도분석", value: ecrStatusCounts["영향도분석"] || 0, gradient: "from-amber-400 to-orange-500", textColor: "text-white" },
{ label: "ECN발행", value: ecrStatusCounts["ECN발행"] || 0, gradient: "from-emerald-400 to-green-600", textColor: "text-white" },
];
const ecnStatCards = [
{ label: "도면변경", value: ecnStatusCounts["도면변경"] || 0, gradient: "from-purple-400 to-violet-600", textColor: "text-white" },
{ label: "통보완료", value: ecnStatusCounts["통보완료"] || 0, gradient: "from-teal-400 to-cyan-600", textColor: "text-white" },
{ label: "적용완료", value: ecnStatusCounts["적용완료"] || 0, gradient: "from-emerald-400 to-green-600", textColor: "text-white" },
];
const currentStatCards = currentTab === "ecr" ? ecrStatCards : ecnStatCards;
const currentList = currentTab === "ecr" ? filteredEcr : filteredEcn;
const currentStatuses = currentTab === "ecr" ? ECR_STATUSES : ECN_STATUSES;
return (
<div className="flex flex-col h-[calc(100vh-4rem)] bg-muted/30 p-4 gap-4 relative">
{loading && (
<div className="absolute inset-0 bg-background/50 flex items-center justify-center z-50">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)}
{/* 검색 섹션 */}
<Card className="shrink-0">
<CardContent className="p-4 flex flex-wrap items-end gap-4">
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground"></Label>
<div className="flex items-center gap-2">
<Input
type="date"
className="w-[140px] h-9"
value={searchDateFrom}
onChange={(e) => setSearchDateFrom(e.target.value)}
/>
<span className="text-muted-foreground">~</span>
<Input
type="date"
className="w-[140px] h-9"
value={searchDateTo}
onChange={(e) => setSearchDateTo(e.target.value)}
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground"></Label>
<Select value={currentTab} onValueChange={(v) => handleTabSwitch(v as TabType)}>
<SelectTrigger className="w-[150px] h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ecr">ECR ()</SelectItem>
<SelectItem value="ecn">ECN ()</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground"></Label>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{currentStatuses.map((s) => (
<SelectItem key={s} value={s}>
{s}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{currentTab === "ecr" && (
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground"></Label>
<Select value={searchChangeType} onValueChange={setSearchChangeType}>
<SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{CHANGE_TYPES.map((t) => (
<SelectItem key={t} value={t}>
{t}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground"></Label>
<Input
placeholder="ECR/ECN번호 / 품목 / 요청자"
className="w-[260px] h-9"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<div className="flex-1" />
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="h-9" onClick={handleResetSearch}>
<RotateCcw className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
{/* 메인 분할 레이아웃 */}
<div className="flex-1 overflow-hidden border rounded-lg bg-background shadow-sm">
<ResizablePanelGroup direction="horizontal">
{/* 왼쪽: 목록 */}
<ResizablePanel defaultSize={65} minSize={35}>
<div className="flex flex-col h-full">
<div className="flex items-center justify-between p-3 border-b bg-muted/10 shrink-0">
<div className="font-semibold flex items-center gap-2">
<ClipboardList className="w-4 h-4" />
{currentTab === "ecr" ? "설계변경요청(ECR) 목록" : "설계변경통지(ECN) 목록"}
<Badge variant="secondary" className="font-normal">
{currentList.length}
</Badge>
</div>
{currentTab === "ecr" && (
<Button size="sm" onClick={openEcrRegisterModal}>
<Plus className="w-4 h-4 mr-1.5" /> ECR
</Button>
)}
</div>
<div className="flex-1 overflow-auto">
{currentTab === "ecr" ? (
<Table>
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableRow>
<TableHead className="w-[50px] text-center">No</TableHead>
<TableHead className="w-[140px]">ECR번호</TableHead>
<TableHead className="w-[90px] text-center"></TableHead>
<TableHead className="w-[90px] text-center"></TableHead>
<TableHead className="w-[60px] text-center"></TableHead>
<TableHead className="w-[200px]"> /</TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[70px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[130px]"> ECN</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredEcr.length === 0 ? (
<TableRow>
<TableCell colSpan={11} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<Inbox className="w-8 h-8 text-muted-foreground/50" />
<span> ECR이 </span>
</div>
</TableCell>
</TableRow>
) : (
filteredEcr.map((item, idx) => (
<TableRow
key={item.id}
className={cn(
"cursor-pointer hover:bg-muted/50 transition-colors",
selectedId === item.id && "bg-primary/5"
)}
onClick={() => setSelectedId(item.id)}
>
<TableCell className="text-center text-muted-foreground">{idx + 1}</TableCell>
<TableCell className="font-semibold text-primary">{item.id}</TableCell>
<TableCell className="text-center">
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getChangeTypeStyle(item.changeType))}>
{item.changeType}
</span>
</TableCell>
<TableCell className="text-center">
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getEcrStatusStyle(item.status))}>
{item.status}
</span>
</TableCell>
<TableCell className="text-center">
{item.urgency === "긴급" ? (
<span className="px-2 py-0.5 rounded-full text-[11px] font-medium border bg-rose-100 text-rose-800 border-rose-200">
</span>
) : (
"-"
)}
</TableCell>
<TableCell className="font-medium">{item.target}</TableCell>
<TableCell className="text-xs text-muted-foreground">{item.drawingNo}</TableCell>
<TableCell>{item.reqDept}</TableCell>
<TableCell>{item.requester}</TableCell>
<TableCell>{item.date}</TableCell>
<TableCell>
{item.ecnNo ? (
<button
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-blue-100 text-blue-800 border border-blue-200 hover:bg-blue-200 transition-colors"
onClick={(e) => {
e.stopPropagation();
navigateToLink(item.ecnNo);
}}
>
{item.ecnNo} <ArrowRight className="w-3 h-3 inline" />
</button>
) : (
"-"
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
) : (
<Table>
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableRow>
<TableHead className="w-[50px] text-center">No</TableHead>
<TableHead className="w-[140px]">ECN번호</TableHead>
<TableHead className="w-[90px] text-center"></TableHead>
<TableHead className="w-[200px]"> /</TableHead>
<TableHead className="w-[160px]"> ( )</TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[140px]"> </TableHead>
<TableHead className="w-[130px]"> ECR</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredEcn.length === 0 ? (
<TableRow>
<TableCell colSpan={10} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<Inbox className="w-8 h-8 text-muted-foreground/50" />
<span> ECN이 </span>
</div>
</TableCell>
</TableRow>
) : (
filteredEcn.map((item, idx) => (
<TableRow
key={item.id}
className={cn(
"cursor-pointer hover:bg-muted/50 transition-colors",
selectedId === item.id && "bg-primary/5"
)}
onClick={() => setSelectedId(item.id)}
>
<TableCell className="text-center text-muted-foreground">{idx + 1}</TableCell>
<TableCell className="font-semibold text-primary">{item.id}</TableCell>
<TableCell className="text-center">
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getEcnStatusStyle(item.status))}>
{item.status}
</span>
</TableCell>
<TableCell className="font-medium">{item.target}</TableCell>
<TableCell className="text-xs text-emerald-600 font-medium">{item.drawingAfter}</TableCell>
<TableCell>{item.designer}</TableCell>
<TableCell>{item.date}</TableCell>
<TableCell>{item.applyDate}</TableCell>
<TableCell className="text-xs text-muted-foreground">{item.notifyDepts.join(", ")}</TableCell>
<TableCell>
<button
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-amber-100 text-amber-800 border border-amber-200 hover:bg-amber-200 transition-colors"
onClick={(e) => {
e.stopPropagation();
navigateToLink(item.ecrNo);
}}
>
{item.ecrNo} <ArrowRight className="w-3 h-3 inline" />
</button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
{/* 오른쪽: 상세 */}
<ResizablePanel defaultSize={35} minSize={20}>
<div className="flex flex-col h-full bg-card">
<div className="flex items-center justify-between p-3 border-b shrink-0">
<span className="font-semibold flex items-center gap-2">
<FileText className="w-4 h-4" />
</span>
{selectedEcr && (
<div className="flex items-center gap-1.5">
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => openEcrEditModal(selectedEcr.id)}>
<Pencil className="w-3 h-3 mr-1" />
</Button>
{selectedEcr.status === "영향도분석" && (
<>
<Button size="sm" className="h-7 text-xs" onClick={() => openEcnIssueModal(selectedEcr.id)}>
<FileText className="w-3 h-3 mr-1" /> ECN
</Button>
<Button variant="destructive" size="sm" className="h-7 text-xs" onClick={() => openRejectModal(selectedEcr.id)}>
<XCircle className="w-3 h-3 mr-1" />
</Button>
</>
)}
</div>
)}
</div>
<div className="flex-1 overflow-auto p-4 space-y-5">
{/* 현황 카드 */}
<div className="grid grid-cols-3 gap-3">
{currentStatCards.map((card) => (
<button
key={card.label}
onClick={() => handleFilterByStatus(card.label)}
className={cn(
"rounded-xl p-4 text-center bg-linear-to-br transition-all hover:-translate-y-0.5 hover:shadow-md cursor-pointer",
card.gradient,
card.textColor
)}
>
<div className="text-xs font-medium opacity-90 mb-1">{card.label}</div>
<div className="text-2xl font-bold">{card.value}</div>
</button>
))}
</div>
{/* ECR 상세 */}
{selectedEcr ? (
<div className="space-y-5">
<section>
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2 pt-2 border-t">
</h3>
<div className="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div>
<span className="text-muted-foreground text-xs block mb-1">ECR번호</span>
<span className="font-medium text-primary">{selectedEcr.id}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"></span>
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border inline-block", getEcrStatusStyle(selectedEcr.status))}>
{selectedEcr.status}
</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"> </span>
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border inline-block", getChangeTypeStyle(selectedEcr.changeType))}>
{selectedEcr.changeType}
</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"></span>
<span>
{selectedEcr.urgency === "긴급" ? (
<span className="px-2 py-0.5 rounded-full text-[11px] font-medium border bg-rose-100 text-rose-800 border-rose-200"></span>
) : (
"보통"
)}
</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"> /</span>
<span className="font-medium">{selectedEcr.target}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"></span>
<span>{selectedEcr.drawingNo}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"> / </span>
<span>{selectedEcr.reqDept} / {selectedEcr.requester}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"></span>
<span>{selectedEcr.date}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"> </span>
<span>{selectedEcr.applyTiming}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"> ECN</span>
{selectedEcr.ecnNo ? (
<button
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-blue-100 text-blue-800 border border-blue-200 hover:bg-blue-200 transition-colors"
onClick={() => navigateToLink(selectedEcr.ecnNo)}
>
{selectedEcr.ecnNo} <ArrowRight className="w-3 h-3 inline" />
</button>
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div>
</section>
<section>
<h3 className="text-sm font-semibold mb-2"> </h3>
<div className="bg-muted/30 p-3 rounded-md border border-border/50 text-sm whitespace-pre-wrap min-h-[50px]">
{selectedEcr.reason}
</div>
</section>
<section>
<h3 className="text-sm font-semibold mb-2"> </h3>
<div className="bg-muted/30 p-3 rounded-md border border-border/50 text-sm whitespace-pre-wrap min-h-[50px]">
{selectedEcr.content}
</div>
</section>
<section>
<h3 className="text-sm font-semibold mb-2"> </h3>
<div className="flex flex-wrap gap-1.5">
{selectedEcr.impact.map((imp) => (
<span key={imp} className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getImpactBadgeStyle(imp))}>
{imp}
</span>
))}
</div>
</section>
<section>
<h3 className="text-sm font-semibold mb-3"> </h3>
<Timeline history={selectedEcr.history} />
</section>
</div>
) : selectedEcn ? (
<div className="space-y-5">
<section>
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2 pt-2 border-t">
ECN
</h3>
<div className="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div>
<span className="text-muted-foreground text-xs block mb-1">ECN번호</span>
<span className="font-medium text-primary">{selectedEcn.id}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"></span>
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border inline-block", getEcnStatusStyle(selectedEcn.status))}>
{selectedEcn.status}
</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"> /</span>
<span className="font-medium">{selectedEcn.target}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"></span>
<span>{selectedEcn.designer}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"></span>
<span>{selectedEcn.date}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"></span>
<span>{selectedEcn.applyDate}</span>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"> ECR</span>
<button
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-amber-100 text-amber-800 border border-amber-200 hover:bg-amber-200 transition-colors"
onClick={() => navigateToLink(selectedEcn.ecrNo)}
>
{selectedEcn.ecrNo} <ArrowRight className="w-3 h-3 inline" />
</button>
</div>
<div>
<span className="text-muted-foreground text-xs block mb-1"> </span>
<span className="text-xs">{selectedEcn.notifyDepts.join(", ")}</span>
</div>
</div>
</section>
<section>
<h3 className="text-sm font-semibold mb-2"> / </h3>
<div className="grid grid-cols-2 gap-3">
<div className="bg-rose-50 p-3 rounded-md border border-rose-200">
<div className="text-xs font-semibold text-rose-800 mb-1.5">
({selectedEcn.drawingBefore})
</div>
<div className="text-sm whitespace-pre-wrap">{selectedEcn.before}</div>
</div>
<div className="bg-emerald-50 p-3 rounded-md border border-emerald-200">
<div className="text-xs font-semibold text-emerald-800 mb-1.5">
({selectedEcn.drawingAfter})
</div>
<div className="text-sm whitespace-pre-wrap">{selectedEcn.after}</div>
</div>
</div>
</section>
<section>
<h3 className="text-sm font-semibold mb-2"> </h3>
<div className="bg-muted/30 p-3 rounded-md border border-border/50 text-sm whitespace-pre-wrap">
{selectedEcn.reason}
</div>
{selectedEcn.remark && (
<p className="text-xs text-muted-foreground mt-2">: {selectedEcn.remark}</p>
)}
</section>
<section>
<h3 className="text-sm font-semibold mb-3"> </h3>
<Timeline history={selectedEcn.history} />
</section>
</div>
) : (
<div className="flex flex-col items-center justify-center py-10 text-center">
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center mb-3">
<FileText className="w-7 h-7 text-muted-foreground" />
</div>
<p className="text-sm text-muted-foreground"> </p>
</div>
)}
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
{/* ECR 등록/수정 모달 */}
<Dialog open={isEcrModalOpen} onOpenChange={setIsEcrModalOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[950px] max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
{isEcrEditMode ? "설계변경요청(ECR) 수정" : "설계변경요청(ECR) 등록"}
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{isEcrEditMode ? "ECR 정보를 수정합니다." : "새로운 설계변경요청을 등록합니다."}
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-auto">
<div className="flex flex-col md:flex-row gap-5">
{/* 좌측: 요청 정보 */}
<div className="md:w-[380px] shrink-0 space-y-3 bg-muted/30 p-4 rounded-lg border border-border/50">
<h3 className="text-sm font-semibold pb-2 border-b"> </h3>
<div>
<Label className="text-xs sm:text-sm">ECR번호</Label>
<Input value={ecrForm.id || ""} readOnly className="h-8 text-xs sm:h-10 sm:text-sm bg-muted cursor-not-allowed" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs sm:text-sm"></Label>
<Input
type="date"
value={ecrForm.date || ""}
onChange={(e) => setEcrForm((p) => ({ ...p, date: e.target.value }))}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select value={ecrForm.applyTiming || "즉시"} onValueChange={(v) => setEcrForm((p) => ({ ...p, applyTiming: v }))}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="즉시"> </SelectItem>
<SelectItem value="재고소진후"> </SelectItem>
<SelectItem value="특정일자"> </SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs sm:text-sm"> <span className="text-destructive">*</span></Label>
<Select value={ecrForm.changeType || ""} onValueChange={(v) => setEcrForm((p) => ({ ...p, changeType: v as ChangeType }))}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{CHANGE_TYPES.map((t) => (
<SelectItem key={t} value={t}>{t}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs sm:text-sm"></Label>
<Select value={ecrForm.urgency || "보통"} onValueChange={(v) => setEcrForm((p) => ({ ...p, urgency: v as "보통" | "긴급" }))}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="보통"></SelectItem>
<SelectItem value="긴급"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label className="text-xs sm:text-sm"> / <span className="text-destructive">*</span></Label>
<Input
value={ecrForm.target || ""}
onChange={(e) => setEcrForm((p) => ({ ...p, target: e.target.value }))}
placeholder="품목코드 / 설비명"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Input
value={ecrForm.drawingNo || ""}
onChange={(e) => setEcrForm((p) => ({ ...p, drawingNo: e.target.value }))}
placeholder="DWG-XXX-XXX"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs sm:text-sm"></Label>
<Select value={ecrForm.reqDept || ""} onValueChange={(v) => setEcrForm((p) => ({ ...p, reqDept: v }))}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{DEPARTMENTS.map((d) => (
<SelectItem key={d} value={d}>{d}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs sm:text-sm"></Label>
<Input
value={ecrForm.requester || ""}
onChange={(e) => setEcrForm((p) => ({ ...p, requester: e.target.value }))}
placeholder="요청자명"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
</div>
</div>
{/* 우측: 변경 내용 */}
<div className="flex-1 min-w-0 flex flex-col gap-4">
<div className="space-y-3 bg-muted/30 p-4 rounded-lg border border-border/50 flex-1">
<h3 className="text-sm font-semibold pb-2 border-b"> </h3>
<div>
<Label className="text-xs sm:text-sm"> <span className="text-destructive">*</span></Label>
<Textarea
value={ecrForm.reason || ""}
onChange={(e) => setEcrForm((p) => ({ ...p, reason: e.target.value }))}
placeholder="변경이 필요한 구체적인 사유를 기술하세요"
className="min-h-[80px] resize-y text-xs sm:text-sm"
/>
</div>
<div>
<Label className="text-xs sm:text-sm"> <span className="text-destructive">*</span></Label>
<Textarea
value={ecrForm.content || ""}
onChange={(e) => setEcrForm((p) => ({ ...p, content: e.target.value }))}
placeholder="어떻게 변경해야 하는지 구체적으로 기술하세요"
className="min-h-[80px] resize-y text-xs sm:text-sm"
/>
</div>
<div>
<Label className="text-xs sm:text-sm"> ( )</Label>
<div className="grid grid-cols-3 gap-2 mt-2">
{IMPACT_OPTIONS.map((opt) => (
<label
key={opt.key}
className="flex items-center gap-2 px-3 py-2 bg-background border rounded-md text-xs cursor-pointer hover:bg-muted/50 transition-colors"
>
<Checkbox
checked={!!ecrImpactChecks[opt.key]}
onCheckedChange={(c) =>
setEcrImpactChecks((prev) => ({ ...prev, [opt.key]: !!c }))
}
/>
{opt.label}
</label>
))}
</div>
</div>
</div>
<div className="space-y-2 bg-muted/30 p-4 rounded-lg border border-border/50">
<h3 className="text-sm font-semibold pb-2 border-b flex items-center gap-2">
<Paperclip className="w-3.5 h-3.5" />
</h3>
<div className="border-2 border-dashed border-border rounded-md p-4 text-center text-xs text-muted-foreground cursor-pointer hover:border-primary/50 hover:bg-muted/30 transition-colors">
<Upload className="w-5 h-5 mx-auto mb-1.5 text-muted-foreground" />
( , )
</div>
</div>
</div>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={() => setIsEcrModalOpen(false)} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
</Button>
<Button onClick={handleSaveEcr} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
<Save className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* ECN 발행 모달 */}
<Dialog open={isEcnModalOpen} onOpenChange={setIsEcnModalOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[950px] max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">ECN ()</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">ECR .</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-auto">
<div className="flex flex-col md:flex-row gap-5">
{/* 좌측 */}
<div className="md:w-[380px] shrink-0 space-y-3 bg-muted/30 p-4 rounded-lg border border-border/50">
<h3 className="text-sm font-semibold pb-2 border-b">ECN </h3>
<div>
<Label className="text-xs sm:text-sm">ECN번호</Label>
<Input value={ecnForm.id || ""} readOnly className="h-8 text-xs sm:h-10 sm:text-sm bg-muted cursor-not-allowed" />
</div>
<div>
<Label className="text-xs sm:text-sm"> ECR번호</Label>
<Input value={ecnForm.ecrNo || ""} readOnly className="h-8 text-xs sm:h-10 sm:text-sm bg-muted cursor-not-allowed" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs sm:text-sm"></Label>
<Input
type="date"
value={ecnForm.date || ""}
onChange={(e) => setEcnForm((p) => ({ ...p, date: e.target.value }))}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label className="text-xs sm:text-sm"> <span className="text-destructive">*</span></Label>
<Input
type="date"
value={ecnForm.applyDate || ""}
onChange={(e) => setEcnForm((p) => ({ ...p, applyDate: e.target.value }))}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
</div>
<div>
<Label className="text-xs sm:text-sm"> /</Label>
<Input value={ecnForm.target || ""} readOnly className="h-8 text-xs sm:h-10 sm:text-sm bg-muted cursor-not-allowed" />
</div>
<div>
<Label className="text-xs sm:text-sm"> ( )</Label>
<Input
value={ecnForm.drawingAfter || ""}
onChange={(e) => setEcnForm((p) => ({ ...p, drawingAfter: e.target.value }))}
placeholder="변경된 도면번호 (Rev 포함)"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select value={ecnForm.designer || ""} onValueChange={(v) => setEcnForm((p) => ({ ...p, designer: v }))}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{DESIGNERS.map((d) => (
<SelectItem key={d} value={d}>{d}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 우측 */}
<div className="flex-1 min-w-0 flex flex-col gap-4">
<div className="space-y-3 bg-muted/30 p-4 rounded-lg border border-border/50">
<h3 className="text-sm font-semibold pb-2 border-b"> / </h3>
<div>
<Label className="text-xs sm:text-sm"> (AS-IS)</Label>
<Textarea
value={ecnForm.before || ""}
onChange={(e) => setEcnForm((p) => ({ ...p, before: e.target.value }))}
placeholder="변경 전 상태/사양/치수 등"
className="min-h-[80px] resize-y text-xs sm:text-sm"
/>
</div>
<div>
<Label className="text-xs sm:text-sm"> (TO-BE) <span className="text-destructive">*</span></Label>
<Textarea
value={ecnForm.after || ""}
onChange={(e) => setEcnForm((p) => ({ ...p, after: e.target.value }))}
placeholder="변경 후 상태/사양/치수 등"
className="min-h-[80px] resize-y text-xs sm:text-sm"
/>
</div>
</div>
<div className="space-y-3 bg-muted/30 p-4 rounded-lg border border-border/50">
<h3 className="text-sm font-semibold pb-2 border-b"> </h3>
<div className="grid grid-cols-3 gap-2">
{NOTIFY_DEPTS.map((dept) => (
<label
key={dept.key}
className="flex items-center gap-2 px-3 py-2 bg-background border rounded-md text-xs cursor-pointer hover:bg-muted/50 transition-colors"
>
<Checkbox
checked={!!ecnNotifyChecks[dept.key]}
onCheckedChange={(c) =>
setEcnNotifyChecks((prev) => ({ ...prev, [dept.key]: !!c }))
}
/>
{dept.label}
</label>
))}
</div>
</div>
<div className="space-y-3 bg-muted/30 p-4 rounded-lg border border-border/50">
<h3 className="text-sm font-semibold pb-2 border-b"> </h3>
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Textarea
value={ecnForm.reason || ""}
readOnly
className="min-h-[60px] resize-y text-xs sm:text-sm bg-muted cursor-not-allowed"
/>
</div>
<div>
<Label className="text-xs sm:text-sm"></Label>
<Textarea
value={ecnForm.remark || ""}
onChange={(e) => setEcnForm((p) => ({ ...p, remark: e.target.value }))}
placeholder="추가 참고사항"
className="min-h-[60px] resize-y text-xs sm:text-sm"
/>
</div>
</div>
</div>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={() => setIsEcnModalOpen(false)} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
</Button>
<Button onClick={handleSaveEcn} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
<FileText className="w-4 h-4 mr-2" /> ECN
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 기각 모달 */}
<Dialog open={isRejectModalOpen} onOpenChange={setIsRejectModalOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg flex items-center gap-2">
<XCircle className="w-5 h-5 text-destructive" />
ECR
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{rejectTargetId} .
</DialogDescription>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
<div>
<Label className="text-xs sm:text-sm"> <span className="text-destructive">*</span></Label>
<Textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="기각 사유를 상세히 입력해주세요"
className="min-h-[100px] resize-y text-xs sm:text-sm"
/>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={() => setIsRejectModalOpen(false)} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
</Button>
<Button variant="destructive" onClick={handleRejectSubmit} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
<XCircle className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}