Files
pipeline/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx
T
kjs 9272ddb345 refactor: Improve table header and cell styling for production plan management and item inspection pages
- Updated the table header and cell styles to enhance visibility and usability, including adjustments to z-index and sticky positioning.
- Implemented dynamic label mapping for inspection types in the item inspection page to improve clarity.
- Enhanced the sales order page by including management item filters in server queries, allowing for better data handling and user experience.
- These changes aim to provide a more intuitive interface and improve data representation across multiple company implementations.
2026-04-13 11:58:26 +09:00

741 lines
42 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import {
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
const TABLE_NAME = "item_inspection_info";
const ITEM_TABLE = "item_info";
const INSPECTION_TABLE = "inspection_standard";
const GRID_COLUMNS = [
{ key: "item_code", label: "품목코드" },
{ key: "item_name", label: "품명" },
{ key: "inspection_type", label: "검사유형" },
{ key: "is_active", label: "사용여부" },
];
const INSPECTION_TYPES = [
{ key: "incoming_inspection", label: "수입검사", matchLabels: ["수입검사", "입고검사", "수입", "입고"] },
{ key: "outgoing_inspection", label: "출하검사", matchLabels: ["출하검사", "출고검사", "출하", "출고"] },
{ key: "process_inspection", label: "공정검사", matchLabels: ["공정검사", "공정"] },
{ key: "final_inspection", label: "최종검사", matchLabels: ["최종검사", "최종", "완제품검사"] },
] as const;
type InspectionRow = {
id: string;
inspection_standard_id: string;
inspection_detail: string;
inspection_method: string;
apply_process: string;
acceptance_criteria: string;
is_required: boolean;
};
export default function ItemInspectionInfoPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
const ts = useTableSettings("c16-item-inspection", TABLE_NAME, GRID_COLUMNS);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
const [checkedIds, setCheckedIds] = useState<string[]>([]);
// 우측 패널: 선택된 품목
const [selectedItemCode, setSelectedItemCode] = useState<string | null>(null);
const [selectedTypeTab, setSelectedTypeTab] = useState<string>("");
// 모달
const [modalOpen, setModalOpen] = useState(false);
const [editMode, setEditMode] = useState(false);
const [form, setForm] = useState<Record<string, any>>({});
const [saving, setSaving] = useState(false);
// FK 옵션
const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]);
const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]);
const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]);
const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]);
const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]);
// 검사유형별 검사항목 rows (모달용)
const [inspectionRows, setInspectionRows] = useState<Record<string, InspectionRow[]>>({});
const [collapsedTypes, setCollapsedTypes] = useState<Record<string, boolean>>({});
// 품목 선택 모달
const [itemModalOpen, setItemModalOpen] = useState(false);
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
const [filteredItems, setFilteredItems] = useState<typeof itemOptions>([]);
/* ═══════════════════ FK 옵션 로드 ═══════════════════ */
useEffect(() => {
const loadOptions = async () => {
try {
const [itemRes, inspRes, userRes] = await Promise.all([
apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, { page: 1, size: 500, autoFilter: true }),
apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/data`, { page: 1, size: 500, autoFilter: true }),
apiClient.post(`/table-management/tables/user_info/data`, { page: 1, size: 500, autoFilter: true }),
]);
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
setItemOptions(items.map((r: any) => ({
code: r.item_number || r.item_code || "",
name: r.item_name || "",
item_type: r.type || r.item_type || "",
unit: r.unit || "",
})));
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
setInspOptions(insps.map((r: any) => ({
code: r.id,
label: r.inspection_criteria || r.inspection_standard || r.id,
detail: r.inspection_item || r.inspection_criteria || "",
method: r.inspection_method || "",
types: r.inspection_type ? r.inspection_type.split(",").filter(Boolean) : [],
})));
// 검사유형 카테고리
try {
const catRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_type/values`);
const flatCats: { code: string; label: string }[] = [];
const flatten = (arr: any[]) => { for (const v of arr) { flatCats.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flatten(v.children); } };
if (catRes.data?.data?.length) flatten(catRes.data.data);
setInspTypeCatOptions(flatCats);
} catch { /* skip */ }
// 검사방법 카테고리
try {
const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`);
const flatMethods: { code: string; label: string }[] = [];
const flattenM = (arr: any[]) => { for (const v of arr) { flatMethods.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flattenM(v.children); } };
if (methodRes.data?.data?.length) flattenM(methodRes.data.data);
setInspMethodCatOptions(flatMethods);
} catch { /* skip */ }
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
setUserOptions(users.map((u: any) => ({
code: u.user_id || u.id,
label: `${u.user_name || u.name || u.user_id}${u.dept_name ? ` (${u.dept_name})` : ""}`,
})));
} catch { /* skip */ }
};
loadOptions();
}, []);
/* ═══════════════════ 품목 선택 모달 ═══════════════════ */
const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); };
const handleItemSearch = () => {
const kw = itemSearchKeyword.trim().toLowerCase();
if (!kw) { setFilteredItems(itemOptions); return; }
setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw)));
};
const selectItem = (item: typeof itemOptions[0]) => {
setForm(p => ({ ...p, item_code: item.code, item_name: item.name }));
setItemModalOpen(false);
};
/* ═══════════════════ 데이터 조회 ═══════════════════ */
const fetchData = useCallback(async () => {
setLoading(true);
try {
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, {
page: 1, size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const rows = res.data?.data?.data || res.data?.data?.rows || [];
setData(rows);
setTotalCount(rows.length);
} catch {
toast.error("품목검사정보 조회에 실패했어요");
} finally {
setLoading(false);
}
}, [searchFilters]);
useEffect(() => { fetchData(); }, [fetchData]);
// item_code별 그룹핑
const groupedData = useMemo(() => {
const map: Record<string, { item_code: string; item_name: string; is_active: string; types: string[]; rows: any[] }> = {};
for (const row of data) {
const key = row.item_code || row.id;
if (!map[key]) {
map[key] = { item_code: row.item_code, item_name: row.item_name, is_active: row.is_active || "", types: [], rows: [] };
}
map[key].rows.push(row);
if (row.inspection_type && !map[key].types.includes(row.inspection_type)) {
map[key].types.push(row.inspection_type);
}
}
return Object.values(map);
}, [data]);
// 선택된 품목의 그룹 데이터
const selectedGroup = useMemo(() => {
if (!selectedItemCode) return null;
return groupedData.find(g => g.item_code === selectedItemCode) || null;
}, [selectedItemCode, groupedData]);
// 선택된 탭의 검사항목 행
const selectedTabRows = useMemo(() => {
if (!selectedGroup || !selectedTypeTab) return [];
return selectedGroup.rows.filter((r: any) => r.inspection_type === selectedTypeTab && r.inspection_standard_id);
}, [selectedGroup, selectedTypeTab]);
// 검사기준 ID → 라벨
const resolveInspLabel = useCallback((id: string) => {
return inspOptions.find(o => o.code === id)?.label || id || "-";
}, [inspOptions]);
// 검사방법 코드 → 라벨
const resolveMethodLabel = useCallback((code: string) => {
return inspMethodCatOptions.find(o => o.code === code)?.label || code || "-";
}, [inspMethodCatOptions]);
/* ═══════════════════ CRUD ═══════════════════ */
const openCreate = () => { setForm({}); setEditMode(false); setInspectionRows({}); setCollapsedTypes({}); setModalOpen(true); };
const openEdit = async (itemCode?: string) => {
const code = itemCode || selectedItemCode;
if (!code) { toast.error("수정할 항목을 선택해주세요"); return; }
const group = groupedData.find(g => g.item_code === code);
if (!group) return;
const row = group.rows[0];
setForm({ ...row });
setEditMode(true);
setCollapsedTypes({});
try {
const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, {
page: 1, size: 500,
dataFilter: { enabled: true, filters: [{ columnName: "item_code", operator: "equals", value: code }] },
autoFilter: true,
});
const allRows = res.data?.data?.data || res.data?.data?.rows || [];
const rowMap: Record<string, InspectionRow[]> = {};
const typeFlags: Record<string, boolean> = {};
for (const r of allRows) {
const inspType = r.inspection_type || "";
const matched = INSPECTION_TYPES.find(t =>
t.matchLabels.some(ml => inspType.includes(ml)) ||
inspTypeCatOptions.some(cat => inspType.includes(cat.code) && t.matchLabels.some(ml => cat.label.includes(ml)))
);
const typeKey = matched?.key || "";
if (!typeKey) continue;
typeFlags[typeKey] = true;
if (!rowMap[typeKey]) rowMap[typeKey] = [];
const mCode = r.inspection_method || "";
const mLabel = inspMethodCatOptions.find(o => o.code === mCode)?.label || mCode;
rowMap[typeKey].push({
id: r.id,
inspection_standard_id: r.inspection_standard_id || "",
inspection_detail: r.inspection_item_name || r.inspection_standard || "",
inspection_method: mLabel,
apply_process: "",
acceptance_criteria: r.pass_criteria || "",
is_required: r.is_required === "true" || r.is_required === true,
});
}
setInspectionRows(rowMap);
setForm(p => ({ ...p, ...typeFlags }));
} catch { setInspectionRows({}); }
setModalOpen(true);
};
/* ═══════════════════ 검사항목 행 관리 (모달) ═══════════════════ */
const addInspRow = (typeKey: string) => {
setInspectionRows(prev => ({
...prev,
[typeKey]: [...(prev[typeKey] || []), { id: crypto.randomUUID(), inspection_standard_id: "", inspection_detail: "", inspection_method: "", apply_process: "", acceptance_criteria: "", is_required: false }],
}));
};
const removeInspRow = (typeKey: string, rowId: string) => {
setInspectionRows(prev => ({ ...prev, [typeKey]: (prev[typeKey] || []).filter(r => r.id !== rowId) }));
};
const updateInspRow = (typeKey: string, rowId: string, field: string, value: any) => {
setInspectionRows(prev => ({
...prev,
[typeKey]: (prev[typeKey] || []).map(r => {
if (r.id !== rowId) return r;
if (field === "inspection_standard_id") {
const opt = inspOptions.find(o => o.code === value);
const methodCode = opt?.method || "";
const methodLabel = inspMethodCatOptions.find(o => o.code === methodCode)?.label || methodCode;
return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: methodLabel };
}
return { ...r, [field]: value };
}),
}));
};
const getFilteredInspOptions = (typeKey: string) => {
const typeDef = INSPECTION_TYPES.find(t => t.key === typeKey);
if (!typeDef) return inspOptions;
const matchCodes = inspTypeCatOptions.filter(cat => typeDef.matchLabels.some(ml => cat.label.includes(ml))).map(cat => cat.code);
if (matchCodes.length === 0) return inspOptions;
return inspOptions.filter(opt => opt.types.some(t => matchCodes.includes(t)));
};
const toggleCollapse = (typeKey: string) => { setCollapsedTypes(prev => ({ ...prev, [typeKey]: !prev[typeKey] })); };
const handleSave = async () => {
if (!form.item_code) { toast.error("품목코드는 필수예요"); return; }
setSaving(true);
try {
if (editMode) {
const existRes = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, {
page: 1, size: 500,
dataFilter: { enabled: true, filters: [{ columnName: "item_code", operator: "equals", value: form.item_code }] },
autoFilter: true,
});
const existing = existRes.data?.data?.data || existRes.data?.data?.rows || [];
if (existing.length > 0) {
await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, { data: existing.map((r: any) => ({ id: r.id })) });
}
}
const enabledTypes = INSPECTION_TYPES.filter(t => !!form[t.key]);
const rows: any[] = [];
for (const t of enabledTypes) {
const typeRows = inspectionRows[t.key] || [];
if (typeRows.length === 0) {
rows.push({ id: crypto.randomUUID(), item_code: form.item_code, item_name: form.item_name, inspection_type: t.label, is_active: form.is_active || "사용", manager_id: form.manager_id || "", memo: form.remarks || "" });
} else {
for (const r of typeRows) {
rows.push({
id: crypto.randomUUID(), item_code: form.item_code, item_name: form.item_name, inspection_type: t.label,
inspection_standard_id: r.inspection_standard_id || "", inspection_item_name: r.inspection_detail || "",
inspection_method: r.inspection_method || "", pass_criteria: r.acceptance_criteria || "",
is_required: r.is_required ? "true" : "false", is_active: form.is_active || "사용",
manager_id: form.manager_id || "", memo: form.remarks || "",
});
}
}
}
for (const row of rows) { await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, row); }
toast.success(editMode ? "수정했어요" : "등록했어요");
setModalOpen(false);
fetchData();
} catch { toast.error("저장에 실패했어요"); }
finally { setSaving(false); }
};
const handleDelete = async () => {
if (!selectedItemCode) { toast.error("삭제할 품목을 선택해주세요"); return; }
const group = groupedData.find(g => g.item_code === selectedItemCode);
if (!group) return;
const ok = await confirm(`${selectedItemCode} 검사정보 삭제`, { description: "선택한 품목의 검사정보를 모두 삭제할까요?", variant: "destructive", confirmText: "삭제" });
if (!ok) return;
try {
await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, { data: group.rows.map((r: any) => ({ id: r.id })) });
toast.success("삭제했어요");
setSelectedItemCode(null);
fetchData();
} catch { toast.error("삭제에 실패했어요"); }
};
/* ═══════════════════ JSX ═══════════════════ */
return (
<div className="flex h-full flex-col">
{ConfirmDialogComponent}
{/* 검색 필터 */}
<div className="shrink-0 px-3 pt-3 pb-2">
<DynamicSearchFilter
tableName={TABLE_NAME}
filterId="c16-item-inspection"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={totalCount}
/>
</div>
{/* 좌우 분할 패널 */}
<div className="flex-1 min-h-0 px-3 pb-3">
<ResizablePanelGroup direction="horizontal" className="h-full rounded-lg border">
{/* ═══════ 좌측: 품목 목록 ═══════ */}
<ResizablePanel defaultSize={50} minSize={30}>
<div className="flex h-full flex-col">
<div className="shrink-0 flex items-center justify-between px-4 py-3 border-b bg-muted/30">
<div className="flex items-center gap-2">
<ClipboardList className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-semibold"> </span>
<Badge variant="secondary" className="text-[10px]">{groupedData.length}</Badge>
</div>
<div className="flex items-center gap-1.5">
<Button size="sm" className="h-7 text-xs" onClick={openCreate}><Plus className="w-3.5 h-3.5 mr-1" /></Button>
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={() => openEdit()}><Pencil className="w-3.5 h-3.5 mr-1" /></Button>
<Button size="sm" variant="destructive" className="h-7 text-xs" onClick={handleDelete}><Trash2 className="w-3.5 h-3.5 mr-1" /></Button>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => ts.setOpen(true)}><Settings2 className="h-3.5 w-3.5" /></Button>
</div>
</div>
<div className="flex-1 min-h-0 overflow-auto">
{loading ? (
<div className="flex items-center justify-center py-16"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>
) : groupedData.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<Inbox className="h-10 w-10 mb-2 opacity-30" />
<p className="text-sm"> </p>
</div>
) : (
<Table>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={ts.thStyle(col.key)}>
{col.label}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{groupedData.map((group) => (
<TableRow
key={group.item_code}
className={cn("cursor-pointer", selectedItemCode === group.item_code ? "bg-primary/10 border-l-2 border-l-primary" : "hover:bg-muted/50")}
onClick={() => {
setSelectedItemCode(group.item_code);
setSelectedTypeTab(group.types[0] || "");
}}
>
{ts.visibleColumns.map((col) => {
switch (col.key) {
case "item_code": return <TableCell key={col.key} className="text-sm font-medium text-primary">{group.item_code}</TableCell>;
case "item_name": return <TableCell key={col.key} className="text-sm">{group.item_name}</TableCell>;
case "inspection_type": return (
<TableCell key={col.key}>
<div className="flex gap-1 flex-wrap">
{group.types.map((t: string) => {
const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t;
return <Badge key={t} variant="secondary" className="text-[10px]">{label}</Badge>;
})}
</div>
</TableCell>
);
case "is_active": return (
<TableCell key={col.key}>
<Badge variant={group.is_active === "사용" || group.is_active === "true" ? "default" : "secondary"} className="text-[10px]">
{group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"}
</Badge>
</TableCell>
);
default: return <TableCell key={col.key}>{(group as any)[col.key] ?? ""}</TableCell>;
}
})}
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
<div className="shrink-0 px-4 py-2 border-t text-xs text-muted-foreground">
{groupedData.length} ( )
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
{/* ═══════ 우측: 검사유형별 검사항목 ═══════ */}
<ResizablePanel defaultSize={50} minSize={30}>
<div className="flex h-full flex-col">
<div className="shrink-0 flex items-center justify-between px-4 py-3 border-b bg-muted/30">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold"> </span>
</div>
{selectedGroup && (
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={() => openEdit()}><Pencil className="w-3.5 h-3.5 mr-1" /></Button>
<Button size="sm" variant="destructive" className="h-7 text-xs" onClick={handleDelete}><Trash2 className="w-3.5 h-3.5 mr-1" /></Button>
</div>
)}
</div>
{!selectedGroup ? (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center space-y-2">
<ClipboardList className="h-8 w-8 mx-auto opacity-30" />
<p className="text-sm"> </p>
</div>
</div>
) : (
<div className="flex-1 min-h-0 flex flex-col">
{/* 검사유형 탭 */}
<div className="shrink-0 flex items-center gap-2 px-4 py-3">
{selectedGroup.types.map((type: string) => {
const count = selectedGroup.rows.filter((r: any) => r.inspection_type === type && r.inspection_standard_id).length;
return (
<button
key={type}
type="button"
onClick={() => setSelectedTypeTab(type)}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-colors border",
selectedTypeTab === type
? "bg-primary text-primary-foreground border-primary"
: "bg-muted/50 text-muted-foreground border-border hover:bg-muted"
)}
>
{inspTypeCatOptions.find((o) => o.code === type)?.label || type}
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[16px] rounded-full text-[10px] font-bold px-1",
selectedTypeTab === type ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted-foreground/20 text-muted-foreground"
)}>
{count}
</span>
</button>
);
})}
</div>
{/* 검사항목 상세 테이블 */}
<div className="flex-1 min-h-0 overflow-auto px-4 pb-4">
{selectedTypeTab && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Badge variant="default" className="text-xs">{selectedTypeTab}</Badge>
<span className="text-sm font-medium"> </span>
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableHead className="text-[10px] font-bold h-8"></TableHead>
<TableHead className="text-[10px] font-bold h-8"></TableHead>
<TableHead className="text-[10px] font-bold h-8"></TableHead>
<TableHead className="text-[10px] font-bold h-8"></TableHead>
<TableHead className="text-[10px] font-bold h-8"></TableHead>
<TableHead className="text-[10px] font-bold h-8"></TableHead>
<TableHead className="text-[10px] font-bold h-8 w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedTabRows.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-xs text-muted-foreground"> </TableCell>
</TableRow>
) : selectedTabRows.map((row: any) => (
<TableRow key={row.id}>
<TableCell className="text-xs py-2">{row.inspection_item_name || "-"}</TableCell>
<TableCell className="text-xs py-2">{resolveInspLabel(row.inspection_standard_id)}</TableCell>
<TableCell className="text-xs py-2">{resolveMethodLabel(row.inspection_method)}</TableCell>
<TableCell className="text-xs py-2">{row.apply_process || "-"}</TableCell>
<TableCell className="text-xs py-2">
{row.judgment_criteria ? (
<Badge variant="outline" className="text-[10px]">{row.judgment_criteria}</Badge>
) : "-"}
</TableCell>
<TableCell className="text-xs py-2 font-mono">{row.pass_criteria || "-"}</TableCell>
<TableCell className="text-xs py-2 text-center">
{row.is_required === "true" || row.is_required === true ? (
<Badge variant="destructive" className="text-[9px]"></Badge>
) : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
</div>
)}
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
{/* ═══════════════════ 등록/수정 모달 ═══════════════════ */}
<Dialog open={modalOpen} onOpenChange={(open) => { if (!open) setItemModalOpen(false); setModalOpen(open); }}>
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] overflow-y-auto", itemModalOpen ? "sm:max-w-xl" : "sm:max-w-4xl")}>
{itemModalOpen ? (
<>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> </DialogDescription>
</DialogHeader>
<div className="flex gap-2">
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={itemSearchKeyword} onChange={(e) => setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} />
<Button size="sm" className="h-9" onClick={handleItemSearch}><Search className="w-4 h-4 mr-1" /></Button>
</div>
<div className="border rounded-lg overflow-auto max-h-[50vh]">
<Table>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="text-[11px] font-bold"></TableHead>
<TableHead className="text-[11px] font-bold"></TableHead>
<TableHead className="text-[11px] font-bold"></TableHead>
<TableHead className="text-[11px] font-bold"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredItems.length === 0 ? (
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm"> </TableCell></TableRow>
) : filteredItems.map((item) => (
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
<TableCell className="text-sm">{item.code}</TableCell>
<TableCell className="text-sm">{item.name}</TableCell>
<TableCell className="text-sm">{item.item_type}</TableCell>
<TableCell className="text-sm">{item.unit}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<DialogFooter><Button variant="outline" onClick={() => setItemModalOpen(false)}></Button></DialogFooter>
</>
) : (
<>
<DialogHeader>
<DialogTitle>{editMode ? "품목검사정보 수정" : "품목검사정보 등록"}</DialogTitle>
<DialogDescription className="sr-only"> </DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 품목 정보 */}
<div className="space-y-3">
<h4 className="text-sm font-semibold"> </h4>
<div className="flex items-end gap-2">
<div className="space-y-1.5 flex-1">
<Label className="text-xs font-semibold text-muted-foreground"> <span className="text-destructive">*</span></Label>
<Input className="h-9 bg-muted" value={form.item_code || ""} readOnly placeholder="품목코드" />
</div>
<div className="space-y-1.5 flex-1">
<Label className="text-xs font-semibold text-muted-foreground"></Label>
<Input className="h-9 bg-muted" value={form.item_name || ""} readOnly placeholder="품목명" />
</div>
<Button type="button" variant="outline" size="sm" className="h-9 px-3 shrink-0" onClick={openItemModal}>
<Search className="w-4 h-4 mr-1" />
</Button>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground"></Label>
<Select value={form.is_active === false || form.is_active === "N" ? "N" : "Y"} onValueChange={(v) => setForm(p => ({ ...p, is_active: v === "Y" ? "사용" : "미사용" }))}>
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="Y"></SelectItem>
<SelectItem value="N"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground"></Label>
<Select value={form.manager || ""} onValueChange={(v) => setForm(p => ({ ...p, manager: v }))}>
<SelectTrigger className="h-9"><SelectValue placeholder="관리자 선택" /></SelectTrigger>
<SelectContent>{userOptions.map(o => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}</SelectContent>
</Select>
</div>
</div>
</div>
{/* 검사유형 선택 */}
<div className="space-y-3">
<h4 className="text-sm font-semibold"> </h4>
<div className="flex flex-wrap gap-4">
{INSPECTION_TYPES.map(({ key, label }) => (
<div key={key} className="flex items-center gap-1.5">
<Checkbox checked={!!form[key]} onCheckedChange={(v) => setForm(p => ({ ...p, [key]: !!v }))} />
<Label className="text-sm cursor-pointer">{label}</Label>
</div>
))}
</div>
</div>
{/* 검사유형별 검사항목 설정 */}
{INSPECTION_TYPES.filter(t => !!form[t.key]).map(({ key, label }) => (
<div key={key} className="space-y-2">
<button type="button" className="w-full flex items-center gap-2 py-2 px-3 rounded-md border bg-muted/50 hover:bg-muted text-left" onClick={() => toggleCollapse(key)}>
<Badge variant="default" className="text-xs">{label}</Badge>
<span className="text-sm font-medium"> </span>
<span className="text-xs text-muted-foreground ml-auto">{(inspectionRows[key] || []).length}</span>
</button>
{!collapsedTypes[key] && (
<div className="space-y-2 pl-1">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-muted-foreground"> </span>
<Button type="button" size="sm" variant="outline" className="h-7 text-xs" onClick={() => addInspRow(key)}>
<Plus className="w-3 h-3 mr-1" />
</Button>
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableHead className="text-[10px] font-bold w-[170px]"> </TableHead>
<TableHead className="text-[10px] font-bold w-[130px]"></TableHead>
<TableHead className="text-[10px] font-bold w-[90px]"></TableHead>
<TableHead className="text-[10px] font-bold w-[100px]"></TableHead>
<TableHead className="text-[10px] font-bold"></TableHead>
<TableHead className="text-[10px] font-bold w-[40px]"></TableHead>
<TableHead className="text-[10px] font-bold w-[36px]" />
</TableRow>
</TableHeader>
<TableBody>
{(!inspectionRows[key] || inspectionRows[key].length === 0) ? (
<TableRow><TableCell colSpan={7} className="text-center py-4 text-xs text-muted-foreground"> </TableCell></TableRow>
) : inspectionRows[key].map((row) => (
<TableRow key={row.id}>
<TableCell className="p-1">
<Select value={row.inspection_standard_id || ""} onValueChange={(v) => updateInspRow(key, row.id, "inspection_standard_id", v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="검사기준 선택" /></SelectTrigger>
<SelectContent>{getFilteredInspOptions(key).map(o => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}</SelectContent>
</Select>
</TableCell>
<TableCell className="p-1"><Input className="h-8 text-xs bg-muted" value={row.inspection_detail} readOnly placeholder="자동" /></TableCell>
<TableCell className="p-1"><Input className="h-8 text-xs bg-muted" value={row.inspection_method} readOnly placeholder="자동" /></TableCell>
<TableCell className="p-1">
<Input className="h-8 text-xs" value={row.apply_process} onChange={(e) => updateInspRow(key, row.id, "apply_process", e.target.value)} placeholder="공정" />
</TableCell>
<TableCell className="p-1">
<Input className="h-8 text-xs" value={row.acceptance_criteria} onChange={(e) => updateInspRow(key, row.id, "acceptance_criteria", e.target.value)} placeholder="합격기준" disabled={!row.inspection_standard_id} />
</TableCell>
<TableCell className="p-1 text-center"><Checkbox checked={row.is_required} onCheckedChange={(v) => updateInspRow(key, row.id, "is_required", !!v)} /></TableCell>
<TableCell className="p-1">
<Button type="button" variant="destructive" size="sm" className="h-7 w-7 p-0" onClick={() => removeInspRow(key, row.id)}><Trash2 className="w-3.5 h-3.5" /></Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
))}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setModalOpen(false)}></Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? <Loader2 className="w-4 h-4 animate-spin mr-1" /> : <Save className="w-4 h-4 mr-1" />}
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
<TableSettingsModal open={ts.open} onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} />
</div>
);
}