diff --git a/frontend/app/(main)/COMPANY_10/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_10/production/process-info/ProcessMasterTab.tsx index 207fa3ad..deb8a99c 100644 --- a/frontend/app/(main)/COMPANY_10/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_10/production/process-info/ProcessMasterTab.tsx @@ -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() {
- + />
-
- {showShortcutHint && ( -
- 탭 전환: - {TAB_META.map((t) => ( - - - Alt+{t.shortcut} - - {t.shortLabel} - - ))} -
- )} - {TAB_META.map(({ value, label, icon: Icon, shortcut }) => ( + {TAB_META.map(({ value, label, icon: Icon }) => ( {label} @@ -168,34 +128,6 @@ export default function ProcessInfoPage() { - {/* 탭 설명 배너 */} -
-
-
- - {activeMeta.shortLabel} - - {activeMeta.detailDesc} -
-
- {activeMeta.actions.map((action, i) => { - const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length]; - return ( - - - {action} - - ); - })} -
-
-
- {/* 탭 컨텐츠 */} diff --git a/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx index 207fa3ad..deb8a99c 100644 --- a/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx @@ -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() {
- + />
-
- {showShortcutHint && ( -
- 탭 전환: - {TAB_META.map((t) => ( - - - Alt+{t.shortcut} - - {t.shortLabel} - - ))} -
- )} - {TAB_META.map(({ value, label, icon: Icon, shortcut }) => ( + {TAB_META.map(({ value, label, icon: Icon }) => ( {label} @@ -168,34 +128,6 @@ export default function ProcessInfoPage() { - {/* 탭 설명 배너 */} -
-
-
- - {activeMeta.shortLabel} - - {activeMeta.detailDesc} -
-
- {activeMeta.actions.map((action, i) => { - const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length]; - return ( - - - {action} - - ); - })} -
-
-
- {/* 탭 컨텐츠 */} diff --git a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx index 15118ffc..80d61947 100644 --- a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx @@ -98,6 +98,11 @@ export default function ItemInspectionInfoPage() { const [inspectionRows, setInspectionRows] = useState>({}); const [collapsedTypes, setCollapsedTypes] = useState>({}); + // 복사 모달: 편집 가능한 기준 데이터 상태 (등록/수정 폼과 평행 구조) + const [copyForm, setCopyForm] = useState>({}); + const [copyInspectionRows, setCopyInspectionRows] = useState>({}); + const [copyCollapsedTypes, setCopyCollapsedTypes] = useState>({}); + // 기본 라우팅 공정 목록 (적용공정 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 = {}; + const typeFlags: Record = {}; + 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() { - {/* ═══════════════════ 복사 모달 ═══════════════════ */} + {/* ═══════════════════ 복사 모달 (2단 분할: 좌 대상 / 우 편집) ═══════════════════ */} { if (!copying) setCopyModalOpen(v); }}> e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} onEscapeKeyDown={(e) => { if (copying) e.preventDefault(); }} > - + {copying ? "검사정보 복사 중..." : "검사정보 복사"} {selectedGroup?.item_name || "-"} ({selectedItemCode}) - {copying ? " 의 검사정보를 복사하고 있습니다" : " 의 검사정보를 아래 선택한 품목들에 복사합니다"} + {copying ? " 의 검사정보를 복사하고 있습니다" : " 의 검사정보를 편집해서 선택한 품목들에 복사합니다. 기준 품목은 변경되지 않아요"} {copying ? ( @@ -1322,81 +1432,225 @@ export default function ItemInspectionInfoPage() {

- ) : (<> -
- setCopySearchKeyword(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") handleCopySearch(); }} /> - -
-
- - - - - 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))); - }} - /> - - 품목코드 - 품목명 - 품목유형 - 단위 - - - - {copyFilteredItems.length === 0 ? ( - - {copySearchLoading ? "검색 중..." : "검색 결과가 없어요"} - - ) : copyFilteredItems.map((item) => ( - toggleCopyChecked(item.code)}> - e.stopPropagation()}> - toggleCopyChecked(item.code)} /> - - {item.code} - {item.name} - {item.item_type} - {item.unit} - - ))} - -
-
-
- - 전체 {copyTotal.toLocaleString()}건 - {copyCheckedIds.length > 0 && 선택 {copyCheckedIds.length}} - -
- - - {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 ( - - ); - })} - - + ) : ( +
+ {/* 좌측: 복사 대상 품목 선택 */} +
+
+ 복사 대상 품목 선택 + {copyCheckedIds.length > 0 && 선택 {copyCheckedIds.length}건} +
+
+ setCopySearchKeyword(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") handleCopySearch(); }} /> + +
+
+ + + + + 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))); + }} + /> + + 품목코드 + 품목명 + + + + {copyFilteredItems.length === 0 ? ( + + {copySearchLoading ? "검색 중..." : "검색 결과가 없어요"} + + ) : copyFilteredItems.map((item) => ( + toggleCopyChecked(item.code)}> + e.stopPropagation()}> + toggleCopyChecked(item.code)} /> + + {item.code} + {item.name} + + ))} + +
+
+
+ 전체 {copyTotal.toLocaleString()} +
+ + + {copyPage}/{copyTotalPages} + + +
+
+
+ + {/* 우측: 편집 폼 (등록/수정 폼과 동일 구조) */} +
+
+ 복사할 검사정보 편집 (기준: {selectedItemCode}) +
+
+
+
+ + +
+
+ + +
+
+ +
+

