From e05af3c6f9022314bb942de2ccbef5450f3dfbbe Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 8 Dec 2025 15:15:44 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A0=89=20=EA=B5=AC=EC=A1=B0=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/노드플로우_개선사항.md | 1 + frontend/lib/registry/components/index.ts | 3 + .../components/rack-structure/README.md | 148 ++++ .../rack-structure/RackStructureComponent.tsx | 724 ++++++++++++++++++ .../RackStructureConfigPanel.tsx | 287 +++++++ .../rack-structure/RackStructureRenderer.tsx | 44 ++ .../components/rack-structure/config.ts | 27 + .../components/rack-structure/index.ts | 74 ++ .../components/rack-structure/types.ts | 91 +++ ..._임베딩_및_데이터_전달_시스템_구현_계획서.md | 1 + 화면_임베딩_시스템_Phase1-4_구현_완료.md | 1 + 화면_임베딩_시스템_충돌_분석_보고서.md | 1 + 12 files changed, 1402 insertions(+) create mode 100644 frontend/lib/registry/components/rack-structure/README.md create mode 100644 frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx create mode 100644 frontend/lib/registry/components/rack-structure/RackStructureConfigPanel.tsx create mode 100644 frontend/lib/registry/components/rack-structure/RackStructureRenderer.tsx create mode 100644 frontend/lib/registry/components/rack-structure/config.ts create mode 100644 frontend/lib/registry/components/rack-structure/index.ts create mode 100644 frontend/lib/registry/components/rack-structure/types.ts diff --git a/docs/노드플로우_개선사항.md b/docs/노드플로우_개선사항.md index 3fe6cde2..85ae186b 100644 --- a/docs/노드플로우_개선사항.md +++ b/docs/노드플로우_개선사항.md @@ -580,3 +580,4 @@ const result = await executeNodeFlow(flowId, { - 프론트엔드 플로우 에디터: `frontend/components/dataflow/node-editor/FlowEditor.tsx` - 프론트엔드 플로우 API: `frontend/lib/api/nodeFlows.ts` + diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index 2a5d45e4..ff9d9240 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -77,6 +77,9 @@ import "./screen-split-panel/ScreenSplitPanelRenderer"; // 화면 분할 패널 // 🆕 범용 폼 모달 컴포넌트 import "./universal-form-modal/UniversalFormModalRenderer"; // 섹션 기반 폼, 채번규칙, 다중 행 저장 지원 +// 🆕 렉 구조 설정 컴포넌트 +import "./rack-structure/RackStructureRenderer"; // 창고 렉 위치 일괄 생성 + /** * 컴포넌트 초기화 함수 */ diff --git a/frontend/lib/registry/components/rack-structure/README.md b/frontend/lib/registry/components/rack-structure/README.md new file mode 100644 index 00000000..bf03d8dc --- /dev/null +++ b/frontend/lib/registry/components/rack-structure/README.md @@ -0,0 +1,148 @@ +# 렉 구조 설정 컴포넌트 (Rack Structure Config) + +창고 렉 위치를 열 범위와 단 수로 일괄 생성하는 컴포넌트입니다. + +## 핵심 개념 + +이 컴포넌트는 **상위 폼의 필드 값을 읽어서** 위치 코드를 생성합니다. + +### 작동 방식 + +1. 사용자가 화면관리에서 테이블 컬럼(창고코드, 층, 구역 등)을 드래그하여 폼에 배치 +2. 렉 구조 컴포넌트 설정에서 **필드 매핑** 설정 (어떤 폼 필드가 창고/층/구역인지) +3. 런타임에 사용자가 폼 필드에 값을 입력하면, 렉 구조 컴포넌트가 해당 값을 읽어서 사용 + +## 기능 + +### 1. 렉 라인 구조 설정 + +- 조건 추가/삭제 +- 각 조건: 열 범위(시작~종료) + 단 수 +- 자동 위치 수 계산 (예: 1열~3열 x 3단 = 9개) +- 템플릿 저장/불러오기 + +### 2. 등록 미리보기 + +- 통계 카드 (총 위치, 열 수, 최대 단) +- 미리보기 생성 버튼 +- 생성될 위치 목록 테이블 + +## 설정 방법 + +### 1. 화면관리에서 배치 + +1. 상위에 테이블 컬럼들을 배치 (창고코드, 층, 구역, 위치유형, 사용여부) +2. 컴포넌트 팔레트에서 "렉 구조 설정" 선택 +3. 캔버스에 드래그하여 배치 + +### 2. 필드 매핑 설정 + +설정 패널에서 상위 폼의 어떤 필드를 사용할지 매핑합니다: + +| 매핑 항목 | 설명 | +| -------------- | ------------------------------------- | +| 창고 코드 필드 | 위치 코드 생성에 사용할 창고 코드 | +| 층 필드 | 위치 코드 생성에 사용할 층 | +| 구역 필드 | 위치 코드 생성에 사용할 구역 | +| 위치 유형 필드 | 미리보기 테이블에 표시할 위치 유형 | +| 사용 여부 필드 | 미리보기 테이블에 표시할 사용 여부 | + +### 예시 + +상위 폼에 다음 필드가 배치되어 있다면: +- `창고코드(조인)` → 필드명: `warehouse_code` +- `층` → 필드명: `floor` +- `구역` → 필드명: `zone` + +설정 패널에서: +- 창고 코드 필드: `warehouse_code` 선택 +- 층 필드: `floor` 선택 +- 구역 필드: `zone` 선택 + +## 위치 코드 생성 규칙 + +기본 패턴: `{창고코드}-{층}{구역}-{열:2자리}-{단}` + +예시 (창고: WH001, 층: 1, 구역: A): + +- WH001-1A-01-1 (01열, 1단) +- WH001-1A-01-2 (01열, 2단) +- WH001-1A-02-1 (02열, 1단) + +## 설정 옵션 + +| 옵션 | 타입 | 기본값 | 설명 | +| -------------- | ------- | ------ | ---------------- | +| maxConditions | number | 10 | 최대 조건 수 | +| maxRows | number | 99 | 최대 열 수 | +| maxLevels | number | 20 | 최대 단 수 | +| showTemplates | boolean | true | 템플릿 기능 표시 | +| showPreview | boolean | true | 미리보기 표시 | +| showStatistics | boolean | true | 통계 카드 표시 | +| readonly | boolean | false | 읽기 전용 | + +## 출력 데이터 + +`onChange` 콜백으로 생성된 위치 데이터 배열을 반환합니다: + +```typescript +interface GeneratedLocation { + rowNum: number; // 열 번호 + levelNum: number; // 단 번호 + locationCode: string; // 위치 코드 + locationName: string; // 위치명 + locationType?: string; // 위치 유형 + status?: string; // 사용 여부 + warehouseCode?: string; // 창고 코드 (매핑된 값) + floor?: string; // 층 (매핑된 값) + zone?: string; // 구역 (매핑된 값) +} +``` + +## 연동 테이블 + +`warehouse_location` 테이블과 연동됩니다: + +| 컬럼 | 설명 | +| ------------- | --------- | +| warehouse_id | 창고 ID | +| floor | 층 | +| zone | 구역 | +| row_num | 열 번호 | +| level_num | 단 번호 | +| location_code | 위치 코드 | +| location_name | 위치명 | +| location_type | 위치 유형 | +| status | 사용 여부 | + +## 예시 시나리오 + +### 시나리오: A구역에 1~3열은 3단, 4~6열은 5단 렉 생성 + +1. **상위 폼에서 기본 정보 입력** + - 창고: 제1창고 (WH001) - 드래그해서 배치한 필드 + - 층: 1 - 드래그해서 배치한 필드 + - 구역: A - 드래그해서 배치한 필드 + - 위치 유형: 선반 - 드래그해서 배치한 필드 + - 사용 여부: 사용 - 드래그해서 배치한 필드 + +2. **렉 구조 컴포넌트에서 조건 추가** + - 조건 1: 1~3열, 3단 → 9개 + - 조건 2: 4~6열, 5단 → 15개 + +3. **미리보기 생성** + - 총 위치: 24개 + - 열 수: 6개 + - 최대 단: 5단 + +4. **저장** + - 24개의 위치 데이터가 warehouse_location 테이블에 저장됨 + +## 필수 필드 검증 + +미리보기 생성 시 다음 필드가 입력되어 있어야 합니다: +- 창고 코드 +- 층 +- 구역 + +필드가 비어있으면 경고 메시지가 표시됩니다. diff --git a/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx b/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx new file mode 100644 index 00000000..f49e4462 --- /dev/null +++ b/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx @@ -0,0 +1,724 @@ +"use client"; + +import React, { useState, useCallback, useMemo, useEffect } from "react"; +import { Plus, X, Save, FolderOpen, RefreshCw, Eye, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { cn } from "@/lib/utils"; +import { + RackStructureComponentProps, + RackLineCondition, + RackStructureTemplate, + GeneratedLocation, + RackStructureContext, +} from "./types"; + +// 고유 ID 생성 +const generateId = () => `cond_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + +// 조건 카드 컴포넌트 +interface ConditionCardProps { + condition: RackLineCondition; + index: number; + onUpdate: (id: string, updates: Partial) => void; + onRemove: (id: string) => void; + maxRows: number; + maxLevels: number; + readonly?: boolean; +} + +const ConditionCard: React.FC = ({ + condition, + index, + onUpdate, + onRemove, + maxRows, + maxLevels, + readonly, +}) => { + // 로컬 상태로 입력값 관리 + const [localValues, setLocalValues] = useState({ + startRow: condition.startRow.toString(), + endRow: condition.endRow.toString(), + levels: condition.levels.toString(), + }); + + // condition이 변경되면 로컬 상태 동기화 + useEffect(() => { + setLocalValues({ + startRow: condition.startRow.toString(), + endRow: condition.endRow.toString(), + levels: condition.levels.toString(), + }); + }, [condition.startRow, condition.endRow, condition.levels]); + + // 계산된 위치 수 + const locationCount = useMemo(() => { + const start = parseInt(localValues.startRow) || 0; + const end = parseInt(localValues.endRow) || 0; + const levels = parseInt(localValues.levels) || 0; + if (start > 0 && end >= start && levels > 0) { + return (end - start + 1) * levels; + } + return 0; + }, [localValues]); + + // 입력값 변경 핸들러 + const handleChange = (field: keyof typeof localValues, value: string) => { + setLocalValues((prev) => ({ ...prev, [field]: value })); + }; + + // blur 시 실제 업데이트 + const handleBlur = (field: keyof typeof localValues) => { + const numValue = parseInt(localValues[field]) || 0; + const clampedValue = Math.max(0, Math.min(numValue, field === "levels" ? maxLevels : maxRows)); + + setLocalValues((prev) => ({ ...prev, [field]: clampedValue.toString() })); + + const updateField = field === "startRow" ? "startRow" : field === "endRow" ? "endRow" : "levels"; + onUpdate(condition.id, { [updateField]: clampedValue }); + }; + + return ( +
+ {/* 헤더 */} +
+ 조건 {index + 1} + {!readonly && ( + + )} +
+ + {/* 내용 */} +
+ {/* 열 범위 */} +
+
+ +
+ handleChange("startRow", e.target.value)} + onBlur={() => handleBlur("startRow")} + disabled={readonly} + className="h-9 text-center" + /> + ~ + handleChange("endRow", e.target.value)} + onBlur={() => handleBlur("endRow")} + disabled={readonly} + className="h-9 text-center" + /> +
+
+
+ + handleChange("levels", e.target.value)} + onBlur={() => handleBlur("levels")} + disabled={readonly} + className="h-9 text-center" + /> +
+
+ + {/* 계산 결과 */} +
+ {locationCount > 0 ? ( + <> + {localValues.startRow}열 ~ {localValues.endRow}열 x {localValues.levels}단 ={" "} + {locationCount}개 + + ) : ( + 값을 입력하세요 + )} +
+
+
+ ); +}; + +// 메인 컴포넌트 +export const RackStructureComponent: React.FC = ({ + config, + context: propContext, + formData, + onChange, + onConditionsChange, + isPreview = false, +}) => { + // 조건 목록 + const [conditions, setConditions] = useState( + config.initialConditions || [] + ); + + // 템플릿 관련 상태 + const [templates, setTemplates] = useState([]); + const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); + const [templateName, setTemplateName] = useState(""); + const [isSaveMode, setIsSaveMode] = useState(false); + + // 미리보기 데이터 + const [previewData, setPreviewData] = useState([]); + const [isPreviewGenerated, setIsPreviewGenerated] = useState(false); + + // 설정값 + const maxConditions = config.maxConditions || 10; + const maxRows = config.maxRows || 99; + const maxLevels = config.maxLevels || 20; + const readonly = config.readonly || isPreview; + const fieldMapping = config.fieldMapping || {}; + + // 필드 매핑을 통해 formData에서 컨텍스트 추출 + const context: RackStructureContext = useMemo(() => { + // propContext가 있으면 우선 사용 + if (propContext) return propContext; + + // formData와 fieldMapping을 사용하여 컨텍스트 생성 + if (!formData) return {}; + + return { + warehouseCode: fieldMapping.warehouseCodeField + ? formData[fieldMapping.warehouseCodeField] + : undefined, + warehouseName: fieldMapping.warehouseNameField + ? formData[fieldMapping.warehouseNameField] + : undefined, + floor: fieldMapping.floorField + ? formData[fieldMapping.floorField]?.toString() + : undefined, + zone: fieldMapping.zoneField + ? formData[fieldMapping.zoneField] + : undefined, + locationType: fieldMapping.locationTypeField + ? formData[fieldMapping.locationTypeField] + : undefined, + status: fieldMapping.statusField + ? formData[fieldMapping.statusField] + : undefined, + }; + }, [propContext, formData, fieldMapping]); + + // 필수 필드 검증 + const missingFields = useMemo(() => { + const missing: string[] = []; + if (!context.warehouseCode) missing.push("창고 코드"); + if (!context.floor) missing.push("층"); + if (!context.zone) missing.push("구역"); + return missing; + }, [context]); + + // 조건 변경 시 콜백 호출 + useEffect(() => { + onConditionsChange?.(conditions); + setIsPreviewGenerated(false); // 조건 변경 시 미리보기 초기화 + }, [conditions, onConditionsChange]); + + // 조건 추가 + const addCondition = useCallback(() => { + if (conditions.length >= maxConditions) return; + + // 마지막 조건의 다음 열부터 시작 + const lastCondition = conditions[conditions.length - 1]; + const startRow = lastCondition ? lastCondition.endRow + 1 : 1; + + const newCondition: RackLineCondition = { + id: generateId(), + startRow, + endRow: startRow + 2, + levels: 3, + }; + + setConditions((prev) => [...prev, newCondition]); + }, [conditions, maxConditions]); + + // 조건 업데이트 + const updateCondition = useCallback((id: string, updates: Partial) => { + setConditions((prev) => + prev.map((cond) => (cond.id === id ? { ...cond, ...updates } : cond)) + ); + }, []); + + // 조건 삭제 + const removeCondition = useCallback((id: string) => { + setConditions((prev) => prev.filter((cond) => cond.id !== id)); + }, []); + + // 통계 계산 + const statistics = useMemo(() => { + let totalLocations = 0; + let totalRows = 0; + let maxLevel = 0; + const rowSet = new Set(); + + conditions.forEach((cond) => { + if (cond.startRow > 0 && cond.endRow >= cond.startRow && cond.levels > 0) { + const rowCount = cond.endRow - cond.startRow + 1; + totalLocations += rowCount * cond.levels; + for (let r = cond.startRow; r <= cond.endRow; r++) { + rowSet.add(r); + } + maxLevel = Math.max(maxLevel, cond.levels); + } + }); + + totalRows = rowSet.size; + return { totalLocations, totalRows, maxLevel }; + }, [conditions]); + + // 위치 코드 생성 + const generateLocationCode = useCallback( + (row: number, level: number): { code: string; name: string } => { + const warehouseCode = context?.warehouseCode || "WH001"; + const floor = context?.floor || "1"; + const zone = context?.zone || "A"; + + // 코드 생성 (예: WH001-1A-01-1) + const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`; + + // 이름 생성 (예: A구역-01열-1단) + const name = `${zone}구역-${row.toString().padStart(2, "0")}열-${level}단`; + + return { code, name }; + }, + [context] + ); + + // 미리보기 생성 + const generatePreview = useCallback(() => { + // 필수 필드 검증 + if (missingFields.length > 0) { + alert(`다음 필드를 먼저 입력해주세요: ${missingFields.join(", ")}`); + return; + } + + const locations: GeneratedLocation[] = []; + + conditions.forEach((cond) => { + if (cond.startRow > 0 && cond.endRow >= cond.startRow && cond.levels > 0) { + for (let row = cond.startRow; row <= cond.endRow; row++) { + for (let level = 1; level <= cond.levels; level++) { + const { code, name } = generateLocationCode(row, level); + locations.push({ + rowNum: row, + levelNum: level, + locationCode: code, + locationName: name, + locationType: context?.locationType || "선반", + status: context?.status || "사용", + // 추가 필드 + warehouseCode: context?.warehouseCode, + floor: context?.floor, + zone: context?.zone, + }); + } + } + } + }); + + // 정렬: 열 -> 단 순서 + locations.sort((a, b) => { + if (a.rowNum !== b.rowNum) return a.rowNum - b.rowNum; + return a.levelNum - b.levelNum; + }); + + setPreviewData(locations); + setIsPreviewGenerated(true); + onChange?.(locations); + }, [conditions, context, generateLocationCode, onChange, missingFields]); + + // 템플릿 저장 + const saveTemplate = useCallback(() => { + if (!templateName.trim()) return; + + const newTemplate: RackStructureTemplate = { + id: generateId(), + name: templateName.trim(), + conditions: [...conditions], + createdAt: new Date().toISOString(), + }; + + setTemplates((prev) => [...prev, newTemplate]); + setTemplateName(""); + setIsTemplateDialogOpen(false); + }, [templateName, conditions]); + + // 템플릿 불러오기 + const loadTemplate = useCallback((template: RackStructureTemplate) => { + setConditions(template.conditions.map((c) => ({ ...c, id: generateId() }))); + setIsTemplateDialogOpen(false); + }, []); + + // 템플릿 삭제 + const deleteTemplate = useCallback((templateId: string) => { + setTemplates((prev) => prev.filter((t) => t.id !== templateId)); + }, []); + + return ( +
+ {/* 렉 라인 구조 설정 섹션 */} + + + +
+ 렉 라인 구조 설정 + + {!readonly && ( +
+ {config.showTemplates && ( + <> + + + )} + +
+ )} + + + {/* 필수 필드 경고 */} + {missingFields.length > 0 && ( + + + + 다음 필드를 먼저 입력해주세요: {missingFields.join(", ")} +
+ + (설정 패널에서 필드 매핑을 확인하세요) + +
+
+ )} + + {/* 현재 매핑된 값 표시 */} + {(context.warehouseCode || context.warehouseName || context.floor || context.zone) && ( +
+ {(context.warehouseCode || context.warehouseName) && ( + + 창고: {context.warehouseName || context.warehouseCode} + {context.warehouseName && context.warehouseCode && ` (${context.warehouseCode})`} + + )} + {context.floor && ( + + 층: {context.floor} + + )} + {context.zone && ( + + 구역: {context.zone} + + )} + {context.locationType && ( + + 유형: {context.locationType} + + )} + {context.status && ( + + 상태: {context.status} + + )} +
+ )} + + {/* 안내 메시지 */} +
+
    +
  1. + + 1 + + 조건 추가 버튼을 클릭하여 렉 라인 조건을 생성하세요 +
  2. +
  3. + + 2 + + 각 조건마다 열 범위와 단 수를 입력하세요 +
  4. +
  5. + + 3 + + 예시: 조건1(1~3열, 3단), 조건2(4~6열, 5단) +
  6. +
