From e0b89036d0321dc364bdab446ce6cad3c339da1e Mon Sep 17 00:00:00 2001
From: kjs
Date: Tue, 21 Apr 2026 10:31:40 +0900
Subject: [PATCH] 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.
---
.../process-info/ProcessMasterTab.tsx | 23 +-
.../production/process-info/page.tsx | 74 +--
.../process-info/ProcessMasterTab.tsx | 23 +-
.../production/process-info/page.tsx | 74 +--
.../quality/item-inspection/page.tsx | 424 ++++++++++++++----
.../process-info/ProcessMasterTab.tsx | 23 +-
.../production/process-info/page.tsx | 74 +--
.../process-info/ProcessMasterTab.tsx | 23 +-
.../production/process-info/page.tsx | 74 +--
.../process-info/ProcessMasterTab.tsx | 23 +-
.../production/process-info/page.tsx | 74 +--
.../process-info/ProcessMasterTab.tsx | 23 +-
.../production/process-info/page.tsx | 74 +--
.../process-info/ProcessMasterTab.tsx | 23 +-
.../production/process-info/page.tsx | 74 +--
frontend/components/common/SmartSelect.tsx | 24 +-
16 files changed, 438 insertions(+), 689 deletions(-)
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() {
-
+ />
("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 (
{/* 페이지 헤더 */}
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
ts.setOpen(true)} title="테이블 설정">
- setShowShortcutHint((v) => !v)}
- aria-label="키보드 단축키 보기"
- >
-
- 단축키
-
- {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단 분할: 좌 대상 / 우 편집) ═══════════════════ */}
- ) : (<>
-
- setCopySearchKeyword(e.target.value)}
- onKeyDown={(e) => { if (e.key === "Enter") handleCopySearch(); }} />
-
- {copySearchLoading ? : <>검색>}
-
-
-
-
-
-
-
- 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}건}
-
-
-
{ 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">
-
{ 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">
- {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 (
-
{ 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}
- );
- })}
-
{ 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">
-
{ 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">
+ ) : (
+
+ {/* 좌측: 복사 대상 품목 선택 */}
+
+
+ 복사 대상 품목 선택
+ {copyCheckedIds.length > 0 && 선택 {copyCheckedIds.length}건}
+
+
+ setCopySearchKeyword(e.target.value)}
+ onKeyDown={(e) => { if (e.key === "Enter") handleCopySearch(); }} />
+
+ {copySearchLoading ? : }
+
+
+
+
+
+
+
+ 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()}건
+
+ { 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">
+ { 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">
+ {copyPage}/{copyTotalPages}
+ { 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">
+ { 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">
+
+
+
+
+ {/* 우측: 편집 폼 (등록/수정 폼과 동일 구조) */}
+
+
+ 복사할 검사정보 편집 (기준: {selectedItemCode})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
검사유형 선택
+
+ {INSPECTION_TYPES.map(({ key, label }) => (
+
+ setCopyForm(p => ({ ...p, [key]: !!v }))} />
+
+
+ ))}
+
+
+
+ {INSPECTION_TYPES.filter(t => !!copyForm[t.key]).map(({ key, label }) => (
+
+
toggleCopyCollapse(key)}>
+ {label}
+ 검사항목 설정
+ {(copyInspectionRows[key] || []).length}개
+
+ {!copyCollapsedTypes[key] && (
+
+
+
검사항목 목록
+
addCopyInspRow(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 || "-"}
+
+ removeCopyInspRow(key, row.id)}>
+
+
+ ))}
+
+
+
+
+ )}
+
+ ))}
+
+
-
- >)}
+ )}
setCopyModalOpen(false)} disabled={copying}>취소
diff --git a/frontend/app/(main)/COMPANY_29/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_29/production/process-info/ProcessMasterTab.tsx
index 207fa3ad..deb8a99c 100644
--- a/frontend/app/(main)/COMPANY_29/production/process-info/ProcessMasterTab.tsx
+++ b/frontend/app/(main)/COMPANY_29/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() {
-
+ />
("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 (
{/* 페이지 헤더 */}
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
ts.setOpen(true)} title="테이블 설정">
- setShowShortcutHint((v) => !v)}
- aria-label="키보드 단축키 보기"
- >
-
- 단축키
-
- {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() {
-
+ />
("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 (
{/* 페이지 헤더 */}
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
ts.setOpen(true)} title="테이블 설정">
- setShowShortcutHint((v) => !v)}
- aria-label="키보드 단축키 보기"
- >
-
- 단축키
-
- {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() {
-
+ />
("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 (
{/* 페이지 헤더 */}
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
ts.setOpen(true)} title="테이블 설정">
- setShowShortcutHint((v) => !v)}
- aria-label="키보드 단축키 보기"
- >
-
- 단축키
-
- {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() {
-
+ />
("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 (
{/* 페이지 헤더 */}
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
ts.setOpen(true)} title="테이블 설정">
- setShowShortcutHint((v) => !v)}
- aria-label="키보드 단축키 보기"
- >
-
- 단축키
-
- {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() {
-
+ />
("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 (
{/* 페이지 헤더 */}
@@ -120,30 +102,8 @@ export default function ProcessInfoPage() {
ts.setOpen(true)} title="테이블 설정">
- setShowShortcutHint((v) => !v)}
- aria-label="키보드 단축키 보기"
- >
-
- 단축키
-
- {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 (