feat: Enhance copy functionality for item inspection information
- Introduced state management for copy form and inspection rows to facilitate editing and copying of inspection data. - Implemented logic to duplicate selected inspection data into a copy modal, allowing users to edit and manage copied information. - Enhanced the copy process to include validation and user feedback, ensuring a smooth experience when copying inspection details to multiple target items. - Updated the modal handling to support dynamic row management for copied inspection items, improving usability and flexibility.
This commit is contained in:
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
import {
|
||||
getProcessList,
|
||||
createProcess,
|
||||
@@ -511,23 +512,17 @@ export function ProcessMasterTab() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">설비 선택</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
key={selectedProcess.id}
|
||||
value={equipmentPick || undefined}
|
||||
options={availableEquipments.map((eq) => ({
|
||||
code: eq.equipment_code,
|
||||
label: `${eq.equipment_code} · ${eq.equipment_name}`,
|
||||
}))}
|
||||
value={equipmentPick || ""}
|
||||
onValueChange={setEquipmentPick}
|
||||
placeholder="설비를 선택해주세요"
|
||||
disabled={addingEquipment || availableEquipments.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9" size="sm">
|
||||
<SelectValue placeholder="설비를 선택해주세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableEquipments.map((eq) => (
|
||||
<SelectItem key={eq.id} value={eq.equipment_code}>
|
||||
{eq.equipment_code} · {eq.equipment_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ClipboardList,
|
||||
ChevronRight,
|
||||
Factory,
|
||||
Keyboard,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
@@ -41,7 +40,6 @@ const TAB_META = [
|
||||
icon: Settings,
|
||||
color: "text-blue-500",
|
||||
badgeColor: "bg-blue-50 text-blue-700 ring-blue-600/20",
|
||||
shortcut: "1",
|
||||
actions: ["공정 등록", "공정 수정", "공정 삭제", "설비 연결"],
|
||||
},
|
||||
{
|
||||
@@ -53,7 +51,6 @@ const TAB_META = [
|
||||
icon: GitBranch,
|
||||
color: "text-emerald-500",
|
||||
badgeColor: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
|
||||
shortcut: "2",
|
||||
actions: ["버전 생성", "공정 순서 설정", "품목 등록", "품목 해제"],
|
||||
},
|
||||
{
|
||||
@@ -65,7 +62,6 @@ const TAB_META = [
|
||||
icon: ClipboardList,
|
||||
color: "text-violet-500",
|
||||
badgeColor: "bg-violet-50 text-violet-700 ring-violet-600/20",
|
||||
shortcut: "3",
|
||||
actions: ["기준서 등록", "기준서 수정", "기준서 삭제", "작업 표준 관리"],
|
||||
},
|
||||
] as const;
|
||||
@@ -77,7 +73,6 @@ const ACTION_ICONS = [Plus, Pencil, Trash2, List] as const;
|
||||
export default function ProcessInfoPage() {
|
||||
const ts = useTableSettings("c16-process-info", "process_mst", GRID_COLUMNS);
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("process");
|
||||
const [showShortcutHint, setShowShortcutHint] = useState(false);
|
||||
|
||||
const activeMeta = TAB_META.find((t) => t.value === activeTab)!;
|
||||
|
||||
@@ -85,19 +80,6 @@ export default function ProcessInfoPage() {
|
||||
setActiveTab(value as TabValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.altKey) return;
|
||||
const tabByShortcut = TAB_META.find((t) => t.shortcut === e.key);
|
||||
if (tabByShortcut) {
|
||||
e.preventDefault();
|
||||
setActiveTab(tabByShortcut.value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col bg-muted/30">
|
||||
{/* 페이지 헤더 */}
|
||||
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
|
||||
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={() => setShowShortcutHint((v) => !v)}
|
||||
aria-label="키보드 단축키 보기"
|
||||
>
|
||||
<Keyboard className="h-3 w-3" />
|
||||
<span>단축키</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showShortcutHint && (
|
||||
<div className="mt-2 flex items-center gap-3 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">탭 전환:</span>
|
||||
{TAB_META.map((t) => (
|
||||
<span key={t.value} className="flex items-center gap-1">
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px]">
|
||||
Alt+{t.shortcut}
|
||||
</kbd>
|
||||
<span>{t.shortLabel}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
@@ -154,12 +114,12 @@ export default function ProcessInfoPage() {
|
||||
{/* 탭 네비게이션 */}
|
||||
<div className="shrink-0 border-b bg-background px-4">
|
||||
<TabsList className="h-12 bg-transparent gap-1">
|
||||
{TAB_META.map(({ value, label, icon: Icon, shortcut }) => (
|
||||
{TAB_META.map(({ value, label, icon: Icon }) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 gap-1.5"
|
||||
aria-label={`${label} (Alt+${shortcut})`}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
@@ -168,34 +128,6 @@ export default function ProcessInfoPage() {
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 탭 설명 배너 */}
|
||||
<div className="shrink-0 border-b bg-background/60 px-6 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${activeMeta.badgeColor}`}
|
||||
>
|
||||
{activeMeta.shortLabel}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{activeMeta.detailDesc}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
{activeMeta.actions.map((action, i) => {
|
||||
const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length];
|
||||
return (
|
||||
<span
|
||||
key={action}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground/70"
|
||||
>
|
||||
<ActionIcon className="h-3 w-3" />
|
||||
{action}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
<TabsContent value="process" className="min-h-0 flex-1 overflow-hidden mt-0">
|
||||
<ProcessMasterTab />
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
import {
|
||||
getProcessList,
|
||||
createProcess,
|
||||
@@ -511,23 +512,17 @@ export function ProcessMasterTab() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">설비 선택</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
key={selectedProcess.id}
|
||||
value={equipmentPick || undefined}
|
||||
options={availableEquipments.map((eq) => ({
|
||||
code: eq.equipment_code,
|
||||
label: `${eq.equipment_code} · ${eq.equipment_name}`,
|
||||
}))}
|
||||
value={equipmentPick || ""}
|
||||
onValueChange={setEquipmentPick}
|
||||
placeholder="설비를 선택해주세요"
|
||||
disabled={addingEquipment || availableEquipments.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9" size="sm">
|
||||
<SelectValue placeholder="설비를 선택해주세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableEquipments.map((eq) => (
|
||||
<SelectItem key={eq.id} value={eq.equipment_code}>
|
||||
{eq.equipment_code} · {eq.equipment_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ClipboardList,
|
||||
ChevronRight,
|
||||
Factory,
|
||||
Keyboard,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
@@ -41,7 +40,6 @@ const TAB_META = [
|
||||
icon: Settings,
|
||||
color: "text-blue-500",
|
||||
badgeColor: "bg-blue-50 text-blue-700 ring-blue-600/20",
|
||||
shortcut: "1",
|
||||
actions: ["공정 등록", "공정 수정", "공정 삭제", "설비 연결"],
|
||||
},
|
||||
{
|
||||
@@ -53,7 +51,6 @@ const TAB_META = [
|
||||
icon: GitBranch,
|
||||
color: "text-emerald-500",
|
||||
badgeColor: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
|
||||
shortcut: "2",
|
||||
actions: ["버전 생성", "공정 순서 설정", "품목 등록", "품목 해제"],
|
||||
},
|
||||
{
|
||||
@@ -65,7 +62,6 @@ const TAB_META = [
|
||||
icon: ClipboardList,
|
||||
color: "text-violet-500",
|
||||
badgeColor: "bg-violet-50 text-violet-700 ring-violet-600/20",
|
||||
shortcut: "3",
|
||||
actions: ["기준서 등록", "기준서 수정", "기준서 삭제", "작업 표준 관리"],
|
||||
},
|
||||
] as const;
|
||||
@@ -77,7 +73,6 @@ const ACTION_ICONS = [Plus, Pencil, Trash2, List] as const;
|
||||
export default function ProcessInfoPage() {
|
||||
const ts = useTableSettings("c16-process-info", "process_mst", GRID_COLUMNS);
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("process");
|
||||
const [showShortcutHint, setShowShortcutHint] = useState(false);
|
||||
|
||||
const activeMeta = TAB_META.find((t) => t.value === activeTab)!;
|
||||
|
||||
@@ -85,19 +80,6 @@ export default function ProcessInfoPage() {
|
||||
setActiveTab(value as TabValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.altKey) return;
|
||||
const tabByShortcut = TAB_META.find((t) => t.shortcut === e.key);
|
||||
if (tabByShortcut) {
|
||||
e.preventDefault();
|
||||
setActiveTab(tabByShortcut.value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col bg-muted/30">
|
||||
{/* 페이지 헤더 */}
|
||||
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
|
||||
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={() => setShowShortcutHint((v) => !v)}
|
||||
aria-label="키보드 단축키 보기"
|
||||
>
|
||||
<Keyboard className="h-3 w-3" />
|
||||
<span>단축키</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showShortcutHint && (
|
||||
<div className="mt-2 flex items-center gap-3 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">탭 전환:</span>
|
||||
{TAB_META.map((t) => (
|
||||
<span key={t.value} className="flex items-center gap-1">
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px]">
|
||||
Alt+{t.shortcut}
|
||||
</kbd>
|
||||
<span>{t.shortLabel}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
@@ -154,12 +114,12 @@ export default function ProcessInfoPage() {
|
||||
{/* 탭 네비게이션 */}
|
||||
<div className="shrink-0 border-b bg-background px-4">
|
||||
<TabsList className="h-12 bg-transparent gap-1">
|
||||
{TAB_META.map(({ value, label, icon: Icon, shortcut }) => (
|
||||
{TAB_META.map(({ value, label, icon: Icon }) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 gap-1.5"
|
||||
aria-label={`${label} (Alt+${shortcut})`}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
@@ -168,34 +128,6 @@ export default function ProcessInfoPage() {
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 탭 설명 배너 */}
|
||||
<div className="shrink-0 border-b bg-background/60 px-6 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${activeMeta.badgeColor}`}
|
||||
>
|
||||
{activeMeta.shortLabel}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{activeMeta.detailDesc}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
{activeMeta.actions.map((action, i) => {
|
||||
const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length];
|
||||
return (
|
||||
<span
|
||||
key={action}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground/70"
|
||||
>
|
||||
<ActionIcon className="h-3 w-3" />
|
||||
{action}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
<TabsContent value="process" className="min-h-0 flex-1 overflow-hidden mt-0">
|
||||
<ProcessMasterTab />
|
||||
|
||||
@@ -98,6 +98,11 @@ export default function ItemInspectionInfoPage() {
|
||||
const [inspectionRows, setInspectionRows] = useState<Record<string, InspectionRow[]>>({});
|
||||
const [collapsedTypes, setCollapsedTypes] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 복사 모달: 편집 가능한 기준 데이터 상태 (등록/수정 폼과 평행 구조)
|
||||
const [copyForm, setCopyForm] = useState<Record<string, any>>({});
|
||||
const [copyInspectionRows, setCopyInspectionRows] = useState<Record<string, InspectionRow[]>>({});
|
||||
const [copyCollapsedTypes, setCopyCollapsedTypes] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 기본 라우팅 공정 목록 (적용공정 Select용)
|
||||
const [processOptions, setProcessOptions] = useState<{ code: string; name: string }[]>([]);
|
||||
|
||||
@@ -294,11 +299,62 @@ export default function ItemInspectionInfoPage() {
|
||||
setCopyTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setCopySearchLoading(false); }
|
||||
};
|
||||
const openCopyModal = () => {
|
||||
const openCopyModal = async () => {
|
||||
if (!selectedItemCode) { toast.error("복사 기준 품목을 먼저 선택해주세요"); return; }
|
||||
const srcGroup = groupedData.find(g => g.item_code === selectedItemCode);
|
||||
if (!srcGroup || srcGroup.rows.length === 0) { toast.error("선택한 품목에 복사할 검사정보가 없어요"); return; }
|
||||
setCopySearchKeyword(""); setCopyPage(1); setCopyCheckedIds([]);
|
||||
|
||||
// 기준 품목 데이터를 편집용 상태로 복제 (openEdit과 동일한 변환 로직)
|
||||
const baseRow = srcGroup.rows[0];
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, {
|
||||
page: 1, size: 0,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "item_code", operator: "equals", value: selectedItemCode }] },
|
||||
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;
|
||||
const inspOpt = inspOptions.find(o => o.code === r.inspection_standard_id);
|
||||
const jcCode = inspOpt?.judgment_criteria || "";
|
||||
const jcLabel = judgmentCatOptions.find(c => c.code === jcCode)?.label || jcCode;
|
||||
const unitCode = inspOpt?.unit || "";
|
||||
const unitLabel = inspUnitCatOptions.find(c => c.code === unitCode)?.label || unitCode;
|
||||
rowMap[typeKey].push({
|
||||
id: crypto.randomUUID(), // 복사본은 새 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,
|
||||
judgment_criteria: jcLabel,
|
||||
selection_options: inspOpt?.selection_options || "",
|
||||
unit: unitLabel,
|
||||
});
|
||||
}
|
||||
setCopyInspectionRows(rowMap);
|
||||
setCopyForm({ ...baseRow, ...typeFlags });
|
||||
setCopyCollapsedTypes({});
|
||||
} catch {
|
||||
setCopyInspectionRows({});
|
||||
setCopyForm({ ...baseRow });
|
||||
setCopyCollapsedTypes({});
|
||||
}
|
||||
|
||||
setCopyModalOpen(true);
|
||||
searchCopyTargets(1);
|
||||
};
|
||||
@@ -309,10 +365,18 @@ export default function ItemInspectionInfoPage() {
|
||||
const handleCopy = async () => {
|
||||
if (!selectedItemCode) { toast.error("복사 기준 품목이 없어요"); return; }
|
||||
if (copyCheckedIds.length === 0) { toast.error("붙여넣을 품목을 선택해주세요"); return; }
|
||||
const sourceGroup = groupedData.find(g => g.item_code === selectedItemCode);
|
||||
if (!sourceGroup || sourceGroup.rows.length === 0) { toast.error("복사할 검사정보가 없어요"); return; }
|
||||
|
||||
// 편집된 rows를 평탄화 (선택된 검사유형의 rows만)
|
||||
const enabledTypes = INSPECTION_TYPES.filter(t => !!copyForm[t.key]);
|
||||
const flatRows: Array<{ row: InspectionRow; typeLabel: string }> = [];
|
||||
for (const t of enabledTypes) {
|
||||
const rows = copyInspectionRows[t.key] || [];
|
||||
for (const r of rows) flatRows.push({ row: r, typeLabel: t.label });
|
||||
}
|
||||
if (flatRows.length === 0) { toast.error("복사할 검사항목이 없어요"); return; }
|
||||
|
||||
const ok = await confirm(
|
||||
`선택한 ${copyCheckedIds.length}개 품목에 검사정보를 복사할까요?`,
|
||||
`선택한 ${copyCheckedIds.length}개 품목에 편집된 검사정보(${flatRows.length}개 행)를 복사할까요?`,
|
||||
{ description: "대상 품목의 기존 검사정보는 삭제 후 교체됩니다.", variant: "info", confirmText: "복사" }
|
||||
);
|
||||
if (!ok) return;
|
||||
@@ -333,13 +397,19 @@ export default function ItemInspectionInfoPage() {
|
||||
if (existing.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, { data: existing.map((r: any) => ({ id: r.id })) });
|
||||
}
|
||||
for (const r of sourceGroup.rows) {
|
||||
const { id: _id, created_at: _c, updated_at: _u, ...rest } = r;
|
||||
for (const { row: r, typeLabel } of flatRows) {
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
|
||||
...rest,
|
||||
id: crypto.randomUUID(),
|
||||
item_code: targetCode,
|
||||
item_name: targetName,
|
||||
inspection_type: typeLabel,
|
||||
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: copyForm.is_active || "사용",
|
||||
manager: copyForm.manager || "",
|
||||
});
|
||||
}
|
||||
setCopyProgress({ current: i + 1, total: copyCheckedIds.length });
|
||||
@@ -525,6 +595,46 @@ export default function ItemInspectionInfoPage() {
|
||||
};
|
||||
const toggleCollapse = (typeKey: string) => { setCollapsedTypes(prev => ({ ...prev, [typeKey]: !prev[typeKey] })); };
|
||||
|
||||
/* ═══════════════════ 복사 모달용 검사항목 행 관리 (등록 폼과 평행) ═══════════════════ */
|
||||
const addCopyInspRow = (typeKey: string) => {
|
||||
setCopyInspectionRows(prev => ({
|
||||
...prev,
|
||||
[typeKey]: [...(prev[typeKey] || []), { id: crypto.randomUUID(), inspection_standard_id: "", inspection_detail: "", inspection_method: "", apply_process: "", acceptance_criteria: "", is_required: false }],
|
||||
}));
|
||||
};
|
||||
const removeCopyInspRow = (typeKey: string, rowId: string) => {
|
||||
setCopyInspectionRows(prev => ({ ...prev, [typeKey]: (prev[typeKey] || []).filter(r => r.id !== rowId) }));
|
||||
};
|
||||
const updateCopyInspRow = (typeKey: string, rowId: string, field: string, value: any) => {
|
||||
setCopyInspectionRows(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;
|
||||
const jcCode = opt?.judgment_criteria || "";
|
||||
const jcLabel = judgmentCatOptions.find(c => c.code === jcCode)?.label || jcCode;
|
||||
const unitCode = opt?.unit || "";
|
||||
const unitLabel = inspUnitCatOptions.find(c => c.code === unitCode)?.label || unitCode;
|
||||
return {
|
||||
...r,
|
||||
inspection_standard_id: value,
|
||||
inspection_detail: opt?.detail || "",
|
||||
inspection_method: methodLabel,
|
||||
judgment_criteria: jcLabel,
|
||||
selection_options: opt?.selection_options || "",
|
||||
unit: unitLabel,
|
||||
acceptance_criteria: "",
|
||||
};
|
||||
}
|
||||
return { ...r, [field]: value };
|
||||
}),
|
||||
}));
|
||||
};
|
||||
const toggleCopyCollapse = (typeKey: string) => { setCopyCollapsedTypes(prev => ({ ...prev, [typeKey]: !prev[typeKey] })); };
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.item_code) { toast.error("품목코드는 필수예요"); return; }
|
||||
setSaving(true);
|
||||
@@ -1285,20 +1395,20 @@ export default function ItemInspectionInfoPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* ═══════════════════ 복사 모달 ═══════════════════ */}
|
||||
{/* ═══════════════════ 복사 모달 (2단 분할: 좌 대상 / 우 편집) ═══════════════════ */}
|
||||
<Dialog open={copyModalOpen} onOpenChange={(v) => { if (!copying) setCopyModalOpen(v); }}>
|
||||
<DialogContent
|
||||
className="max-w-3xl w-[95vw] max-h-[85vh] flex flex-col overflow-hidden"
|
||||
className="max-w-[95vw] sm:max-w-[1400px] w-[95vw] max-h-[90vh] flex flex-col overflow-hidden"
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => { if (copying) e.preventDefault(); }}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle>{copying ? "검사정보 복사 중..." : "검사정보 복사"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<span className="font-medium text-foreground">{selectedGroup?.item_name || "-"}</span>
|
||||
<span className="text-muted-foreground"> ({selectedItemCode})</span>
|
||||
<span>{copying ? " 의 검사정보를 복사하고 있습니다" : " 의 검사정보를 아래 선택한 품목들에 복사합니다"}</span>
|
||||
<span>{copying ? " 의 검사정보를 복사하고 있습니다" : " 의 검사정보를 편집해서 선택한 품목들에 복사합니다. 기준 품목은 변경되지 않아요"}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{copying ? (
|
||||
@@ -1322,81 +1432,225 @@ export default function ItemInspectionInfoPage() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (<>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={copySearchKeyword}
|
||||
onChange={(e) => setCopySearchKeyword(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleCopySearch(); }} />
|
||||
<Button size="sm" className="h-9" onClick={handleCopySearch} disabled={copySearchLoading}>
|
||||
{copySearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" />검색</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 border rounded-lg overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[40px] text-center">
|
||||
<Checkbox
|
||||
checked={copyFilteredItems.length > 0 && copyFilteredItems.every(i => copyCheckedIds.includes(i.code))}
|
||||
onCheckedChange={(v) => {
|
||||
if (v) setCopyCheckedIds(prev => Array.from(new Set([...prev, ...copyFilteredItems.map(i => i.code)])));
|
||||
else setCopyCheckedIds(prev => prev.filter(c => !copyFilteredItems.some(i => i.code === c)));
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{copyFilteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={5} className="text-center py-8 text-muted-foreground text-sm">
|
||||
{copySearchLoading ? "검색 중..." : "검색 결과가 없어요"}
|
||||
</TableCell></TableRow>
|
||||
) : copyFilteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5"
|
||||
onClick={() => toggleCopyChecked(item.code)}>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox checked={copyCheckedIds.includes(item.code)} onCheckedChange={() => toggleCopyChecked(item.code)} />
|
||||
</TableCell>
|
||||
<TableCell className="text-sm font-mono">{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>
|
||||
<div className="shrink-0 flex items-center justify-between border-t px-2 py-2 text-xs text-muted-foreground">
|
||||
<span>
|
||||
전체 <span className="font-medium text-foreground">{copyTotal.toLocaleString()}</span>건
|
||||
{copyCheckedIds.length > 0 && <span className="ml-2">선택 <span className="font-medium text-primary">{copyCheckedIds.length}</span>건</span>}
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setCopyPage(1); searchCopyTargets(1); }} disabled={copyPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { const p = copyPage - 1; setCopyPage(p); searchCopyTargets(p); }} disabled={copyPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3.5 w-3.5" /></button>
|
||||
{Array.from({ length: Math.min(5, copyTotalPages) }, (_, i) => {
|
||||
const start = Math.max(1, Math.min(copyPage - 2, copyTotalPages - 4));
|
||||
const p = start + i;
|
||||
if (p > copyTotalPages) return null;
|
||||
return (
|
||||
<button key={p} onClick={() => { setCopyPage(p); searchCopyTargets(p); }}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
p === copyPage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>{p}</button>
|
||||
);
|
||||
})}
|
||||
<button onClick={() => { const p = copyPage + 1; setCopyPage(p); searchCopyTargets(p); }} disabled={copyPage >= copyTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { setCopyPage(copyTotalPages); searchCopyTargets(copyTotalPages); }} disabled={copyPage >= copyTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3.5 w-3.5" /></button>
|
||||
) : (
|
||||
<div className="flex-1 grid grid-cols-[420px_1fr] gap-4 overflow-hidden">
|
||||
{/* 좌측: 복사 대상 품목 선택 */}
|
||||
<div className="flex flex-col overflow-hidden border rounded-lg">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-3 py-2">
|
||||
<span className="text-xs font-semibold">복사 대상 품목 선택</span>
|
||||
{copyCheckedIds.length > 0 && <span className="text-[10px] text-primary">선택 {copyCheckedIds.length}건</span>}
|
||||
</div>
|
||||
<div className="flex gap-2 px-2 pt-2">
|
||||
<Input className="h-8 flex-1 text-xs" placeholder="품목코드 또는 품목명" value={copySearchKeyword}
|
||||
onChange={(e) => setCopySearchKeyword(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleCopySearch(); }} />
|
||||
<Button size="sm" className="h-8 text-xs" onClick={handleCopySearch} disabled={copySearchLoading}>
|
||||
{copySearchLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Search className="w-3.5 h-3.5" />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto mt-2">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10 bg-background">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[36px] text-center text-[10px]">
|
||||
<Checkbox
|
||||
checked={copyFilteredItems.length > 0 && copyFilteredItems.every(i => copyCheckedIds.includes(i.code))}
|
||||
onCheckedChange={(v) => {
|
||||
if (v) setCopyCheckedIds(prev => Array.from(new Set([...prev, ...copyFilteredItems.map(i => i.code)])));
|
||||
else setCopyCheckedIds(prev => prev.filter(c => !copyFilteredItems.some(i => i.code === c)));
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[120px]">품목코드</TableHead>
|
||||
<TableHead className="text-[10px] font-bold">품목명</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{copyFilteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={3} className="text-center py-6 text-muted-foreground text-xs">
|
||||
{copySearchLoading ? "검색 중..." : "검색 결과가 없어요"}
|
||||
</TableCell></TableRow>
|
||||
) : copyFilteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5"
|
||||
onClick={() => toggleCopyChecked(item.code)}>
|
||||
<TableCell className="text-center p-1" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox checked={copyCheckedIds.includes(item.code)} onCheckedChange={() => toggleCopyChecked(item.code)} />
|
||||
</TableCell>
|
||||
<TableCell className="text-xs font-mono p-1">{item.code}</TableCell>
|
||||
<TableCell className="text-xs p-1 truncate">{item.name}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="border-t flex items-center justify-between px-2 py-1 text-[10px] text-muted-foreground">
|
||||
<span>전체 <span className="font-medium text-foreground">{copyTotal.toLocaleString()}</span>건</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setCopyPage(1); searchCopyTargets(1); }} disabled={copyPage <= 1}
|
||||
className="h-6 w-6 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3 w-3" /></button>
|
||||
<button onClick={() => { const p = copyPage - 1; setCopyPage(p); searchCopyTargets(p); }} disabled={copyPage <= 1}
|
||||
className="h-6 w-6 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3 w-3" /></button>
|
||||
<span className="text-[10px] mx-1">{copyPage}/{copyTotalPages}</span>
|
||||
<button onClick={() => { const p = copyPage + 1; setCopyPage(p); searchCopyTargets(p); }} disabled={copyPage >= copyTotalPages}
|
||||
className="h-6 w-6 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3 w-3" /></button>
|
||||
<button onClick={() => { setCopyPage(copyTotalPages); searchCopyTargets(copyTotalPages); }} disabled={copyPage >= copyTotalPages}
|
||||
className="h-6 w-6 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3 w-3" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측: 편집 폼 (등록/수정 폼과 동일 구조) */}
|
||||
<div className="flex flex-col overflow-hidden border rounded-lg">
|
||||
<div className="border-b bg-muted/50 px-3 py-2">
|
||||
<span className="text-xs font-semibold">복사할 검사정보 편집 (기준: {selectedItemCode})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
||||
<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={copyForm.is_active === false || copyForm.is_active === "N" ? "N" : "Y"} onValueChange={(v) => setCopyForm(p => ({ ...p, is_active: v === "Y" ? "사용" : "미사용" }))}>
|
||||
<SelectTrigger className="h-8 text-xs"><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={copyForm.manager || ""} onValueChange={(v) => setCopyForm(p => ({ ...p, manager: v }))}>
|
||||
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="관리자 선택" /></SelectTrigger>
|
||||
<SelectContent>{userOptions.map(o => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold">검사유형 선택</h4>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{INSPECTION_TYPES.map(({ key, label }) => (
|
||||
<div key={key} className="flex items-center gap-1.5">
|
||||
<Checkbox checked={!!copyForm[key]} onCheckedChange={(v) => setCopyForm(p => ({ ...p, [key]: !!v }))} />
|
||||
<Label className="text-xs cursor-pointer">{label}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{INSPECTION_TYPES.filter(t => !!copyForm[t.key]).map(({ key, label }) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<button type="button" className="w-full flex items-center gap-2 py-1.5 px-2 rounded-md border bg-muted/50 hover:bg-muted text-left" onClick={() => toggleCopyCollapse(key)}>
|
||||
<Badge variant="default" className="text-[10px]">{label}</Badge>
|
||||
<span className="text-xs font-medium">검사항목 설정</span>
|
||||
<span className="text-[10px] text-muted-foreground ml-auto">{(copyInspectionRows[key] || []).length}개</span>
|
||||
</button>
|
||||
{!copyCollapsedTypes[key] && (
|
||||
<div className="space-y-1.5 pl-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-semibold text-muted-foreground">검사항목 목록</span>
|
||||
<Button type="button" size="sm" variant="outline" className="h-6 text-[10px]" onClick={() => addCopyInspRow(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-[150px]">검사기준 선택</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[110px]">검사기준 상세</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[80px]">검사방법</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[80px]">적용공정</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[70px]">판단기준</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[180px]">합격기준</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[36px]">필수</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[60px]">단위</TableHead>
|
||||
<TableHead className="text-[10px] font-bold w-[32px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(!copyInspectionRows[key] || copyInspectionRows[key].length === 0) ? (
|
||||
<TableRow><TableCell colSpan={9} className="text-center py-3 text-[10px] text-muted-foreground">항목추가 버튼으로 검사항목을 추가하세요</TableCell></TableRow>
|
||||
) : copyInspectionRows[key].map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell className="p-1">
|
||||
<Select value={row.inspection_standard_id || ""} onValueChange={(v) => updateCopyInspRow(key, row.id, "inspection_standard_id", v)}>
|
||||
<SelectTrigger className="h-7 text-[10px]"><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-7 text-[10px] bg-muted" value={row.inspection_detail} readOnly placeholder="자동" /></TableCell>
|
||||
<TableCell className="p-1"><Input className="h-7 text-[10px] bg-muted" value={row.inspection_method} readOnly placeholder="자동" /></TableCell>
|
||||
<TableCell className="p-1">
|
||||
{processOptions.length > 0 ? (
|
||||
<Select value={row.apply_process || ""} onValueChange={(v) => updateCopyInspRow(key, row.id, "apply_process", v)}>
|
||||
<SelectTrigger className="h-7 text-[10px]"><SelectValue placeholder="공정" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{processOptions.map((p) => (
|
||||
<SelectItem key={p.code} value={p.code} className="text-xs">{p.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input className="h-7 text-[10px]" value={row.apply_process} onChange={(e) => updateCopyInspRow(key, row.id, "apply_process", e.target.value)} placeholder="공정" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-1 text-center">
|
||||
{row.judgment_criteria ? <Badge variant="outline" className="text-[9px]">{row.judgment_criteria}</Badge> : <span className="text-[9px] text-muted-foreground">-</span>}
|
||||
</TableCell>
|
||||
<TableCell className="p-1">
|
||||
{row.judgment_criteria === "선택형" && row.selection_options ? (
|
||||
<Select value={row.acceptance_criteria || ""} onValueChange={(v) => updateCopyInspRow(key, row.id, "acceptance_criteria", v)} disabled={!row.inspection_standard_id}>
|
||||
<SelectTrigger className="h-7 text-[10px]"><SelectValue placeholder="선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{row.selection_options.split(",").filter(Boolean).map((opt) => (
|
||||
<SelectItem key={opt} value={opt} className="text-xs">{opt}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : row.judgment_criteria === "O/X" ? (
|
||||
<Select value={row.acceptance_criteria || ""} onValueChange={(v) => updateCopyInspRow(key, row.id, "acceptance_criteria", v)} disabled={!row.inspection_standard_id}>
|
||||
<SelectTrigger className="h-7 text-[10px]"><SelectValue placeholder="선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="O" className="text-xs">O (합격)</SelectItem>
|
||||
<SelectItem value="X" className="text-xs">X (불합격)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : row.judgment_criteria === "수치(범위)" ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Input className="h-7 text-[10px] w-14" value={row.acceptance_criteria?.split("|")[0] || ""} onChange={(e) => {
|
||||
const parts = (row.acceptance_criteria || "||").split("|");
|
||||
parts[0] = e.target.value;
|
||||
updateCopyInspRow(key, row.id, "acceptance_criteria", parts.join("|"));
|
||||
}} placeholder="기준" disabled={!row.inspection_standard_id} />
|
||||
<span className="text-[9px] text-muted-foreground">±</span>
|
||||
<Input className="h-7 text-[10px] w-10" value={row.acceptance_criteria?.split("|")[1] || ""} onChange={(e) => {
|
||||
const parts = (row.acceptance_criteria || "||").split("|");
|
||||
parts[1] = e.target.value;
|
||||
updateCopyInspRow(key, row.id, "acceptance_criteria", parts.join("|"));
|
||||
}} placeholder="±" disabled={!row.inspection_standard_id} />
|
||||
</div>
|
||||
) : (
|
||||
<Input className="h-7 text-[10px]" value={row.acceptance_criteria} onChange={(e) => updateCopyInspRow(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) => updateCopyInspRow(key, row.id, "is_required", !!v)} /></TableCell>
|
||||
<TableCell className="p-1 text-[10px] text-muted-foreground">{row.unit || "-"}</TableCell>
|
||||
<TableCell className="p-1">
|
||||
<Button type="button" variant="destructive" size="sm" className="h-6 w-6 p-0" onClick={() => removeCopyInspRow(key, row.id)}><Trash2 className="w-3 h-3" /></Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
)}
|
||||
<DialogFooter className="shrink-0">
|
||||
<Button variant="outline" onClick={() => setCopyModalOpen(false)} disabled={copying}>취소</Button>
|
||||
<Button onClick={handleCopy} disabled={copying || copyCheckedIds.length === 0}>
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
import {
|
||||
getProcessList,
|
||||
createProcess,
|
||||
@@ -511,23 +512,17 @@ export function ProcessMasterTab() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">설비 선택</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
key={selectedProcess.id}
|
||||
value={equipmentPick || undefined}
|
||||
options={availableEquipments.map((eq) => ({
|
||||
code: eq.equipment_code,
|
||||
label: `${eq.equipment_code} · ${eq.equipment_name}`,
|
||||
}))}
|
||||
value={equipmentPick || ""}
|
||||
onValueChange={setEquipmentPick}
|
||||
placeholder="설비를 선택해주세요"
|
||||
disabled={addingEquipment || availableEquipments.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9" size="sm">
|
||||
<SelectValue placeholder="설비를 선택해주세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableEquipments.map((eq) => (
|
||||
<SelectItem key={eq.id} value={eq.equipment_code}>
|
||||
{eq.equipment_code} · {eq.equipment_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ClipboardList,
|
||||
ChevronRight,
|
||||
Factory,
|
||||
Keyboard,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
@@ -41,7 +40,6 @@ const TAB_META = [
|
||||
icon: Settings,
|
||||
color: "text-blue-500",
|
||||
badgeColor: "bg-blue-50 text-blue-700 ring-blue-600/20",
|
||||
shortcut: "1",
|
||||
actions: ["공정 등록", "공정 수정", "공정 삭제", "설비 연결"],
|
||||
},
|
||||
{
|
||||
@@ -53,7 +51,6 @@ const TAB_META = [
|
||||
icon: GitBranch,
|
||||
color: "text-emerald-500",
|
||||
badgeColor: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
|
||||
shortcut: "2",
|
||||
actions: ["버전 생성", "공정 순서 설정", "품목 등록", "품목 해제"],
|
||||
},
|
||||
{
|
||||
@@ -65,7 +62,6 @@ const TAB_META = [
|
||||
icon: ClipboardList,
|
||||
color: "text-violet-500",
|
||||
badgeColor: "bg-violet-50 text-violet-700 ring-violet-600/20",
|
||||
shortcut: "3",
|
||||
actions: ["기준서 등록", "기준서 수정", "기준서 삭제", "작업 표준 관리"],
|
||||
},
|
||||
] as const;
|
||||
@@ -77,7 +73,6 @@ const ACTION_ICONS = [Plus, Pencil, Trash2, List] as const;
|
||||
export default function ProcessInfoPage() {
|
||||
const ts = useTableSettings("c16-process-info", "process_mst", GRID_COLUMNS);
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("process");
|
||||
const [showShortcutHint, setShowShortcutHint] = useState(false);
|
||||
|
||||
const activeMeta = TAB_META.find((t) => t.value === activeTab)!;
|
||||
|
||||
@@ -85,19 +80,6 @@ export default function ProcessInfoPage() {
|
||||
setActiveTab(value as TabValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.altKey) return;
|
||||
const tabByShortcut = TAB_META.find((t) => t.shortcut === e.key);
|
||||
if (tabByShortcut) {
|
||||
e.preventDefault();
|
||||
setActiveTab(tabByShortcut.value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col bg-muted/30">
|
||||
{/* 페이지 헤더 */}
|
||||
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
|
||||
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={() => setShowShortcutHint((v) => !v)}
|
||||
aria-label="키보드 단축키 보기"
|
||||
>
|
||||
<Keyboard className="h-3 w-3" />
|
||||
<span>단축키</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showShortcutHint && (
|
||||
<div className="mt-2 flex items-center gap-3 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">탭 전환:</span>
|
||||
{TAB_META.map((t) => (
|
||||
<span key={t.value} className="flex items-center gap-1">
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px]">
|
||||
Alt+{t.shortcut}
|
||||
</kbd>
|
||||
<span>{t.shortLabel}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
@@ -154,12 +114,12 @@ export default function ProcessInfoPage() {
|
||||
{/* 탭 네비게이션 */}
|
||||
<div className="shrink-0 border-b bg-background px-4">
|
||||
<TabsList className="h-12 bg-transparent gap-1">
|
||||
{TAB_META.map(({ value, label, icon: Icon, shortcut }) => (
|
||||
{TAB_META.map(({ value, label, icon: Icon }) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 gap-1.5"
|
||||
aria-label={`${label} (Alt+${shortcut})`}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
@@ -168,34 +128,6 @@ export default function ProcessInfoPage() {
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 탭 설명 배너 */}
|
||||
<div className="shrink-0 border-b bg-background/60 px-6 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${activeMeta.badgeColor}`}
|
||||
>
|
||||
{activeMeta.shortLabel}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{activeMeta.detailDesc}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
{activeMeta.actions.map((action, i) => {
|
||||
const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length];
|
||||
return (
|
||||
<span
|
||||
key={action}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground/70"
|
||||
>
|
||||
<ActionIcon className="h-3 w-3" />
|
||||
{action}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
<TabsContent value="process" className="min-h-0 flex-1 overflow-hidden mt-0">
|
||||
<ProcessMasterTab />
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
import {
|
||||
getProcessList,
|
||||
createProcess,
|
||||
@@ -511,23 +512,17 @@ export function ProcessMasterTab() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">설비 선택</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
key={selectedProcess.id}
|
||||
value={equipmentPick || undefined}
|
||||
options={availableEquipments.map((eq) => ({
|
||||
code: eq.equipment_code,
|
||||
label: `${eq.equipment_code} · ${eq.equipment_name}`,
|
||||
}))}
|
||||
value={equipmentPick || ""}
|
||||
onValueChange={setEquipmentPick}
|
||||
placeholder="설비를 선택해주세요"
|
||||
disabled={addingEquipment || availableEquipments.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9" size="sm">
|
||||
<SelectValue placeholder="설비를 선택해주세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableEquipments.map((eq) => (
|
||||
<SelectItem key={eq.id} value={eq.equipment_code}>
|
||||
{eq.equipment_code} · {eq.equipment_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ClipboardList,
|
||||
ChevronRight,
|
||||
Factory,
|
||||
Keyboard,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
@@ -41,7 +40,6 @@ const TAB_META = [
|
||||
icon: Settings,
|
||||
color: "text-blue-500",
|
||||
badgeColor: "bg-blue-50 text-blue-700 ring-blue-600/20",
|
||||
shortcut: "1",
|
||||
actions: ["공정 등록", "공정 수정", "공정 삭제", "설비 연결"],
|
||||
},
|
||||
{
|
||||
@@ -53,7 +51,6 @@ const TAB_META = [
|
||||
icon: GitBranch,
|
||||
color: "text-emerald-500",
|
||||
badgeColor: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
|
||||
shortcut: "2",
|
||||
actions: ["버전 생성", "공정 순서 설정", "품목 등록", "품목 해제"],
|
||||
},
|
||||
{
|
||||
@@ -65,7 +62,6 @@ const TAB_META = [
|
||||
icon: ClipboardList,
|
||||
color: "text-violet-500",
|
||||
badgeColor: "bg-violet-50 text-violet-700 ring-violet-600/20",
|
||||
shortcut: "3",
|
||||
actions: ["기준서 등록", "기준서 수정", "기준서 삭제", "작업 표준 관리"],
|
||||
},
|
||||
] as const;
|
||||
@@ -77,7 +73,6 @@ const ACTION_ICONS = [Plus, Pencil, Trash2, List] as const;
|
||||
export default function ProcessInfoPage() {
|
||||
const ts = useTableSettings("c16-process-info", "process_mst", GRID_COLUMNS);
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("process");
|
||||
const [showShortcutHint, setShowShortcutHint] = useState(false);
|
||||
|
||||
const activeMeta = TAB_META.find((t) => t.value === activeTab)!;
|
||||
|
||||
@@ -85,19 +80,6 @@ export default function ProcessInfoPage() {
|
||||
setActiveTab(value as TabValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.altKey) return;
|
||||
const tabByShortcut = TAB_META.find((t) => t.shortcut === e.key);
|
||||
if (tabByShortcut) {
|
||||
e.preventDefault();
|
||||
setActiveTab(tabByShortcut.value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col bg-muted/30">
|
||||
{/* 페이지 헤더 */}
|
||||
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
|
||||
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={() => setShowShortcutHint((v) => !v)}
|
||||
aria-label="키보드 단축키 보기"
|
||||
>
|
||||
<Keyboard className="h-3 w-3" />
|
||||
<span>단축키</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showShortcutHint && (
|
||||
<div className="mt-2 flex items-center gap-3 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">탭 전환:</span>
|
||||
{TAB_META.map((t) => (
|
||||
<span key={t.value} className="flex items-center gap-1">
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px]">
|
||||
Alt+{t.shortcut}
|
||||
</kbd>
|
||||
<span>{t.shortLabel}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
@@ -154,12 +114,12 @@ export default function ProcessInfoPage() {
|
||||
{/* 탭 네비게이션 */}
|
||||
<div className="shrink-0 border-b bg-background px-4">
|
||||
<TabsList className="h-12 bg-transparent gap-1">
|
||||
{TAB_META.map(({ value, label, icon: Icon, shortcut }) => (
|
||||
{TAB_META.map(({ value, label, icon: Icon }) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 gap-1.5"
|
||||
aria-label={`${label} (Alt+${shortcut})`}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
@@ -168,34 +128,6 @@ export default function ProcessInfoPage() {
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 탭 설명 배너 */}
|
||||
<div className="shrink-0 border-b bg-background/60 px-6 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${activeMeta.badgeColor}`}
|
||||
>
|
||||
{activeMeta.shortLabel}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{activeMeta.detailDesc}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
{activeMeta.actions.map((action, i) => {
|
||||
const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length];
|
||||
return (
|
||||
<span
|
||||
key={action}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground/70"
|
||||
>
|
||||
<ActionIcon className="h-3 w-3" />
|
||||
{action}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
<TabsContent value="process" className="min-h-0 flex-1 overflow-hidden mt-0">
|
||||
<ProcessMasterTab />
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
import {
|
||||
getProcessList,
|
||||
createProcess,
|
||||
@@ -511,23 +512,17 @@ export function ProcessMasterTab() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">설비 선택</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
key={selectedProcess.id}
|
||||
value={equipmentPick || undefined}
|
||||
options={availableEquipments.map((eq) => ({
|
||||
code: eq.equipment_code,
|
||||
label: `${eq.equipment_code} · ${eq.equipment_name}`,
|
||||
}))}
|
||||
value={equipmentPick || ""}
|
||||
onValueChange={setEquipmentPick}
|
||||
placeholder="설비를 선택해주세요"
|
||||
disabled={addingEquipment || availableEquipments.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9" size="sm">
|
||||
<SelectValue placeholder="설비를 선택해주세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableEquipments.map((eq) => (
|
||||
<SelectItem key={eq.id} value={eq.equipment_code}>
|
||||
{eq.equipment_code} · {eq.equipment_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ClipboardList,
|
||||
ChevronRight,
|
||||
Factory,
|
||||
Keyboard,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
@@ -41,7 +40,6 @@ const TAB_META = [
|
||||
icon: Settings,
|
||||
color: "text-blue-500",
|
||||
badgeColor: "bg-blue-50 text-blue-700 ring-blue-600/20",
|
||||
shortcut: "1",
|
||||
actions: ["공정 등록", "공정 수정", "공정 삭제", "설비 연결"],
|
||||
},
|
||||
{
|
||||
@@ -53,7 +51,6 @@ const TAB_META = [
|
||||
icon: GitBranch,
|
||||
color: "text-emerald-500",
|
||||
badgeColor: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
|
||||
shortcut: "2",
|
||||
actions: ["버전 생성", "공정 순서 설정", "품목 등록", "품목 해제"],
|
||||
},
|
||||
{
|
||||
@@ -65,7 +62,6 @@ const TAB_META = [
|
||||
icon: ClipboardList,
|
||||
color: "text-violet-500",
|
||||
badgeColor: "bg-violet-50 text-violet-700 ring-violet-600/20",
|
||||
shortcut: "3",
|
||||
actions: ["기준서 등록", "기준서 수정", "기준서 삭제", "작업 표준 관리"],
|
||||
},
|
||||
] as const;
|
||||
@@ -77,7 +73,6 @@ const ACTION_ICONS = [Plus, Pencil, Trash2, List] as const;
|
||||
export default function ProcessInfoPage() {
|
||||
const ts = useTableSettings("c16-process-info", "process_mst", GRID_COLUMNS);
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("process");
|
||||
const [showShortcutHint, setShowShortcutHint] = useState(false);
|
||||
|
||||
const activeMeta = TAB_META.find((t) => t.value === activeTab)!;
|
||||
|
||||
@@ -85,19 +80,6 @@ export default function ProcessInfoPage() {
|
||||
setActiveTab(value as TabValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.altKey) return;
|
||||
const tabByShortcut = TAB_META.find((t) => t.shortcut === e.key);
|
||||
if (tabByShortcut) {
|
||||
e.preventDefault();
|
||||
setActiveTab(tabByShortcut.value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col bg-muted/30">
|
||||
{/* 페이지 헤더 */}
|
||||
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
|
||||
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={() => setShowShortcutHint((v) => !v)}
|
||||
aria-label="키보드 단축키 보기"
|
||||
>
|
||||
<Keyboard className="h-3 w-3" />
|
||||
<span>단축키</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showShortcutHint && (
|
||||
<div className="mt-2 flex items-center gap-3 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">탭 전환:</span>
|
||||
{TAB_META.map((t) => (
|
||||
<span key={t.value} className="flex items-center gap-1">
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px]">
|
||||
Alt+{t.shortcut}
|
||||
</kbd>
|
||||
<span>{t.shortLabel}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
@@ -154,12 +114,12 @@ export default function ProcessInfoPage() {
|
||||
{/* 탭 네비게이션 */}
|
||||
<div className="shrink-0 border-b bg-background px-4">
|
||||
<TabsList className="h-12 bg-transparent gap-1">
|
||||
{TAB_META.map(({ value, label, icon: Icon, shortcut }) => (
|
||||
{TAB_META.map(({ value, label, icon: Icon }) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 gap-1.5"
|
||||
aria-label={`${label} (Alt+${shortcut})`}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
@@ -168,34 +128,6 @@ export default function ProcessInfoPage() {
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 탭 설명 배너 */}
|
||||
<div className="shrink-0 border-b bg-background/60 px-6 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${activeMeta.badgeColor}`}
|
||||
>
|
||||
{activeMeta.shortLabel}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{activeMeta.detailDesc}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
{activeMeta.actions.map((action, i) => {
|
||||
const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length];
|
||||
return (
|
||||
<span
|
||||
key={action}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground/70"
|
||||
>
|
||||
<ActionIcon className="h-3 w-3" />
|
||||
{action}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
<TabsContent value="process" className="min-h-0 flex-1 overflow-hidden mt-0">
|
||||
<ProcessMasterTab />
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
import {
|
||||
getProcessList,
|
||||
createProcess,
|
||||
@@ -512,23 +513,17 @@ export function ProcessMasterTab() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">설비 선택</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
key={selectedProcess.id}
|
||||
value={equipmentPick || undefined}
|
||||
options={availableEquipments.map((eq) => ({
|
||||
code: eq.equipment_code,
|
||||
label: `${eq.equipment_code} · ${eq.equipment_name}`,
|
||||
}))}
|
||||
value={equipmentPick || ""}
|
||||
onValueChange={setEquipmentPick}
|
||||
placeholder="설비를 선택해주세요"
|
||||
disabled={addingEquipment || availableEquipments.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9" size="sm">
|
||||
<SelectValue placeholder="설비를 선택해주세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableEquipments.map((eq) => (
|
||||
<SelectItem key={eq.id} value={eq.equipment_code}>
|
||||
{eq.equipment_code} · {eq.equipment_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ClipboardList,
|
||||
ChevronRight,
|
||||
Factory,
|
||||
Keyboard,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
@@ -41,7 +40,6 @@ const TAB_META = [
|
||||
icon: Settings,
|
||||
color: "text-blue-500",
|
||||
badgeColor: "bg-blue-50 text-blue-700 ring-blue-600/20",
|
||||
shortcut: "1",
|
||||
actions: ["공정 등록", "공정 수정", "공정 삭제", "설비 연결"],
|
||||
},
|
||||
{
|
||||
@@ -53,7 +51,6 @@ const TAB_META = [
|
||||
icon: GitBranch,
|
||||
color: "text-emerald-500",
|
||||
badgeColor: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
|
||||
shortcut: "2",
|
||||
actions: ["버전 생성", "공정 순서 설정", "품목 등록", "품목 해제"],
|
||||
},
|
||||
{
|
||||
@@ -65,7 +62,6 @@ const TAB_META = [
|
||||
icon: ClipboardList,
|
||||
color: "text-violet-500",
|
||||
badgeColor: "bg-violet-50 text-violet-700 ring-violet-600/20",
|
||||
shortcut: "3",
|
||||
actions: ["기준서 등록", "기준서 수정", "기준서 삭제", "작업 표준 관리"],
|
||||
},
|
||||
] as const;
|
||||
@@ -77,7 +73,6 @@ const ACTION_ICONS = [Plus, Pencil, Trash2, List] as const;
|
||||
export default function ProcessInfoPage() {
|
||||
const ts = useTableSettings("c16-process-info", "process_mst", GRID_COLUMNS);
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("process");
|
||||
const [showShortcutHint, setShowShortcutHint] = useState(false);
|
||||
|
||||
const activeMeta = TAB_META.find((t) => t.value === activeTab)!;
|
||||
|
||||
@@ -85,19 +80,6 @@ export default function ProcessInfoPage() {
|
||||
setActiveTab(value as TabValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.altKey) return;
|
||||
const tabByShortcut = TAB_META.find((t) => t.shortcut === e.key);
|
||||
if (tabByShortcut) {
|
||||
e.preventDefault();
|
||||
setActiveTab(tabByShortcut.value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col bg-muted/30">
|
||||
{/* 페이지 헤더 */}
|
||||
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
|
||||
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={() => setShowShortcutHint((v) => !v)}
|
||||
aria-label="키보드 단축키 보기"
|
||||
>
|
||||
<Keyboard className="h-3 w-3" />
|
||||
<span>단축키</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showShortcutHint && (
|
||||
<div className="mt-2 flex items-center gap-3 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">탭 전환:</span>
|
||||
{TAB_META.map((t) => (
|
||||
<span key={t.value} className="flex items-center gap-1">
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px]">
|
||||
Alt+{t.shortcut}
|
||||
</kbd>
|
||||
<span>{t.shortLabel}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
@@ -154,12 +114,12 @@ export default function ProcessInfoPage() {
|
||||
{/* 탭 네비게이션 */}
|
||||
<div className="shrink-0 border-b bg-background px-4">
|
||||
<TabsList className="h-12 bg-transparent gap-1">
|
||||
{TAB_META.map(({ value, label, icon: Icon, shortcut }) => (
|
||||
{TAB_META.map(({ value, label, icon: Icon }) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 gap-1.5"
|
||||
aria-label={`${label} (Alt+${shortcut})`}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
@@ -168,34 +128,6 @@ export default function ProcessInfoPage() {
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 탭 설명 배너 */}
|
||||
<div className="shrink-0 border-b bg-background/60 px-6 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${activeMeta.badgeColor}`}
|
||||
>
|
||||
{activeMeta.shortLabel}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{activeMeta.detailDesc}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
{activeMeta.actions.map((action, i) => {
|
||||
const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length];
|
||||
return (
|
||||
<span
|
||||
key={action}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground/70"
|
||||
>
|
||||
<ActionIcon className="h-3 w-3" />
|
||||
{action}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
<TabsContent value="process" className="min-h-0 flex-1 overflow-hidden mt-0">
|
||||
<ProcessMasterTab />
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
import {
|
||||
getProcessList,
|
||||
createProcess,
|
||||
@@ -511,23 +512,17 @@ export function ProcessMasterTab() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">설비 선택</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
key={selectedProcess.id}
|
||||
value={equipmentPick || undefined}
|
||||
options={availableEquipments.map((eq) => ({
|
||||
code: eq.equipment_code,
|
||||
label: `${eq.equipment_code} · ${eq.equipment_name}`,
|
||||
}))}
|
||||
value={equipmentPick || ""}
|
||||
onValueChange={setEquipmentPick}
|
||||
placeholder="설비를 선택해주세요"
|
||||
disabled={addingEquipment || availableEquipments.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9" size="sm">
|
||||
<SelectValue placeholder="설비를 선택해주세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableEquipments.map((eq) => (
|
||||
<SelectItem key={eq.id} value={eq.equipment_code}>
|
||||
{eq.equipment_code} · {eq.equipment_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ClipboardList,
|
||||
ChevronRight,
|
||||
Factory,
|
||||
Keyboard,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
@@ -41,7 +40,6 @@ const TAB_META = [
|
||||
icon: Settings,
|
||||
color: "text-blue-500",
|
||||
badgeColor: "bg-blue-50 text-blue-700 ring-blue-600/20",
|
||||
shortcut: "1",
|
||||
actions: ["공정 등록", "공정 수정", "공정 삭제", "설비 연결"],
|
||||
},
|
||||
{
|
||||
@@ -53,7 +51,6 @@ const TAB_META = [
|
||||
icon: GitBranch,
|
||||
color: "text-emerald-500",
|
||||
badgeColor: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
|
||||
shortcut: "2",
|
||||
actions: ["버전 생성", "공정 순서 설정", "품목 등록", "품목 해제"],
|
||||
},
|
||||
{
|
||||
@@ -65,7 +62,6 @@ const TAB_META = [
|
||||
icon: ClipboardList,
|
||||
color: "text-violet-500",
|
||||
badgeColor: "bg-violet-50 text-violet-700 ring-violet-600/20",
|
||||
shortcut: "3",
|
||||
actions: ["기준서 등록", "기준서 수정", "기준서 삭제", "작업 표준 관리"],
|
||||
},
|
||||
] as const;
|
||||
@@ -77,7 +73,6 @@ const ACTION_ICONS = [Plus, Pencil, Trash2, List] as const;
|
||||
export default function ProcessInfoPage() {
|
||||
const ts = useTableSettings("c16-process-info", "process_mst", GRID_COLUMNS);
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("process");
|
||||
const [showShortcutHint, setShowShortcutHint] = useState(false);
|
||||
|
||||
const activeMeta = TAB_META.find((t) => t.value === activeTab)!;
|
||||
|
||||
@@ -85,19 +80,6 @@ export default function ProcessInfoPage() {
|
||||
setActiveTab(value as TabValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.altKey) return;
|
||||
const tabByShortcut = TAB_META.find((t) => t.shortcut === e.key);
|
||||
if (tabByShortcut) {
|
||||
e.preventDefault();
|
||||
setActiveTab(tabByShortcut.value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col bg-muted/30">
|
||||
{/* 페이지 헤더 */}
|
||||
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
|
||||
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={() => setShowShortcutHint((v) => !v)}
|
||||
aria-label="키보드 단축키 보기"
|
||||
>
|
||||
<Keyboard className="h-3 w-3" />
|
||||
<span>단축키</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showShortcutHint && (
|
||||
<div className="mt-2 flex items-center gap-3 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">탭 전환:</span>
|
||||
{TAB_META.map((t) => (
|
||||
<span key={t.value} className="flex items-center gap-1">
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px]">
|
||||
Alt+{t.shortcut}
|
||||
</kbd>
|
||||
<span>{t.shortLabel}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
@@ -154,12 +114,12 @@ export default function ProcessInfoPage() {
|
||||
{/* 탭 네비게이션 */}
|
||||
<div className="shrink-0 border-b bg-background px-4">
|
||||
<TabsList className="h-12 bg-transparent gap-1">
|
||||
{TAB_META.map(({ value, label, icon: Icon, shortcut }) => (
|
||||
{TAB_META.map(({ value, label, icon: Icon }) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 gap-1.5"
|
||||
aria-label={`${label} (Alt+${shortcut})`}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
@@ -168,34 +128,6 @@ export default function ProcessInfoPage() {
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 탭 설명 배너 */}
|
||||
<div className="shrink-0 border-b bg-background/60 px-6 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${activeMeta.badgeColor}`}
|
||||
>
|
||||
{activeMeta.shortLabel}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{activeMeta.detailDesc}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
{activeMeta.actions.map((action, i) => {
|
||||
const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length];
|
||||
return (
|
||||
<span
|
||||
key={action}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground/70"
|
||||
>
|
||||
<ActionIcon className="h-3 w-3" />
|
||||
{action}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
<TabsContent value="process" className="min-h-0 flex-1 overflow-hidden mt-0">
|
||||
<ProcessMasterTab />
|
||||
|
||||
@@ -47,9 +47,15 @@ export function SmartSelect({
|
||||
const [search, setSearch] = useState("");
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// code가 비어있는 옵션은 자동 제외 (Radix Select value 제약 + key 중복 방지)
|
||||
const safeOptions = useMemo(
|
||||
() => options.filter((o) => o.code !== null && o.code !== undefined && o.code !== ""),
|
||||
[options],
|
||||
);
|
||||
|
||||
const selectedLabel = useMemo(
|
||||
() => options.find((o) => o.code === value)?.label,
|
||||
[options, value],
|
||||
() => safeOptions.find((o) => o.code === value)?.label,
|
||||
[safeOptions, value],
|
||||
);
|
||||
|
||||
// 팝오버 닫힐 때 검색어 리셋
|
||||
@@ -60,9 +66,9 @@ export function SmartSelect({
|
||||
// 검색어로 옵션 필터 (대소문자 무시)
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
if (!q) return options;
|
||||
return options.filter((o) => o.label.toLowerCase().includes(q));
|
||||
}, [options, search]);
|
||||
if (!q) return safeOptions;
|
||||
return safeOptions.filter((o) => o.label.toLowerCase().includes(q));
|
||||
}, [safeOptions, search]);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: filtered.length,
|
||||
@@ -78,15 +84,15 @@ export function SmartSelect({
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [open, virtualizer, filtered.length]);
|
||||
|
||||
if (options.length < SEARCH_THRESHOLD) {
|
||||
if (safeOptions.length < SEARCH_THRESHOLD) {
|
||||
return (
|
||||
<Select value={value} onValueChange={onValueChange} disabled={disabled}>
|
||||
<SelectTrigger className={cn("h-9", className)}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>
|
||||
{safeOptions.map((o, idx) => (
|
||||
<SelectItem key={`${o.code}-${idx}`} value={o.code}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -146,7 +152,7 @@ export function SmartSelect({
|
||||
const isSelected = value === o.code;
|
||||
return (
|
||||
<button
|
||||
key={o.code}
|
||||
key={`${o.code}-${vItem.index}`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onValueChange(o.code);
|
||||
|
||||
Reference in New Issue
Block a user