"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. 2 각 조건마다 열 범위와 단 수를 입력하세요
  3. 3 예시: 조건1(1~3열, 3단), 조건2(4~6열, 5단)
{/* 조건 목록 또는 빈 상태 */} {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 (
); };