+ 다른 테이블의 카테고리 값 참조 시 입력 +
+화면 데이터가 없습니다.
@@ -1273,7 +1288,7 @@ export const ScreenModal: React.FC- 규칙 사이에 들어갈 문자입니다 -
-규칙을 추가하여 코드를 구성하세요
데이터 바인딩
-- Phase 4에서 구현 예정 -
-연결 없음
++ 이 컴포넌트는 다른 컴포넌트와 연결할 수 없습니다 +
+연결 수정
+ +새 연결 추가
+ )} + + {/* 보내는 값 */} +필터할 컬럼
+ + {dbColumnsLoading ? ( +화면 표시 컬럼
+ {displayColumns.map((col) => ( +데이터 전용 컬럼
+ {dataOnlyColumns.map((col) => ( ++ {filterColumns.length}개 컬럼 중 하나라도 일치하면 표시 +
+ )} + + {/* 필터 방식 */} +필터 방식
+ ++ {r.description} +
+ )} +연결된 소스
+ {incoming.map((conn) => { + const sourceComp = allComponents.find( + (c) => c.id === conn.sourceComponent + ); + return ( ++ 아직 연결된 소스가 없습니다. 보내는 컴포넌트에서 연결을 설정하세요. +
+ )} +화면 데이터가 없습니다.
diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 30fc5539..a35c5ed2 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -571,8 +571,38 @@ export const InteractiveScreenViewerDynamic: React.FC테이블, 반복 필드 그룹 등 데이터를 제공하는 컴포넌트
++ 레이어별로 다른 테이블이 있을 경우 "자동 탐색"을 선택하면 현재 활성화된 레이어의 테이블을 자동으로 사용합니다 +
타겟 테이블에 저장될 필드명
+추가 데이터가 저장될 타겟 테이블 컬럼
- 소스 필드를 타겟 필드에 매핑합니다. 비워두면 같은 이름의 필드로 자동 매핑됩니다. + 여러 소스 테이블에서 데이터를 전달할 때, 각 테이블별로 매핑 규칙을 설정합니다. 런타임에 소스 테이블을 자동 감지합니다.
- {!config.action?.dataTransfer?.sourceTable || !config.action?.dataTransfer?.targetTable ? ( + {!config.action?.dataTransfer?.targetTable ? (먼저 소스 테이블과 타겟 테이블을 선택하세요.
+먼저 타겟 테이블을 선택하세요.
- 매핑 규칙이 없습니다. 같은 이름의 필드로 자동 매핑됩니다. + 매핑 그룹이 없습니다. 소스 테이블을 추가하세요.
소스 테이블을 먼저 선택하세요.
+ ) : activeRules.length === 0 ? ( +매핑 없음 (동일 필드명 자동 매핑)
+ ) : ( + activeRules.map((rule: any, rIdx: number) => { + const popoverKeyS = `${activeMappingGroupIndex}-${rIdx}-s`; + const popoverKeyT = `${activeMappingGroupIndex}-${rIdx}-t`; + return ( +{column.tableLabel || column.tableName}
-{column.tableLabel || column.tableName}
+메인 폼에서 가져올 부모 채번 필드
++ 예시: WO-20260223-005{col.autoFill?.separator ?? "-"}{String(1).padStart(col.autoFill?.sequenceLength ?? 2, "0")} +
++ {targetTable} 테이블에서 옵션을 불러올 때 적용할 조건 +
+ + {loadingColumns && ( ++ 필터 조건이 없습니다 +
+ )} + +테이블
+{config.categoryTable || tableName || "-"}
+컬럼
+{config.categoryColumn || columnName || "-"}
+화면 로드 시 자동 선택될 카테고리 값
++ 카테고리 값이 없습니다. 테이블 카테고리 관리에서 값을 추가해주세요. +
+ )} +테이블 컬럼을 조회할 수 없습니다. 테이블 타입 관리에서 참조 테이블을 설정해주세요.
)} - {/* 자동 채움 안내 */} {config.entityTable && entityColumns.length > 0 && (- 같은 폼에 참조 테이블({config.entityTable})의 컬럼이 배치되어 있으면, 엔티티 선택 시 해당 필드가 자동으로 채워집니다. + 같은 폼에 참조 테이블({config.entityTable})의 컬럼이 배치되어 있으면, 엔티티 선택 시 해당 필드가 자동으로 + 채워집니다.
+ 데이터 바인딩 값이 없을 때 표시할 기본 이미지 +
|
return (
{/* 요약 정보 */}
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index d0f9d5aa..aee70dd2 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -781,6 +781,7 @@ export const TableListComponent: React.FC
+
{componentConfig.leftPanel?.showSearch && (
@@ -3278,6 +3372,10 @@ export const SplitPanelLayoutComponent: React.FC
{values.map((val, idx) => {
const categoryData = mapping?.[val];
const displayLabel = categoryData?.label || val;
@@ -4316,7 +4330,7 @@ export const TableListComponent: React.FC
- {/* 확인 다이얼로그 - EditModal보다 위에 표시하도록 z-index 최상위로 설정 */}
+ {/* 확인 다이얼로그 */}
- {/* 파일 업로드 영역 - 높이 축소 */}
- {!isDesignMode && (
+ {/* 파일 업로드 영역 - readonly/disabled이면 숨김 */}
+ {!isDesignMode && !config.readonly && !config.disabled && (
@@ -487,8 +486,8 @@ export const FileManagerModal: React.FC {
- if (!config.disabled && !isDesignMode) {
- fileInputRef.current?.click();
- }
+ fileInputRef.current?.click();
}}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
@@ -267,7 +265,6 @@ export const FileManagerModal: React.FC
- {/* 좌측: 이미지 미리보기 (확대/축소 가능) */}
- }
+ {/* 좌측: 이미지 미리보기 (확대/축소 가능) - showPreview가 false면 숨김 */}
+ {(config.showPreview !== false) && }
- {/* 우측: 파일 목록 (고정 너비) */}
-
{/* 확대/축소 컨트롤 */}
{selectedFile && previewImageUrl && (
+
@@ -369,10 +366,10 @@ export const FileManagerModal: React.FC
)}
-
+ {/* 우측: 파일 목록 - showFileList가 false면 숨김, showPreview가 false면 전체 너비 */}
+ {(config.showFileList !== false) &&
+ 업로드된 파일@@ -404,7 +401,7 @@ export const FileManagerModal: React.FC- {formatFileSize(file.fileSize)} • {file.fileExt.toUpperCase()} + {config.showFileSize !== false && <>{formatFileSize(file.fileSize)} • >}{file.fileExt.toUpperCase()}
@@ -434,19 +431,21 @@ export const FileManagerModal: React.FC
)}
+
+ );
+ }
+
+ return (
+
+
+ + 품목별 라우팅 관리 + ++ 품목 선택 - 라우팅 버전 - 공정 순서 + +
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-item-routing/ItemRoutingConfigPanel.tsx b/frontend/lib/registry/components/v2-item-routing/ItemRoutingConfigPanel.tsx
new file mode 100644
index 00000000..f6fefd2e
--- /dev/null
+++ b/frontend/lib/registry/components/v2-item-routing/ItemRoutingConfigPanel.tsx
@@ -0,0 +1,780 @@
+"use client";
+
+import React, { useState, useEffect, useCallback } from "react";
+import { Plus, Trash2, Check, ChevronsUpDown } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
+import { ItemRoutingConfig, ProcessColumnDef } from "./types";
+import { defaultConfig } from "./config";
+
+interface TableInfo {
+ tableName: string;
+ displayName?: string;
+}
+
+interface ColumnInfo {
+ columnName: string;
+ displayName?: string;
+ dataType?: string;
+}
+
+interface ScreenInfo {
+ screenId: number;
+ screenName: string;
+ screenCode: string;
+}
+
+// 테이블 셀렉터 Combobox
+function TableSelector({
+ value,
+ onChange,
+ tables,
+ loading,
+}: {
+ value: string;
+ onChange: (v: string) => void;
+ tables: TableInfo[];
+ loading: boolean;
+}) {
+ const [open, setOpen] = useState(false);
+ const selected = tables.find((t) => t.tableName === value);
+
+ return (
+
+ {/* 좌측 패널: 품목 목록 */}
+
+
+ {/* 삭제 확인 다이얼로그 */}
+
+
+
+ {/* 우측 패널: 버전 + 공정 */}
+
+
+
+ {/* 검색 */}
+ + {config.leftPanelTitle || "품목 목록"} ++
+ setSearchText(e.target.value)}
+ onKeyDown={handleSearchKeyDown}
+ placeholder="품목명/품번 검색"
+ className="h-8 text-xs"
+ />
+
+
+ {/* 품목 리스트 */}
+
+ {items.length === 0 ? (
+
+
+
+ ) : (
+ + {loading ? "로딩 중..." : "품목이 없습니다"} + +
+ {items.map((item) => {
+ const itemCode =
+ item[config.dataSource.itemCodeColumn] || item.item_code || item.item_number;
+ const itemName =
+ item[config.dataSource.itemNameColumn] || item.item_name;
+ const isSelected = selectedItemCode === itemCode;
+
+ return (
+
+ )}
+
+
+ {itemName} +{itemCode} +
+ {selectedItemCode ? (
+ <>
+ {/* 헤더: 선택된 품목 + 버전 추가 */}
+
+
+
+
+ {/* 버전 선택 버튼들 */}
+ {versions.length > 0 ? (
+
+
+ {!config.readonly && (
+ {selectedItemName}+{selectedItemCode} +
+ 버전:
+ {versions.map((ver) => {
+ const isActive = selectedVersionId === ver.id;
+ const isDefault = ver.is_default === true;
+ return (
+
+ ) : (
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* 공정 테이블 */}
+ {selectedVersionId ? (
+ + 라우팅 버전이 없습니다. 버전을 추가해주세요. + +
+ {/* 공정 테이블 헤더 */}
+
+ ) : (
+ versions.length > 0 && (
+
+
+
+ {/* 테이블 */}
+ + {config.rightPanelTitle || "공정 순서"} ({details.length}건) ++ {!config.readonly && ( +
+ {details.length === 0 ? (
+
+
+
+ ) : (
+ + {loading ? "로딩 중..." : "등록된 공정이 없습니다"} + +
+
+ )
+ )}
+ >
+ ) : (
+ + 라우팅 버전을 선택해주세요 + +
+
+ )}
+ + 좌측에서 품목을 선택하세요 + ++ 품목을 선택하면 라우팅 버전별 공정 순서를 관리할 수 있습니다 + ++ 해당 버전에 포함된 모든 공정 정보도 함께 삭제됩니다. + > + )} +
+
+ {t.displayName || t.tableName}
+
+ {t.displayName && (
+
+ {t.tableName}
+
+ )}
+
+
+
+ {c.displayName || c.columnName}
+
+ {c.displayName && (
+
+ {c.columnName}
+
+ )}
+
+
+ {s.screenName}
+
+ {s.screenCode} (ID: {s.screenId})
+
+
+
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-item-routing/ItemRoutingRenderer.tsx b/frontend/lib/registry/components/v2-item-routing/ItemRoutingRenderer.tsx
new file mode 100644
index 00000000..7a9fa624
--- /dev/null
+++ b/frontend/lib/registry/components/v2-item-routing/ItemRoutingRenderer.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import React from "react";
+import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
+import { V2ItemRoutingDefinition } from "./index";
+import { ItemRoutingComponent } from "./ItemRoutingComponent";
+
+export class ItemRoutingRenderer extends AutoRegisteringComponentRenderer {
+ static componentDefinition = V2ItemRoutingDefinition;
+
+ render(): React.ReactElement {
+ const { formData, isPreview, config, tableName } = this.props as Record<
+ string,
+ unknown
+ >;
+
+ return (
+ 품목별 라우팅 설정+ + {/* 데이터 소스 설정 */} ++ 데이터 소스 + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 모달 연동 + +
+
+
+
+
+
+
+
+
+
+
+
+ + 공정 테이블 컬럼 + +
+ {config.processColumns.map((col, idx) => (
+
+
+
+ ))}
+ UI 설정 + +
+
+ update({ splitRatio: Number(e.target.value) })}
+ min={20}
+ max={60}
+ className="mt-1 h-8 w-20 text-xs"
+ />
+
+
+
+
+ update({ leftPanelTitle: e.target.value })}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+ update({ rightPanelTitle: e.target.value })}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+ update({ versionAddButtonText: e.target.value })}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+ update({ processAddButtonText: e.target.value })}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ + 공정 작업기준 + ++ {sortedPhases.map((p) => p.label).join(" / ")} + +
+ {/* 메인 콘텐츠 */}
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardConfigPanel.tsx b/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardConfigPanel.tsx
new file mode 100644
index 00000000..21a5d69f
--- /dev/null
+++ b/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardConfigPanel.tsx
@@ -0,0 +1,282 @@
+"use client";
+
+import React from "react";
+import { Plus, Trash2, GripVertical } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { ProcessWorkStandardConfig, WorkPhaseDefinition, DetailTypeDefinition } from "./types";
+import { defaultConfig } from "./config";
+
+interface ConfigPanelProps {
+ config: Partial
+ {/* 좌측 패널 */}
+
+
+ {/* 작업 항목 추가/수정 모달 */}
+
+
+
+ {/* 우측 패널 */}
+
+ {/* 우측 헤더 */}
+ {selection.routingDetailId ? (
+ <>
+
+
+
+
+ {/* 작업 단계별 섹션 */}
+
+
+ {!config.readonly && (
+ + {selection.itemName} - {selection.processName} ++
+ 품목: {selection.itemCode}
+ 공정: {selection.processName}
+ 버전: {selection.routingVersionName}
+
+
+ {sortedPhases.map((phase) => (
+
+ >
+ ) : (
+
+
+ )}
+ + 좌측에서 품목과 공정을 선택하세요 + ++ 품목을 펼쳐 라우팅별 공정을 선택하면 작업기준을 관리할 수 + 있습니다 + +
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardRenderer.tsx b/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardRenderer.tsx
new file mode 100644
index 00000000..cb1e0e85
--- /dev/null
+++ b/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardRenderer.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import React from "react";
+import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
+import { V2ProcessWorkStandardDefinition } from "./index";
+import { ProcessWorkStandardComponent } from "./ProcessWorkStandardComponent";
+
+export class ProcessWorkStandardRenderer extends AutoRegisteringComponentRenderer {
+ static componentDefinition = V2ProcessWorkStandardDefinition;
+
+ render(): React.ReactElement {
+ const { formData, isPreview, config, tableName } = this.props as Record<
+ string,
+ unknown
+ >;
+
+ return (
+ 공정 작업기준 설정+ + {/* 데이터 소스 설정 */} +데이터 소스 설정 + +
+
+ updateDataSource("itemTable", e.target.value)}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+
+
+ updateDataSource("itemNameColumn", e.target.value)}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+ updateDataSource("itemCodeColumn", e.target.value)}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+ updateDataSource("routingVersionTable", e.target.value)}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+ updateDataSource("routingFkColumn", e.target.value)}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+ updateDataSource("processTable", e.target.value)}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+
+ updateDataSource("processNameColumn", e.target.value)}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+ updateDataSource("processCodeColumn", e.target.value)}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+ 작업 단계 설정 +
+ {config.phases.map((phase, idx) => (
+
+
+
+ ))}
+
+
+
+ 상세 유형 옵션 +
+ {config.detailTypes.map((dt, idx) => (
+
+
+ updateDetailType(idx, "value", e.target.value)}
+ className="h-7 w-24 text-[10px]"
+ placeholder="값"
+ />
+ updateDetailType(idx, "label", e.target.value)}
+ className="h-7 flex-1 text-[10px]"
+ placeholder="표시명"
+ />
+
+ ))}
+ UI 설정 + +
+
+ update({ splitRatio: Number(e.target.value) })}
+ min={15}
+ max={50}
+ className="mt-1 h-8 w-20 text-xs"
+ />
+
+
+
+
+ update({ leftPanelTitle: e.target.value })}
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+
+ {/* 헤더 */}
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx
new file mode 100644
index 00000000..e9f97c02
--- /dev/null
+++ b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx
@@ -0,0 +1,350 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Plus, Trash2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { DetailTypeDefinition, WorkItem } from "../types";
+
+interface ModalDetail {
+ id: string;
+ detail_type: string;
+ content: string;
+ is_required: string;
+ sort_order: number;
+}
+
+interface WorkItemAddModalProps {
+ open: boolean;
+ onClose: () => void;
+ onSave: (data: {
+ work_phase: string;
+ title: string;
+ is_required: string;
+ description?: string;
+ details?: Array<{
+ detail_type?: string;
+ content: string;
+ is_required: string;
+ sort_order: number;
+ }>;
+ }) => void;
+ phaseKey: string;
+ phaseLabel: string;
+ detailTypes: DetailTypeDefinition[];
+ editItem?: WorkItem | null;
+}
+
+export function WorkItemAddModal({
+ open,
+ onClose,
+ onSave,
+ phaseKey,
+ phaseLabel,
+ detailTypes,
+ editItem,
+}: WorkItemAddModalProps) {
+ const [title, setTitle] = useState("");
+ const [isRequired, setIsRequired] = useState("Y");
+ const [description, setDescription] = useState("");
+ const [details, setDetails] = useState
+
+
+ {/* 트리 목록 */}
+
+
+
+
+
+ {items.length === 0 ? (
+
+
+
+ ) : (
+ items.map((item) => (
+ + 라우팅이 등록된 품목이 없습니다 + +
+ {/* 품목 헤더 */}
+
+ ))
+ )}
+
+ {routings.length === 0 ? (
+
+ )}
+ + 등록된 공정이 없습니다 + + ) : ( + routings.map((routing) => ( +
+ {/* 라우팅 버전 */}
+
+ ))
+ )}
+
+
+
+ {/* 공정 목록 */}
+ {routing.processes.map((proc) => (
+
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemDetailList.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemDetailList.tsx
new file mode 100644
index 00000000..89d1b464
--- /dev/null
+++ b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemDetailList.tsx
@@ -0,0 +1,220 @@
+"use client";
+
+import React, { useState } from "react";
+import { Plus, Pencil, Trash2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { cn } from "@/lib/utils";
+import { WorkItem, WorkItemDetail, DetailTypeDefinition } from "../types";
+import { DetailFormModal } from "./DetailFormModal";
+
+interface WorkItemDetailListProps {
+ workItem: WorkItem | null;
+ details: WorkItemDetail[];
+ detailTypes: DetailTypeDefinition[];
+ readonly?: boolean;
+ onCreateDetail: (data: Partial
+
+
+ {!readonly && (
+
+ {item.title}
+
+
+
+
+
+ )}
+
+
+ );
+ }
+
+ const getTypeLabel = (value?: string) =>
+ detailTypes.find((t) => t.value === value)?.label || value || "-";
+
+ const handleOpenAdd = () => {
+ setModalMode("add");
+ setEditTarget(null);
+ setModalOpen(true);
+ };
+
+ const handleOpenEdit = (detail: WorkItemDetail) => {
+ setModalMode("edit");
+ setEditTarget(detail);
+ setModalOpen(true);
+ };
+
+ const handleSubmit = (data: Partial+ 왼쪽에서 항목을 선택하세요 + +
+ {/* 헤더 */}
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/WorkPhaseSection.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/WorkPhaseSection.tsx
new file mode 100644
index 00000000..1f2e7e4a
--- /dev/null
+++ b/frontend/lib/registry/components/v2-process-work-standard/components/WorkPhaseSection.tsx
@@ -0,0 +1,120 @@
+"use client";
+
+import React from "react";
+import { Plus, ClipboardList } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { WorkItemCard } from "./WorkItemCard";
+import { WorkItemDetailList } from "./WorkItemDetailList";
+import {
+ WorkItem,
+ WorkItemDetail,
+ WorkPhaseDefinition,
+ DetailTypeDefinition,
+} from "../types";
+
+interface WorkPhaseSectionProps {
+ phase: WorkPhaseDefinition;
+ items: WorkItem[];
+ selectedWorkItemId: string | null;
+ selectedWorkItemDetails: WorkItemDetail[];
+ detailTypes: DetailTypeDefinition[];
+ readonly?: boolean;
+ onSelectWorkItem: (workItemId: string, phaseKey: string) => void;
+ onAddWorkItem: (phase: string) => void;
+ onEditWorkItem: (item: WorkItem) => void;
+ onDeleteWorkItem: (id: string) => void;
+ onCreateDetail: (workItemId: string, data: Partial
+
+
+ {/* 테이블 */}
+
+ {workItem.title}
+
+ {!readonly && (
+
+
+
+ {/* 추가/수정 모달 */}
+
+
+ )}
+ + 상세 항목이 없습니다. "상세 추가" 버튼을 클릭하여 추가하세요. + +
+ {/* 섹션 헤더 */}
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-process-work-standard/config.ts b/frontend/lib/registry/components/v2-process-work-standard/config.ts
new file mode 100644
index 00000000..43ad60cd
--- /dev/null
+++ b/frontend/lib/registry/components/v2-process-work-standard/config.ts
@@ -0,0 +1,34 @@
+/**
+ * 공정 작업기준 기본 설정
+ */
+
+import { ProcessWorkStandardConfig } from "./types";
+
+export const defaultConfig: ProcessWorkStandardConfig = {
+ dataSource: {
+ itemTable: "item_info",
+ itemNameColumn: "item_name",
+ itemCodeColumn: "item_number",
+ routingVersionTable: "item_routing_version",
+ routingFkColumn: "item_code",
+ routingVersionNameColumn: "version_name",
+ routingDetailTable: "item_routing_detail",
+ processTable: "process_mng",
+ processNameColumn: "process_name",
+ processCodeColumn: "process_code",
+ },
+ phases: [
+ { key: "PRE", label: "작업 전 (Pre-Work)", sortOrder: 1 },
+ { key: "IN", label: "작업 중 (In-Work)", sortOrder: 2 },
+ { key: "POST", label: "작업 후 (Post-Work)", sortOrder: 3 },
+ ],
+ detailTypes: [
+ { value: "check", label: "체크리스트" },
+ { value: "inspect", label: "검사항목" },
+ { value: "procedure", label: "작업절차" },
+ { value: "input", label: "직접입력" },
+ ],
+ splitRatio: 30,
+ leftPanelTitle: "품목 및 공정 선택",
+ readonly: false,
+};
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
new file mode 100644
index 00000000..e909d291
--- /dev/null
+++ b/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts
@@ -0,0 +1,355 @@
+"use client";
+
+import { useState, useCallback } from "react";
+import { apiClient } from "@/lib/api/client";
+import {
+ ProcessWorkStandardConfig,
+ ItemData,
+ RoutingVersion,
+ WorkItem,
+ WorkItemDetail,
+ SelectionState,
+} from "../types";
+
+const API_BASE = "/process-work-standard";
+
+export function useProcessWorkStandard(config: ProcessWorkStandardConfig) {
+ const [items, setItems] = useState
+
+
+ {/* 콘텐츠 영역 */}
+
+
+ {!readonly && (
+ {phase.label}+
+ {/* 좌측: 작업 항목 카드 목록 */}
+
+
+ {items.length === 0 ? (
+
+
+ {/* 우측: 상세 리스트 */}
+
+
+ ) : (
+ + 등록된 항목이 없습니다 + +
+ {items.map((item) => (
+
+ )}
+
+
+
+ {!isDesignMode && (componentConfig.leftPanel as any)?.showBomExcelUpload && (
+
@@ -3302,6 +3400,10 @@ export const SplitPanelLayoutComponent: React.FC
+ |
+ )}
| |||||||||||||
|
+
+ {(componentConfig.leftPanel?.showEdit !== false) && (
+
+ |
+ )}
| + | + )} @@ -3378,7 +3516,7 @@ export const SplitPanelLayoutComponent: React.FC
|---|
|
+
+ {(componentConfig.leftPanel?.showEdit !== false) && (
+
+ |
+ )}