+
+ + {/* 조건 목록 또는 빈 상태 */} + {conditions.length === 0 ? ( +
+
📦
+

조건을 추가하여 렉 구조를 설정하세요

+ {!readonly && ( + + )} +
+ ) : ( +
+ {conditions.map((condition, index) => ( +
+ +
+ ))} +
+ )} +
+ + + {/* 등록 미리보기 섹션 */} + {config.showPreview && conditions.length > 0 && ( + + + + + 등록 미리보기 + + + + + {/* 통계 카드 */} + {config.showStatistics && ( +
+
+
총 위치
+
{statistics.totalLocations}개
+
+
+
열 수
+
{statistics.totalRows}개
+
+
+
최대 단
+
{statistics.maxLevel}단
+
+
+ )} + + {/* 미리보기 테이블 */} + {isPreviewGenerated && previewData.length > 0 ? ( +
+ + + + + No + 위치코드 + 위치명 + + 구역 + + + 유형 + 비고 + + + + {previewData.map((loc, idx) => ( + + {idx + 1} + {loc.locationCode} + {loc.locationName} + {context?.floor || "1"} + {context?.zone || "A"} + + {loc.rowNum.toString().padStart(2, "0")} + + {loc.levelNum} + {loc.locationType} + - + + ))} + +
+
+
+ ) : ( +
+ +

미리보기 생성 버튼을 클릭하여 결과를 확인하세요

+
+ )} +
+
+ )} + + {/* 템플릿 다이얼로그 */} + + + + + {isSaveMode ? "템플릿 저장" : "템플릿 관리"} + + + + {isSaveMode ? ( +
+
+ + setTemplateName(e.target.value)} + placeholder="템플릿 이름을 입력하세요" + /> +
+ + + + +
+ ) : ( +
+ {/* 저장 버튼 */} + {conditions.length > 0 && ( + + )} + + {/* 템플릿 목록 */} + {templates.length > 0 ? ( +
+
저장된 템플릿
+ + {templates.map((template) => ( +
+
+
{template.name}
+
+ {template.conditions.length}개 조건 +
+
+
+ + +
+
+ ))} +
+
+ ) : ( +
+ 저장된 템플릿이 없습니다 +
+ )} +
+ )} +
+
+
+ ); +}; + +// Wrapper 컴포넌트 (레지스트리용) +export const RackStructureWrapper: React.FC = (props) => { + return ( +
+ +
+ ); +}; + + diff --git a/frontend/lib/registry/components/rack-structure/RackStructureConfigPanel.tsx b/frontend/lib/registry/components/rack-structure/RackStructureConfigPanel.tsx new file mode 100644 index 00000000..8f0c8177 --- /dev/null +++ b/frontend/lib/registry/components/rack-structure/RackStructureConfigPanel.tsx @@ -0,0 +1,287 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { RackStructureComponentConfig, FieldMapping } from "./types"; + +interface RackStructureConfigPanelProps { + config: RackStructureComponentConfig; + onChange: (config: RackStructureComponentConfig) => void; + // 화면관리에서 전달하는 테이블 컬럼 정보 + tables?: Array<{ + tableName: string; + tableLabel?: string; + columns: Array<{ + columnName: string; + columnLabel?: string; + dataType?: string; + }>; + }>; +} + +export const RackStructureConfigPanel: React.FC = ({ + config, + onChange, + tables = [], +}) => { + // 사용 가능한 컬럼 목록 추출 + const [availableColumns, setAvailableColumns] = useState< + Array<{ value: string; label: string }> + >([]); + + useEffect(() => { + // 모든 테이블의 컬럼을 플랫하게 추출 + const columns: Array<{ value: string; label: string }> = []; + tables.forEach((table) => { + table.columns.forEach((col) => { + columns.push({ + value: col.columnName, + label: col.columnLabel || col.columnName, + }); + }); + }); + setAvailableColumns(columns); + }, [tables]); + + const handleChange = (key: keyof RackStructureComponentConfig, value: any) => { + onChange({ ...config, [key]: value }); + }; + + const handleFieldMappingChange = (field: keyof FieldMapping, value: string) => { + const currentMapping = config.fieldMapping || {}; + onChange({ + ...config, + fieldMapping: { + ...currentMapping, + [field]: value === "__none__" ? undefined : value, + }, + }); + }; + + const fieldMapping = config.fieldMapping || {}; + + return ( +
+ {/* 필드 매핑 섹션 */} +
+
필드 매핑
+

+ 상위 폼에 배치된 필드 중 어떤 필드를 사용할지 선택하세요 +

+ + {/* 창고 코드 필드 */} +
+ + +
+ + {/* 창고명 필드 */} +
+ + +
+ + {/* 층 필드 */} +
+ + +
+ + {/* 구역 필드 */} +
+ + +
+ + {/* 위치 유형 필드 */} +
+ + +
+ + {/* 사용 여부 필드 */} +
+ + +
+
+ + {/* 제한 설정 */} +
+
제한 설정
+ +
+ + handleChange("maxConditions", parseInt(e.target.value) || 10)} + className="h-8" + /> +
+ +
+ + handleChange("maxRows", parseInt(e.target.value) || 99)} + className="h-8" + /> +
+ +
+ + handleChange("maxLevels", parseInt(e.target.value) || 20)} + className="h-8" + /> +
+
+ + {/* UI 설정 */} +
+
UI 설정
+ +
+ + handleChange("showTemplates", checked)} + /> +
+ +
+ + handleChange("showPreview", checked)} + /> +
+ +
+ + handleChange("showStatistics", checked)} + /> +
+ +
+ + handleChange("readonly", checked)} + /> +
+
+
+ ); +}; diff --git a/frontend/lib/registry/components/rack-structure/RackStructureRenderer.tsx b/frontend/lib/registry/components/rack-structure/RackStructureRenderer.tsx new file mode 100644 index 00000000..ab832f51 --- /dev/null +++ b/frontend/lib/registry/components/rack-structure/RackStructureRenderer.tsx @@ -0,0 +1,44 @@ +"use client"; + +import React from "react"; +import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; +import { RackStructureDefinition } from "./index"; +import { RackStructureComponent } from "./RackStructureComponent"; +import { GeneratedLocation } from "./types"; + +/** + * 렉 구조 설정 렌더러 + * 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록 + */ +export class RackStructureRenderer extends AutoRegisteringComponentRenderer { + static componentDefinition = RackStructureDefinition; + + render(): React.ReactElement { + const { formData, isPreview, config } = this.props as any; + + return ( + + ); + } + + /** + * 생성된 위치 데이터 변경 핸들러 + */ + protected handleLocationsChange = (locations: GeneratedLocation[]) => { + // 생성된 위치 데이터를 formData에 저장 + this.updateComponent({ generatedLocations: locations }); + }; +} + +// 자동 등록 실행 +RackStructureRenderer.registerSelf(); + +// Hot Reload 지원 (개발 모드) +if (process.env.NODE_ENV === "development") { + RackStructureRenderer.enableHotReload(); +} diff --git a/frontend/lib/registry/components/rack-structure/config.ts b/frontend/lib/registry/components/rack-structure/config.ts new file mode 100644 index 00000000..09d9d04b --- /dev/null +++ b/frontend/lib/registry/components/rack-structure/config.ts @@ -0,0 +1,27 @@ +/** + * 렉 구조 컴포넌트 기본 설정 + */ + +import { RackStructureComponentConfig } from "./types"; + +export const defaultConfig: RackStructureComponentConfig = { + // 기본 제한 + maxConditions: 10, + maxRows: 99, + maxLevels: 20, + + // 기본 코드 패턴 + codePattern: "{warehouseCode}-{floor}{zone}-{row:02d}-{level}", + namePattern: "{zone}구역-{row:02d}열-{level}단", + + // UI 설정 + showTemplates: true, + showPreview: true, + showStatistics: true, + readonly: false, + + // 초기 조건 없음 + initialConditions: [], +}; + + diff --git a/frontend/lib/registry/components/rack-structure/index.ts b/frontend/lib/registry/components/rack-structure/index.ts new file mode 100644 index 00000000..a84cc4c6 --- /dev/null +++ b/frontend/lib/registry/components/rack-structure/index.ts @@ -0,0 +1,74 @@ +"use client"; + +import React from "react"; +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { ComponentCategory } from "@/types/component"; +import { RackStructureWrapper } from "./RackStructureComponent"; +import { RackStructureConfigPanel } from "./RackStructureConfigPanel"; +import { defaultConfig } from "./config"; + +/** + * 렉 구조 컴포넌트 정의 + * 창고 렉 위치를 일괄 생성하기 위한 구조 설정 컴포넌트 + */ +export const RackStructureDefinition = createComponentDefinition({ + id: "rack-structure", + name: "렉 구조 설정", + nameEng: "Rack Structure Config", + description: "창고 렉 위치를 열 범위와 단 수로 일괄 생성하는 컴포넌트", + category: ComponentCategory.INPUT, + webType: "component", + component: RackStructureWrapper, + defaultConfig: defaultConfig, + defaultSize: { + width: 1200, + height: 800, + gridColumnSpan: "12", + }, + configPanel: RackStructureConfigPanel, + icon: "LayoutGrid", + tags: ["창고", "렉", "위치", "구조", "일괄생성", "WMS"], + version: "1.0.0", + author: "개발팀", + documentation: ` +창고 렉 위치를 일괄 생성하기 위한 구조 설정 컴포넌트입니다. + +## 주요 기능 +- 조건별 열 범위 및 단 수 설정 +- 자동 위치 코드/이름 생성 +- 미리보기 및 통계 표시 +- 템플릿 저장/불러오기 + +## 사용 방법 +1. 상위 폼에서 창고, 층, 구역 정보 선택 +2. 조건 추가 버튼으로 렉 라인 조건 생성 +3. 각 조건의 열 범위와 단 수 입력 +4. 미리보기 생성으로 결과 확인 +5. 저장 시 생성된 위치 데이터가 함께 저장됨 + +## 컨텍스트 데이터 +formData에서 다음 필드를 자동으로 읽어옵니다: +- warehouse_id / warehouseId: 창고 ID +- warehouse_code / warehouseCode: 창고 코드 +- floor: 층 +- zone: 구역 +- location_type / locationType: 위치 유형 +- status: 사용 여부 + `, +}); + +// 타입 내보내기 +export type { + RackStructureComponentConfig, + RackStructureContext, + RackLineCondition, + RackStructureTemplate, + GeneratedLocation, +} from "./types"; + +// 컴포넌트 내보내기 +export { RackStructureComponent, RackStructureWrapper } from "./RackStructureComponent"; +export { RackStructureRenderer } from "./RackStructureRenderer"; +export { RackStructureConfigPanel } from "./RackStructureConfigPanel"; + + diff --git a/frontend/lib/registry/components/rack-structure/types.ts b/frontend/lib/registry/components/rack-structure/types.ts new file mode 100644 index 00000000..485a2208 --- /dev/null +++ b/frontend/lib/registry/components/rack-structure/types.ts @@ -0,0 +1,91 @@ +/** + * 렉 구조 컴포넌트 타입 정의 + */ + +// 렉 라인 조건 (열 범위 + 단 수) +export interface RackLineCondition { + id: string; + startRow: number; // 시작 열 + endRow: number; // 종료 열 + levels: number; // 단 수 +} + +// 렉 구조 템플릿 +export interface RackStructureTemplate { + id: string; + name: string; + conditions: RackLineCondition[]; + createdAt?: string; +} + +// 생성될 위치 데이터 +export interface GeneratedLocation { + rowNum: number; // 열 번호 + levelNum: number; // 단 번호 + locationCode: string; // 위치 코드 (예: WH001-1A-01-1) + locationName: string; // 위치명 (예: A구역-01열-1단) + locationType?: string; // 위치 유형 + status?: string; // 사용 여부 + // 추가 필드 (상위 폼에서 매핑된 값) + warehouseCode?: string; + floor?: string; + zone?: string; +} + +// 필드 매핑 설정 (상위 폼의 어떤 필드를 사용할지) +export interface FieldMapping { + warehouseCodeField?: string; // 창고 코드로 사용할 폼 필드명 + warehouseNameField?: string; // 창고명으로 사용할 폼 필드명 + floorField?: string; // 층으로 사용할 폼 필드명 + zoneField?: string; // 구역으로 사용할 폼 필드명 + locationTypeField?: string; // 위치 유형으로 사용할 폼 필드명 + statusField?: string; // 사용 여부로 사용할 폼 필드명 +} + +// 컴포넌트 설정 +export interface RackStructureComponentConfig { + // 기본 설정 + maxConditions?: number; // 최대 조건 수 (기본: 10) + maxRows?: number; // 최대 열 수 (기본: 99) + maxLevels?: number; // 최대 단 수 (기본: 20) + + // 필드 매핑 (상위 폼의 필드와 연결) + fieldMapping?: FieldMapping; + + // 위치 코드 생성 규칙 + codePattern?: string; // 코드 패턴 (예: "{warehouse}-{floor}{zone}-{row:02d}-{level}") + namePattern?: string; // 이름 패턴 (예: "{zone}구역-{row:02d}열-{level}단") + + // UI 설정 + showTemplates?: boolean; // 템플릿 기능 표시 + showPreview?: boolean; // 미리보기 표시 + showStatistics?: boolean; // 통계 카드 표시 + readonly?: boolean; // 읽기 전용 + + // 초기값 + initialConditions?: RackLineCondition[]; +} + +// 상위 폼에서 전달받는 컨텍스트 데이터 +export interface RackStructureContext { + warehouseId?: string; // 창고 ID + warehouseCode?: string; // 창고 코드 (예: WH001) + warehouseName?: string; // 창고명 (예: 제1창고) + floor?: string; // 층 (예: 1) + zone?: string; // 구역 (예: A) + locationType?: string; // 위치 유형 (예: 선반) + status?: string; // 사용 여부 (예: 사용) +} + +// 컴포넌트 Props +export interface RackStructureComponentProps { + config: RackStructureComponentConfig; + context?: RackStructureContext; + formData?: Record; // 상위 폼 데이터 (필드 매핑에 사용) + onChange?: (locations: GeneratedLocation[]) => void; + onConditionsChange?: (conditions: RackLineCondition[]) => void; + isPreview?: boolean; + tableName?: string; +} + + diff --git a/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md b/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md index 313a7567..29da36f1 100644 --- a/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md +++ b/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md @@ -1682,3 +1682,4 @@ const 출고등록_설정: ScreenSplitPanel = { + diff --git a/화면_임베딩_시스템_Phase1-4_구현_완료.md b/화면_임베딩_시스템_Phase1-4_구현_완료.md index 373b6ec7..2f382cb3 100644 --- a/화면_임베딩_시스템_Phase1-4_구현_완료.md +++ b/화면_임베딩_시스템_Phase1-4_구현_완료.md @@ -529,3 +529,4 @@ const { data: config } = await getScreenSplitPanel(screenId); + diff --git a/화면_임베딩_시스템_충돌_분석_보고서.md b/화면_임베딩_시스템_충돌_분석_보고서.md index 5d315706..8e4cdbd2 100644 --- a/화면_임베딩_시스템_충돌_분석_보고서.md +++ b/화면_임베딩_시스템_충돌_분석_보고서.md @@ -516,3 +516,4 @@ function ScreenViewPage() { +