검사유형 선택

+
+ {INSPECTION_TYPES.map(({ key, label }) => ( +
+ setCopyForm(p => ({ ...p, [key]: !!v }))} /> + +
+ ))} +
+
+ + {INSPECTION_TYPES.filter(t => !!copyForm[t.key]).map(({ key, label }) => ( +
+ + {!copyCollapsedTypes[key] && ( +
+
+ 검사항목 목록 + +
+
+ + + + 검사기준 선택 + 검사기준 상세 + 검사방법 + 적용공정 + 판단기준 + 합격기준 + 필수 + 단위 + + + + + {(!copyInspectionRows[key] || copyInspectionRows[key].length === 0) ? ( + 항목추가 버튼으로 검사항목을 추가하세요 + ) : copyInspectionRows[key].map((row) => ( + + + + + + + + {processOptions.length > 0 ? ( + + ) : ( + updateCopyInspRow(key, row.id, "apply_process", e.target.value)} placeholder="공정" /> + )} + + + {row.judgment_criteria ? {row.judgment_criteria} : -} + + + {row.judgment_criteria === "선택형" && row.selection_options ? ( + + ) : row.judgment_criteria === "O/X" ? ( + + ) : row.judgment_criteria === "수치(범위)" ? ( +
+ { + 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} /> + ± + { + 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} /> +
+ ) : ( + updateCopyInspRow(key, row.id, "acceptance_criteria", e.target.value)} placeholder="합격기준" disabled={!row.inspection_standard_id} /> + )} +
+ updateCopyInspRow(key, row.id, "is_required", !!v)} /> + {row.unit || "-"} + + + +
+ ))} +
+
+
+
+ )} +
+ ))} +
+
-
- )} + )} -
- {showShortcutHint && ( -
- 탭 전환: - {TAB_META.map((t) => ( - - - Alt+{t.shortcut} - - {t.shortLabel} - - ))} -
- )} - {TAB_META.map(({ value, label, icon: Icon, shortcut }) => ( + {TAB_META.map(({ value, label, icon: Icon }) => ( {label} @@ -168,34 +128,6 @@ export default function ProcessInfoPage() { - {/* 탭 설명 배너 */} -
-
-
- - {activeMeta.shortLabel} - - {activeMeta.detailDesc} -
-
- {activeMeta.actions.map((action, i) => { - const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length]; - return ( - - - {action} - - ); - })} -
-
-
- {/* 탭 컨텐츠 */} diff --git a/frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx index 207fa3ad..deb8a99c 100644 --- a/frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx @@ -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() {
- + />
-
- {showShortcutHint && ( -
- 탭 전환: - {TAB_META.map((t) => ( - - - Alt+{t.shortcut} - - {t.shortLabel} - - ))} -
- )} - {TAB_META.map(({ value, label, icon: Icon, shortcut }) => ( + {TAB_META.map(({ value, label, icon: Icon }) => ( {label} @@ -168,34 +128,6 @@ export default function ProcessInfoPage() { - {/* 탭 설명 배너 */} -
-
-
- - {activeMeta.shortLabel} - - {activeMeta.detailDesc} -
-
- {activeMeta.actions.map((action, i) => { - const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length]; - return ( - - - {action} - - ); - })} -
-
-
- {/* 탭 컨텐츠 */} diff --git a/frontend/app/(main)/COMPANY_7/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_7/production/process-info/ProcessMasterTab.tsx index 207fa3ad..deb8a99c 100644 --- a/frontend/app/(main)/COMPANY_7/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_7/production/process-info/ProcessMasterTab.tsx @@ -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() {
- + />
-
- {showShortcutHint && ( -
- 탭 전환: - {TAB_META.map((t) => ( - - - Alt+{t.shortcut} - - {t.shortLabel} - - ))} -
- )} - {TAB_META.map(({ value, label, icon: Icon, shortcut }) => ( + {TAB_META.map(({ value, label, icon: Icon }) => ( {label} @@ -168,34 +128,6 @@ export default function ProcessInfoPage() { - {/* 탭 설명 배너 */} -
-
-
- - {activeMeta.shortLabel} - - {activeMeta.detailDesc} -
-
- {activeMeta.actions.map((action, i) => { - const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length]; - return ( - - - {action} - - ); - })} -
-
-
- {/* 탭 컨텐츠 */} diff --git a/frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx index ab75ae0d..2cfef9ef 100644 --- a/frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx @@ -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() {
- + />
-
- {showShortcutHint && ( -
- 탭 전환: - {TAB_META.map((t) => ( - - - Alt+{t.shortcut} - - {t.shortLabel} - - ))} -
- )} - {TAB_META.map(({ value, label, icon: Icon, shortcut }) => ( + {TAB_META.map(({ value, label, icon: Icon }) => ( {label} @@ -168,34 +128,6 @@ export default function ProcessInfoPage() { - {/* 탭 설명 배너 */} -
-
-
- - {activeMeta.shortLabel} - - {activeMeta.detailDesc} -
-
- {activeMeta.actions.map((action, i) => { - const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length]; - return ( - - - {action} - - ); - })} -
-
-
- {/* 탭 컨텐츠 */} diff --git a/frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx index 207fa3ad..deb8a99c 100644 --- a/frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx @@ -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() {
- + />
-
- {showShortcutHint && ( -
- 탭 전환: - {TAB_META.map((t) => ( - - - Alt+{t.shortcut} - - {t.shortLabel} - - ))} -
- )} - {TAB_META.map(({ value, label, icon: Icon, shortcut }) => ( + {TAB_META.map(({ value, label, icon: Icon }) => ( {label} @@ -168,34 +128,6 @@ export default function ProcessInfoPage() { - {/* 탭 설명 배너 */} -
-
-
- - {activeMeta.shortLabel} - - {activeMeta.detailDesc} -
-
- {activeMeta.actions.map((action, i) => { - const ActionIcon = ACTION_ICONS[i % ACTION_ICONS.length]; - return ( - - - {action} - - ); - })} -
-
-
- {/* 탭 컨텐츠 */} diff --git a/frontend/components/common/SmartSelect.tsx b/frontend/components/common/SmartSelect.tsx index fb65d6f5..52948708 100644 --- a/frontend/components/common/SmartSelect.tsx +++ b/frontend/components/common/SmartSelect.tsx @@ -47,9 +47,15 @@ export function SmartSelect({ const [search, setSearch] = useState(""); const scrollRef = useRef(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 (