diff --git a/backend-node/src/controllers/processWorkStandardController.ts b/backend-node/src/controllers/processWorkStandardController.ts index 7c38d6d9..e72f6b9f 100644 --- a/backend-node/src/controllers/processWorkStandardController.ts +++ b/backend-node/src/controllers/processWorkStandardController.ts @@ -39,16 +39,18 @@ export async function getItemsWithRouting(req: AuthenticatedRequest, res: Respon if (search) params.push(`%${search}%`); const query = ` - SELECT DISTINCT + SELECT i.id, i.${nameColumn} AS item_name, - i.${codeColumn} AS item_code + i.${codeColumn} AS item_code, + COUNT(rv.id) AS routing_count FROM ${tableName} i - INNER JOIN ${routingTable} rv ON rv.${routingFkColumn} = i.${codeColumn} + LEFT JOIN ${routingTable} rv ON rv.${routingFkColumn} = i.${codeColumn} AND rv.company_code = i.company_code WHERE i.company_code = $1 ${searchCondition} - ORDER BY i.${codeColumn} + GROUP BY i.id, i.${nameColumn}, i.${codeColumn}, i.created_date + ORDER BY i.created_date DESC NULLS LAST `; const result = await getPool().query(query, params); @@ -82,10 +84,10 @@ export async function getRoutingsWithProcesses(req: AuthenticatedRequest, res: R // 라우팅 버전 목록 const versionsQuery = ` - SELECT id, version_name, description, created_date + SELECT id, version_name, description, created_date, COALESCE(is_default, false) AS is_default FROM ${routingVersionTable} WHERE ${routingFkColumn} = $1 AND company_code = $2 - ORDER BY created_date DESC + ORDER BY is_default DESC, created_date DESC `; const versionsResult = await getPool().query(versionsQuery, [ itemCode, @@ -127,6 +129,92 @@ export async function getRoutingsWithProcesses(req: AuthenticatedRequest, res: R } } +// ============================================================ +// 기본 버전 설정 +// ============================================================ + +/** + * 라우팅 버전을 기본 버전으로 설정 + * 같은 품목의 다른 버전은 기본 해제 + */ +export async function setDefaultVersion(req: AuthenticatedRequest, res: Response) { + const pool = getPool(); + const client = await pool.connect(); + try { + const companyCode = req.user?.companyCode; + if (!companyCode) { + return res.status(401).json({ success: false, message: "인증 필요" }); + } + + const { versionId } = req.params; + const { + routingVersionTable = "item_routing_version", + routingFkColumn = "item_code", + } = req.body; + + await client.query("BEGIN"); + + const versionResult = await client.query( + `SELECT ${routingFkColumn} AS item_code FROM ${routingVersionTable} WHERE id = $1 AND company_code = $2`, + [versionId, companyCode] + ); + + if (versionResult.rowCount === 0) { + await client.query("ROLLBACK"); + return res.status(404).json({ success: false, message: "버전을 찾을 수 없습니다" }); + } + + const itemCode = versionResult.rows[0].item_code; + + await client.query( + `UPDATE ${routingVersionTable} SET is_default = false WHERE ${routingFkColumn} = $1 AND company_code = $2`, + [itemCode, companyCode] + ); + + await client.query( + `UPDATE ${routingVersionTable} SET is_default = true WHERE id = $1 AND company_code = $2`, + [versionId, companyCode] + ); + + await client.query("COMMIT"); + + logger.info("기본 버전 설정", { companyCode, versionId, itemCode }); + return res.json({ success: true, message: "기본 버전이 설정되었습니다" }); + } catch (error: any) { + await client.query("ROLLBACK"); + logger.error("기본 버전 설정 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } +} + +/** + * 기본 버전 해제 + */ +export async function unsetDefaultVersion(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user?.companyCode; + if (!companyCode) { + return res.status(401).json({ success: false, message: "인증 필요" }); + } + + const { versionId } = req.params; + const { routingVersionTable = "item_routing_version" } = req.body; + + await getPool().query( + `UPDATE ${routingVersionTable} SET is_default = false WHERE id = $1 AND company_code = $2`, + [versionId, companyCode] + ); + + logger.info("기본 버전 해제", { companyCode, versionId }); + return res.json({ success: true, message: "기본 버전이 해제되었습니다" }); + } catch (error: any) { + logger.error("기본 버전 해제 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + // ============================================================ // 작업 항목 CRUD // ============================================================ @@ -330,7 +418,10 @@ export async function getWorkItemDetails(req: AuthenticatedRequest, res: Respons const { workItemId } = req.params; const query = ` - SELECT id, work_item_id, detail_type, content, is_required, sort_order, remark, created_date + SELECT id, work_item_id, detail_type, content, is_required, sort_order, remark, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + duration_minutes, input_type, lookup_target, display_fields, + created_date FROM process_work_item_detail WHERE work_item_id = $1 AND company_code = $2 ORDER BY sort_order, created_date @@ -355,7 +446,11 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo return res.status(401).json({ success: false, message: "인증 필요" }); } - const { work_item_id, detail_type, content, is_required, sort_order, remark } = req.body; + const { + work_item_id, detail_type, content, is_required, sort_order, remark, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + duration_minutes, input_type, lookup_target, display_fields, + } = req.body; if (!work_item_id || !content) { return res.status(400).json({ @@ -375,8 +470,10 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo const query = ` INSERT INTO process_work_item_detail - (company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + (company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + duration_minutes, input_type, lookup_target, display_fields) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING * `; @@ -389,6 +486,15 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo sort_order || 0, remark || null, writer, + inspection_code || null, + inspection_method || null, + unit || null, + lower_limit || null, + upper_limit || null, + duration_minutes || null, + input_type || null, + lookup_target || null, + display_fields || null, ]); logger.info("작업 항목 상세 생성", { companyCode, id: result.rows[0].id }); @@ -410,7 +516,11 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo } const { id } = req.params; - const { detail_type, content, is_required, sort_order, remark } = req.body; + const { + detail_type, content, is_required, sort_order, remark, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + duration_minutes, input_type, lookup_target, display_fields, + } = req.body; const query = ` UPDATE process_work_item_detail @@ -419,6 +529,15 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo is_required = COALESCE($3, is_required), sort_order = COALESCE($4, sort_order), remark = COALESCE($5, remark), + inspection_code = $8, + inspection_method = $9, + unit = $10, + lower_limit = $11, + upper_limit = $12, + duration_minutes = $13, + input_type = $14, + lookup_target = $15, + display_fields = $16, updated_date = NOW() WHERE id = $6 AND company_code = $7 RETURNING * @@ -432,6 +551,15 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo remark, id, companyCode, + inspection_code || null, + inspection_method || null, + unit || null, + lower_limit || null, + upper_limit || null, + duration_minutes || null, + input_type || null, + lookup_target || null, + display_fields || null, ]); if (result.rowCount === 0) { @@ -544,8 +672,10 @@ export async function saveAll(req: AuthenticatedRequest, res: Response) { for (const detail of item.details) { await client.query( `INSERT INTO process_work_item_detail - (company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, + (company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + duration_minutes, input_type, lookup_target, display_fields) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`, [ companyCode, workItemId, @@ -555,6 +685,15 @@ export async function saveAll(req: AuthenticatedRequest, res: Response) { detail.sort_order || 0, detail.remark || null, writer, + detail.inspection_code || null, + detail.inspection_method || null, + detail.unit || null, + detail.lower_limit || null, + detail.upper_limit || null, + detail.duration_minutes || null, + detail.input_type || null, + detail.lookup_target || null, + detail.display_fields || null, ] ); } diff --git a/backend-node/src/routes/processWorkStandardRoutes.ts b/backend-node/src/routes/processWorkStandardRoutes.ts index 0c052007..7630b359 100644 --- a/backend-node/src/routes/processWorkStandardRoutes.ts +++ b/backend-node/src/routes/processWorkStandardRoutes.ts @@ -14,6 +14,10 @@ router.use(authenticateToken); router.get("/items", ctrl.getItemsWithRouting); router.get("/items/:itemCode/routings", ctrl.getRoutingsWithProcesses); +// 기본 버전 설정/해제 +router.put("/versions/:versionId/set-default", ctrl.setDefaultVersion); +router.put("/versions/:versionId/unset-default", ctrl.unsetDefaultVersion); + // 작업 항목 CRUD router.get("/routing-detail/:routingDetailId/work-items", ctrl.getWorkItems); router.post("/work-items", ctrl.createWorkItem); diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index e94b6cce..b7b4191d 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -2534,14 +2534,14 @@ export const SplitPanelLayoutComponent: React.FC {group.items.map((item, idx) => { const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id"; - const itemId = item[sourceColumn] || item.id || item.ID || idx; + const itemId = item[sourceColumn] || item.id || item.ID; const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); return ( handleLeftItemSelect(item)} className={`hover:bg-accent cursor-pointer transition-colors ${ isSelected ? "bg-primary/10" : "" @@ -2596,14 +2596,14 @@ export const SplitPanelLayoutComponent: React.FC {filteredData.map((item, idx) => { const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id"; - const itemId = item[sourceColumn] || item.id || item.ID || idx; + const itemId = item[sourceColumn] || item.id || item.ID; const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); return ( handleLeftItemSelect(item)} className={`hover:bg-accent cursor-pointer transition-colors ${ isSelected ? "bg-primary/10" : "" @@ -2698,7 +2698,8 @@ export const SplitPanelLayoutComponent: React.FC // 재귀 렌더링 함수 const renderTreeItem = (item: any, index: number): React.ReactNode => { const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id"; - const itemId = item[sourceColumn] || item.id || item.ID || index; + const rawItemId = item[sourceColumn] || item.id || item.ID; + const itemId = rawItemId != null ? rawItemId : index; const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); const hasChildren = item.children && item.children.length > 0; @@ -2749,7 +2750,7 @@ export const SplitPanelLayoutComponent: React.FC const displaySubtitle = displayFields[1]?.value || null; return ( - + {/* 현재 항목 */}
return (
{currentTabData.map((item: any, idx: number) => { - const itemId = item.id || idx; + const itemId = item.id ?? idx; const isExpanded = expandedRightItems.has(itemId); // 표시할 컬럼 결정 @@ -3097,7 +3098,7 @@ export const SplitPanelLayoutComponent: React.FC const detailColumns = columnsToShow.slice(summaryCount); return ( -
+
toggleRightItemExpansion(itemId)} @@ -3287,10 +3288,10 @@ export const SplitPanelLayoutComponent: React.FC {filteredData.map((item, idx) => { - const itemId = item.id || item.ID || idx; + const itemId = item.id || item.ID; return ( - + {columnsToShow.map((col, colIdx) => ( return (
{/* 요약 정보 */} diff --git a/frontend/lib/registry/components/v2-item-routing/ItemRoutingComponent.tsx b/frontend/lib/registry/components/v2-item-routing/ItemRoutingComponent.tsx index 492f7255..a8f752f9 100644 --- a/frontend/lib/registry/components/v2-item-routing/ItemRoutingComponent.tsx +++ b/frontend/lib/registry/components/v2-item-routing/ItemRoutingComponent.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; -import { Search, Plus, Trash2, Edit, ListOrdered, Package } from "lucide-react"; +import { Search, Plus, Trash2, Edit, ListOrdered, Package, Star } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -51,6 +51,8 @@ export function ItemRoutingComponent({ refreshDetails, deleteDetail, deleteVersion, + setDefaultVersion, + unsetDefaultVersion, } = useItemRouting(configProp || {}); const [searchText, setSearchText] = useState(""); @@ -70,16 +72,21 @@ export function ItemRoutingComponent({ }, [fetchItems]); // 모달 저장 성공 감지 -> 데이터 새로고침 + const refreshVersionsRef = React.useRef(refreshVersions); + const refreshDetailsRef = React.useRef(refreshDetails); + refreshVersionsRef.current = refreshVersions; + refreshDetailsRef.current = refreshDetails; + useEffect(() => { const handleSaveSuccess = () => { - refreshVersions(); - refreshDetails(); + refreshVersionsRef.current(); + refreshDetailsRef.current(); }; window.addEventListener("saveSuccessInModal", handleSaveSuccess); return () => { window.removeEventListener("saveSuccessInModal", handleSaveSuccess); }; - }, [refreshVersions, refreshDetails]); + }, []); // 품목 검색 const handleSearch = useCallback(() => { @@ -156,6 +163,24 @@ export function ItemRoutingComponent({ [config] ); + // 기본 버전 토글 + const handleToggleDefault = useCallback( + async (versionId: string, currentIsDefault: boolean) => { + let success: boolean; + if (currentIsDefault) { + success = await unsetDefaultVersion(versionId); + if (success) toast({ title: "기본 버전이 해제되었습니다" }); + } else { + success = await setDefaultVersion(versionId); + if (success) toast({ title: "기본 버전으로 설정되었습니다" }); + } + if (!success) { + toast({ title: "기본 버전 변경 실패", variant: "destructive" }); + } + }, + [setDefaultVersion, unsetDefaultVersion, toast] + ); + // 삭제 확인 const handleConfirmDelete = useCallback(async () => { if (!deleteTarget) return; @@ -175,12 +200,6 @@ export function ItemRoutingComponent({ setDeleteTarget(null); }, [deleteTarget, deleteVersion, deleteDetail, toast]); - // entity join으로 가져온 공정명 컬럼 이름 추정 - const processNameKey = useMemo(() => { - const ds = config.dataSource; - return `${ds.processTable}_${ds.processNameColumn}`; - }, [config.dataSource]); - const splitRatio = config.splitRatio || 40; if (isPreview) { @@ -295,34 +314,56 @@ export function ItemRoutingComponent({ 버전: {versions.map((ver) => { const isActive = selectedVersionId === ver.id; + const isDefault = ver.is_default === true; return (
selectVersion(ver.id)} > + {isDefault && } {ver[config.dataSource.routingVersionNameColumn] || ver.version_name || ver.id} {!config.readonly && ( - + <> + + + )}
); @@ -394,11 +435,11 @@ export function ItemRoutingComponent({ {config.processColumns.map((col) => { let cellValue = detail[col.name]; - if ( - col.name === "process_code" && - detail[processNameKey] - ) { - cellValue = `${detail[col.name]} (${detail[processNameKey]})`; + if (cellValue == null) { + const aliasKey = Object.keys(detail).find( + (k) => k.endsWith(`_${col.name}`) + ); + if (aliasKey) cellValue = detail[aliasKey]; } return ( ) { [configKey] ); - // 공정 상세 목록 조회 (특정 버전의 공정들) + // 공정 상세 목록 조회 (특정 버전의 공정들) - entity join 포함 const fetchDetails = useCallback( async (versionId: string) => { try { setLoading(true); const ds = configRef.current.dataSource; - const res = await apiClient.get("/table-data/entity-join-api/data-with-joins", { - params: { - tableName: ds.routingDetailTable, - searchConditions: JSON.stringify({ - [ds.routingDetailFkColumn]: { - value: versionId, - operator: "equals", - }, - }), - sortColumn: "seq_no", - sortDirection: "ASC", - }, + const searchConditions = { + [ds.routingDetailFkColumn]: { value: versionId, operator: "equals" }, + }; + const params = new URLSearchParams({ + page: "1", + size: "1000", + search: JSON.stringify(searchConditions), + sortBy: "seq_no", + sortOrder: "ASC", + enableEntityJoin: "true", }); + const res = await apiClient.get( + `/table-management/tables/${ds.routingDetailTable}/data-with-joins?${params}` + ); if (res.data?.success) { - setDetails(res.data.data || []); + const result = res.data.data; + setDetails(Array.isArray(result) ? result : result?.data || []); } } catch (err) { console.error("공정 상세 조회 실패", err); @@ -136,14 +138,17 @@ export function useItemRouting(configPartial: Partial) { const versionList = await fetchVersions(itemCode); - // 첫번째 버전 자동 선택 - if (config.autoSelectFirstVersion && versionList.length > 0) { - const firstVersion = versionList[0]; - setSelectedVersionId(firstVersion.id); - await fetchDetails(firstVersion.id); + if (versionList.length > 0) { + // 기본 버전 우선, 없으면 첫번째 버전 선택 + const defaultVersion = versionList.find((v: RoutingVersionData) => v.is_default); + const targetVersion = defaultVersion || (configRef.current.autoSelectFirstVersion ? versionList[0] : null); + if (targetVersion) { + setSelectedVersionId(targetVersion.id); + await fetchDetails(targetVersion.id); + } } }, - [fetchVersions, fetchDetails, config.autoSelectFirstVersion] + [fetchVersions, fetchDetails] ); // 버전 선택 @@ -181,7 +186,8 @@ export function useItemRouting(configPartial: Partial) { try { const ds = configRef.current.dataSource; const res = await apiClient.delete( - `/table-data/${ds.routingDetailTable}/${detailId}` + `/table-management/tables/${ds.routingDetailTable}/delete`, + { data: [{ id: detailId }] } ); if (res.data?.success) { await refreshDetails(); @@ -201,7 +207,8 @@ export function useItemRouting(configPartial: Partial) { try { const ds = configRef.current.dataSource; const res = await apiClient.delete( - `/table-data/${ds.routingVersionTable}/${versionId}` + `/table-management/tables/${ds.routingVersionTable}/delete`, + { data: [{ id: versionId }] } ); if (res.data?.success) { if (selectedVersionId === versionId) { @@ -219,6 +226,51 @@ export function useItemRouting(configPartial: Partial) { [selectedVersionId, refreshVersions] ); + // 기본 버전 설정 + const setDefaultVersion = useCallback( + async (versionId: string) => { + try { + const ds = configRef.current.dataSource; + const res = await apiClient.put(`${API_BASE}/versions/${versionId}/set-default`, { + routingVersionTable: ds.routingVersionTable, + routingFkColumn: ds.routingVersionFkColumn, + }); + if (res.data?.success) { + if (selectedItemCode) { + await fetchVersions(selectedItemCode); + } + return true; + } + } catch (err) { + console.error("기본 버전 설정 실패", err); + } + return false; + }, + [selectedItemCode, fetchVersions] + ); + + // 기본 버전 해제 + const unsetDefaultVersion = useCallback( + async (versionId: string) => { + try { + const ds = configRef.current.dataSource; + const res = await apiClient.put(`${API_BASE}/versions/${versionId}/unset-default`, { + routingVersionTable: ds.routingVersionTable, + }); + if (res.data?.success) { + if (selectedItemCode) { + await fetchVersions(selectedItemCode); + } + return true; + } + } catch (err) { + console.error("기본 버전 해제 실패", err); + } + return false; + }, + [selectedItemCode, fetchVersions] + ); + return { config, items, @@ -235,5 +287,7 @@ export function useItemRouting(configPartial: Partial) { refreshDetails, deleteDetail, deleteVersion, + setDefaultVersion, + unsetDefaultVersion, }; } diff --git a/frontend/lib/registry/components/v2-item-routing/types.ts b/frontend/lib/registry/components/v2-item-routing/types.ts index e5b1aa38..06b108da 100644 --- a/frontend/lib/registry/components/v2-item-routing/types.ts +++ b/frontend/lib/registry/components/v2-item-routing/types.ts @@ -65,6 +65,7 @@ export interface ItemData { export interface RoutingVersionData { id: string; version_name: string; + is_default?: boolean; [key: string]: any; } diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx new file mode 100644 index 00000000..d9828aa0 --- /dev/null +++ b/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx @@ -0,0 +1,445 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { WorkItemDetail, DetailTypeDefinition, InspectionStandard } from "../types"; +import { InspectionStandardLookup } from "./InspectionStandardLookup"; + +interface DetailFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: Partial) => void; + detailTypes: DetailTypeDefinition[]; + editData?: WorkItemDetail | null; + mode: "add" | "edit"; +} + +const LOOKUP_TARGETS = [ + { value: "equipment", label: "설비정보" }, + { value: "material", label: "자재정보" }, + { value: "worker", label: "작업자정보" }, + { value: "tool", label: "공구정보" }, + { value: "document", label: "문서정보" }, +]; + +const INPUT_TYPES = [ + { value: "text", label: "텍스트" }, + { value: "number", label: "숫자" }, + { value: "date", label: "날짜" }, + { value: "textarea", label: "장문텍스트" }, + { value: "select", label: "선택형" }, +]; + +export function DetailFormModal({ + open, + onClose, + onSubmit, + detailTypes, + editData, + mode, +}: DetailFormModalProps) { + const [formData, setFormData] = useState>({}); + const [inspectionLookupOpen, setInspectionLookupOpen] = useState(false); + const [selectedInspection, setSelectedInspection] = useState(null); + + useEffect(() => { + if (open) { + if (mode === "edit" && editData) { + setFormData({ ...editData }); + if (editData.inspection_code) { + setSelectedInspection({ + id: "", + inspection_code: editData.inspection_code, + inspection_item: editData.content || "", + inspection_method: editData.inspection_method || "", + unit: editData.unit || "", + lower_limit: editData.lower_limit || "", + upper_limit: editData.upper_limit || "", + }); + } + } else { + setFormData({ + detail_type: detailTypes[0]?.value || "", + content: "", + is_required: "Y", + }); + setSelectedInspection(null); + } + } + }, [open, mode, editData, detailTypes]); + + const updateField = (field: string, value: any) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleInspectionSelect = (item: InspectionStandard) => { + setSelectedInspection(item); + setFormData((prev) => ({ + ...prev, + inspection_code: item.inspection_code, + content: item.inspection_item, + inspection_method: item.inspection_method, + unit: item.unit, + lower_limit: item.lower_limit || "", + upper_limit: item.upper_limit || "", + })); + }; + + const handleSubmit = () => { + if (!formData.detail_type) return; + + const type = formData.detail_type; + + if (type === "check" && !formData.content?.trim()) return; + if (type === "inspect" && !formData.content?.trim()) return; + if (type === "procedure" && !formData.content?.trim()) return; + if (type === "input" && !formData.content?.trim()) return; + if (type === "info" && !formData.lookup_target) return; + + onSubmit(formData); + onClose(); + }; + + const currentType = formData.detail_type || ""; + + return ( + <> + !v && onClose()}> + + + + 상세 항목 {mode === "add" ? "추가" : "수정"} + + + 상세 항목의 유형을 선택하고 내용을 입력하세요 + + + +
+ {/* 유형 선택 */} +
+ + +
+ + {/* 체크리스트 */} + {currentType === "check" && ( + <> +
+ + updateField("content", e.target.value)} + placeholder="예: 전원 상태 확인" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ + )} + + {/* 검사항목 */} + {currentType === "inspect" && ( + <> +
+ +
+ + +
+
+ + {selectedInspection && ( +
+

+ 선택된 검사기준 정보 +

+
+

+ 검사코드: {selectedInspection.inspection_code} +

+

+ 검사항목: {selectedInspection.inspection_item} +

+

+ 검사방법: {selectedInspection.inspection_method || "-"} +

+

+ 단위: {selectedInspection.unit || "-"} +

+

+ 하한값: {selectedInspection.lower_limit || "-"} +

+

+ 상한값: {selectedInspection.upper_limit || "-"} +

+
+
+ )} + +
+ + updateField("content", e.target.value)} + placeholder="예: 외경 치수" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ +
+
+ + updateField("inspection_method", e.target.value)} + placeholder="예: 마이크로미터" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+
+ + updateField("unit", e.target.value)} + placeholder="예: mm" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+
+ +
+
+ + updateField("lower_limit", e.target.value)} + placeholder="예: 7.95" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+
+ + updateField("upper_limit", e.target.value)} + placeholder="예: 8.05" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+
+ + )} + + {/* 작업절차 */} + {currentType === "procedure" && ( + <> +
+ + updateField("content", e.target.value)} + placeholder="예: 자재 투입" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+
+ + + updateField( + "duration_minutes", + e.target.value ? Number(e.target.value) : undefined + ) + } + placeholder="예: 5" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ + )} + + {/* 직접입력 */} + {currentType === "input" && ( + <> +
+ + updateField("content", e.target.value)} + placeholder="예: 작업자 의견" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+
+ + +
+ + )} + + {/* 정보조회 */} + {currentType === "info" && ( + <> +
+ + +
+
+ + updateField("display_fields", e.target.value)} + placeholder="예: 설비명, 설비코드" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ + )} + + {/* 필수 여부 (모든 유형 공통) */} + {currentType && ( +
+ + +
+ )} +
+ + + + + +
+
+ + setInspectionLookupOpen(false)} + onSelect={handleInspectionSelect} + /> + + ); +} diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/InspectionStandardLookup.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/InspectionStandardLookup.tsx new file mode 100644 index 00000000..75094d58 --- /dev/null +++ b/frontend/lib/registry/components/v2-process-work-standard/components/InspectionStandardLookup.tsx @@ -0,0 +1,187 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { Search, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { apiClient } from "@/lib/api/client"; +import { InspectionStandard } from "../types"; + +interface InspectionStandardLookupProps { + open: boolean; + onClose: () => void; + onSelect: (item: InspectionStandard) => void; +} + +export function InspectionStandardLookup({ + open, + onClose, + onSelect, +}: InspectionStandardLookupProps) { + const [data, setData] = useState([]); + const [searchText, setSearchText] = useState(""); + const [loading, setLoading] = useState(false); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + if (searchText.trim()) { + search.inspection_item = searchText.trim(); + search.inspection_code = searchText.trim(); + } + const params = new URLSearchParams({ + page: "1", + size: "100", + enableEntityJoin: "true", + ...(searchText.trim() ? { search: JSON.stringify(search) } : {}), + }); + const res = await apiClient.get( + `/table-management/tables/inspection_standard/data-with-joins?${params}` + ); + if (res.data?.success) { + const result = res.data.data; + setData(Array.isArray(result) ? result : result?.data || []); + } + } catch (err) { + console.error("검사기준 조회 실패", err); + } finally { + setLoading(false); + } + }, [searchText]); + + useEffect(() => { + if (open) { + fetchData(); + } + }, [open, fetchData]); + + const handleSelect = (item: any) => { + onSelect({ + id: item.id, + inspection_code: item.inspection_code || "", + inspection_item: item.inspection_item || item.inspection_criteria || "", + inspection_method: item.inspection_method || "", + unit: item.unit || "", + lower_limit: item.lower_limit || "", + upper_limit: item.upper_limit || "", + }); + onClose(); + }; + + return ( + !v && onClose()}> + + + + + 검사기준 조회 + + + 검사기준을 검색하여 선택하세요 + + + +
+
+ setSearchText(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && fetchData()} + className="h-9 text-sm" + /> +
+ +
+ + + + + + + + + + + + + + {loading ? ( + + + + ) : data.length === 0 ? ( + + + + ) : ( + data.map((item, idx) => ( + + + + + + + + + + )) + )} + +
+ 검사코드 + + 검사항목 + + 검사방법 + + 하한 + + 상한 + + 단위 + + 선택 +
+ 조회 중... +
+ 검사기준이 없습니다 +
{item.inspection_code || "-"} + {item.inspection_item || item.inspection_criteria || "-"} + {item.inspection_method || "-"} + {item.lower_limit || "-"} + + {item.upper_limit || "-"} + {item.unit || "-"} + +
+
+
+ + + + +
+
+ ); +} diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/ItemProcessSelector.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/ItemProcessSelector.tsx index 59ea4f71..689d006d 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/components/ItemProcessSelector.tsx +++ b/frontend/lib/registry/components/v2-process-work-standard/components/ItemProcessSelector.tsx @@ -87,7 +87,7 @@ export function ItemProcessSelector({
) : ( items.map((item) => ( -
+
{/* 품목 헤더 */} - -
- - - ) : ( - <> - - {idx + 1} - - - - {getTypeLabel(detail.detail_type)} - - - {detail.content} - - - {detail.is_required === "Y" ? "필수" : "선택"} - - - {!readonly && ( - -
- - -
- - )} - + + +
+ )} ))} - - {/* 추가 행 */} - {isAdding && ( - - - {details.length + 1} - - - - - - - setNewData((prev) => ({ - ...prev, - content: e.target.value, - })) - } - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - className="h-7 text-xs" - /> - - - - - -
- - -
- - - )} - {details.length === 0 && !isAdding && ( + {details.length === 0 && (

상세 항목이 없습니다. "상세 추가" 버튼을 클릭하여 추가하세요. @@ -375,6 +205,16 @@ export function WorkItemDetailList({

)}
+ + {/* 추가/수정 모달 */} + setModalOpen(false)} + onSubmit={handleSubmit} + detailTypes={detailTypes} + editData={editTarget} + mode={modalMode} + />
); } diff --git a/frontend/lib/registry/components/v2-process-work-standard/config.ts b/frontend/lib/registry/components/v2-process-work-standard/config.ts index bef2ed6d..8c73ffd7 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/config.ts +++ b/frontend/lib/registry/components/v2-process-work-standard/config.ts @@ -23,9 +23,11 @@ export const defaultConfig: ProcessWorkStandardConfig = { { key: "POST", label: "작업 후 (Post-Work)", sortOrder: 3 }, ], detailTypes: [ - { value: "CHECK", label: "체크" }, - { value: "INSPECTION", label: "검사" }, - { value: "MEASUREMENT", label: "측정" }, + { value: "check", label: "체크리스트" }, + { value: "inspect", label: "검사항목" }, + { value: "procedure", label: "작업절차" }, + { value: "input", label: "직접입력" }, + { value: "info", label: "정보조회" }, ], splitRatio: 30, leftPanelTitle: "품목 및 공정 선택", diff --git a/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts b/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts index ca9d8019..759eb9c7 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts +++ b/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts @@ -11,7 +11,7 @@ import { SelectionState, } from "../types"; -const API_BASE = "/api/process-work-standard"; +const API_BASE = "/process-work-standard"; export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { const [items, setItems] = useState([]); diff --git a/frontend/lib/registry/components/v2-process-work-standard/types.ts b/frontend/lib/registry/components/v2-process-work-standard/types.ts index f2668ada..6d2b0bea 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/types.ts +++ b/frontend/lib/registry/components/v2-process-work-standard/types.ts @@ -87,6 +87,29 @@ export interface WorkItemDetail { sort_order: number; remark?: string; created_date?: string; + // 검사항목 전용 + inspection_code?: string; + inspection_method?: string; + unit?: string; + lower_limit?: string; + upper_limit?: string; + // 작업절차 전용 + duration_minutes?: number; + // 직접입력 전용 + input_type?: string; + // 정보조회 전용 + lookup_target?: string; + display_fields?: string; +} + +export interface InspectionStandard { + id: string; + inspection_code: string; + inspection_item: string; + inspection_method: string; + unit: string; + lower_limit?: string; + upper_limit?: string; } // ============================================================