"use client"; import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { ArrowLeft, Save, Loader2, Plus, Settings, Trash2, Edit2 } from "lucide-react"; import { yardLayoutApi } from "@/lib/api/yardLayoutApi"; import dynamic from "next/dynamic"; import { YardLayout, YardPlacement } from "./types"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertCircle, CheckCircle } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), { ssr: false, loading: () => (
), }); // 나중에 구현할 데이터 바인딩 패널 const YardElementConfigPanel = dynamic(() => import("./YardElementConfigPanel"), { ssr: false, loading: () =>
로딩 중...
, }); interface YardEditorProps { layout: YardLayout; onBack: () => void; } export default function YardEditor({ layout, onBack }: YardEditorProps) { const [placements, setPlacements] = useState([]); const [originalPlacements, setOriginalPlacements] = useState([]); // 원본 데이터 보관 const [selectedPlacement, setSelectedPlacement] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [showConfigPanel, setShowConfigPanel] = useState(false); const [error, setError] = useState(null); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // 미저장 변경사항 추적 const [nextPlacementId, setNextPlacementId] = useState(-1); // 임시 ID (음수 사용) const [saveResultDialog, setSaveResultDialog] = useState<{ open: boolean; success: boolean; message: string; }>({ open: false, success: false, message: "" }); const [deleteConfirmDialog, setDeleteConfirmDialog] = useState<{ open: boolean; placementId: number | null; }>({ open: false, placementId: null }); const [editLayoutDialog, setEditLayoutDialog] = useState<{ open: boolean; name: string; }>({ open: false, name: "" }); // 배치 목록 로드 useEffect(() => { const loadPlacements = async () => { try { setIsLoading(true); const response = await yardLayoutApi.getPlacementsByLayoutId(layout.id); if (response.success) { const loadedData = response.data as YardPlacement[]; setPlacements(loadedData); setOriginalPlacements(JSON.parse(JSON.stringify(loadedData))); // 깊은 복사 } } catch (error) { console.error("배치 목록 로드 실패:", error); setError("배치 목록을 불러올 수 없습니다."); } finally { setIsLoading(false); } }; loadPlacements(); }, [layout.id]); // 빈 요소 추가 (로컬 상태에만 추가, 저장 시 서버에 반영) const handleAddElement = () => { const newPlacement: YardPlacement = { id: nextPlacementId, // 임시 음수 ID yard_layout_id: layout.id, material_code: null, material_name: null, quantity: null, unit: null, position_x: 0, position_y: 2.5, position_z: 0, size_x: 5, size_y: 5, size_z: 5, color: "#9ca3af", data_source_type: null, data_source_config: null, data_binding: null, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; setPlacements((prev) => [...prev, newPlacement]); setSelectedPlacement(newPlacement); setShowConfigPanel(true); setHasUnsavedChanges(true); setNextPlacementId((prev) => prev - 1); // 다음 임시 ID }; // 요소 선택 (3D 캔버스 또는 목록에서) const handleSelectPlacement = (placement: YardPlacement) => { setSelectedPlacement(placement); setShowConfigPanel(false); // 선택 시에는 설정 패널 닫기 }; // 설정 버튼 클릭 const handleConfigClick = (placement: YardPlacement) => { setSelectedPlacement(placement); setShowConfigPanel(true); }; // 요소 삭제 확인 Dialog 열기 const handleDeletePlacement = (placementId: number) => { setDeleteConfirmDialog({ open: true, placementId }); }; // 요소 삭제 확정 (로컬 상태에서만 삭제, 저장 시 서버에 반영) const confirmDeletePlacement = () => { const { placementId } = deleteConfirmDialog; if (placementId === null) return; setPlacements((prev) => prev.filter((p) => p.id !== placementId)); if (selectedPlacement?.id === placementId) { setSelectedPlacement(null); setShowConfigPanel(false); } setHasUnsavedChanges(true); setDeleteConfirmDialog({ open: false, placementId: null }); }; // 자재 드래그 (3D 캔버스에서, 로컬 상태에만 반영) const handlePlacementDrag = (id: number, position: { x: number; y: number; z: number }) => { const updatedPosition = { position_x: Math.round(position.x * 2) / 2, position_y: position.y, position_z: Math.round(position.z * 2) / 2, }; setPlacements((prev) => prev.map((p) => (p.id === id ? { ...p, ...updatedPosition } : p))); if (selectedPlacement?.id === id) { setSelectedPlacement((prev) => (prev ? { ...prev, ...updatedPosition } : null)); } setHasUnsavedChanges(true); }; // 전체 저장 (신규/수정/삭제 모두 처리) const handleSave = async () => { setIsSaving(true); try { // 1. 삭제된 요소 처리 (원본에는 있지만 현재 state에 없는 경우) const deletedIds = originalPlacements .filter((orig) => !placements.find((p) => p.id === orig.id)) .map((p) => p.id); for (const id of deletedIds) { await yardLayoutApi.removePlacement(id); } // 2. 신규 추가 요소 처리 (ID가 음수인 경우) const newPlacements = placements.filter((p) => p.id < 0); const addedPlacements: YardPlacement[] = []; for (const newPlacement of newPlacements) { const response = await yardLayoutApi.addMaterialPlacement(layout.id, { material_code: newPlacement.material_code, material_name: newPlacement.material_name, quantity: newPlacement.quantity, unit: newPlacement.unit, position_x: newPlacement.position_x, position_y: newPlacement.position_y, position_z: newPlacement.position_z, size_x: newPlacement.size_x, size_y: newPlacement.size_y, size_z: newPlacement.size_z, color: newPlacement.color, data_source_type: newPlacement.data_source_type, data_source_config: newPlacement.data_source_config, data_binding: newPlacement.data_binding, memo: newPlacement.memo, }); if (response.success) { addedPlacements.push(response.data as YardPlacement); } } // 3. 기존 요소 수정 처리 (ID가 양수인 경우) const existingPlacements = placements.filter((p) => p.id > 0); for (const placement of existingPlacements) { await yardLayoutApi.updatePlacement(placement.id, { material_code: placement.material_code, material_name: placement.material_name, quantity: placement.quantity, unit: placement.unit, position_x: placement.position_x, position_y: placement.position_y, position_z: placement.position_z, size_x: placement.size_x, size_y: placement.size_y, size_z: placement.size_z, color: placement.color, data_source_type: placement.data_source_type, data_source_config: placement.data_source_config, data_binding: placement.data_binding, memo: placement.memo, }); } // 4. 저장 성공 후 데이터 다시 로드 const response = await yardLayoutApi.getPlacementsByLayoutId(layout.id); if (response.success) { const loadedData = response.data as YardPlacement[]; setPlacements(loadedData); setOriginalPlacements(JSON.parse(JSON.stringify(loadedData))); setHasUnsavedChanges(false); setSelectedPlacement(null); setShowConfigPanel(false); setSaveResultDialog({ open: true, success: true, message: "모든 변경사항이 성공적으로 저장되었습니다.", }); } } catch (error) { console.error("저장 실패:", error); setSaveResultDialog({ open: true, success: false, message: `저장에 실패했습니다: ${error instanceof Error ? error.message : "알 수 없는 오류"}`, }); } finally { setIsSaving(false); } }; // 설정 패널에서 데이터 업데이트 (로컬 상태에만 반영, 서버 저장 안함) const handleSaveConfig = (updatedData: Partial) => { if (!selectedPlacement) return; // 로컬 상태만 업데이트 setPlacements((prev) => prev.map((p) => { if (p.id === selectedPlacement.id) { return { ...p, ...updatedData }; } return p; }), ); // 선택된 요소도 업데이트 setSelectedPlacement((prev) => (prev ? { ...prev, ...updatedData } : null)); setShowConfigPanel(false); setHasUnsavedChanges(true); }; // 요소가 설정되었는지 확인 const isConfigured = (placement: YardPlacement): boolean => { return !!(placement.material_name && placement.quantity && placement.unit); }; // 레이아웃 편집 Dialog 열기 const handleEditLayout = () => { setEditLayoutDialog({ open: true, name: layout.name, }); }; // 레이아웃 정보 저장 const handleSaveLayoutInfo = async () => { try { const response = await yardLayoutApi.updateLayout(layout.id, { name: editLayoutDialog.name, }); if (response.success) { // 레이아웃 정보 업데이트 layout.name = editLayoutDialog.name; setEditLayoutDialog({ open: false, name: "" }); } } catch (error) { console.error("레이아웃 정보 수정 실패:", error); setError("레이아웃 정보 수정에 실패했습니다."); } }; return (
{/* 상단 툴바 */}

{layout.name}

{layout.description &&

{layout.description}

}
{hasUnsavedChanges && 미저장 변경사항 있음}
{/* 에러 메시지 */} {error && ( {error} )} {/* 메인 컨텐츠 영역 */}
{/* 좌측: 3D 캔버스 */}
{isLoading ? (
) : ( handleSelectPlacement(placement as YardPlacement)} onPlacementDrag={handlePlacementDrag} /> )}
{/* 우측: 요소 목록 또는 설정 패널 */}
{showConfigPanel && selectedPlacement ? ( // 설정 패널 setShowConfigPanel(false)} /> ) : ( // 요소 목록

요소 목록

총 {placements.length}개

{placements.length === 0 ? (
요소가 없습니다.
{'위의 "요소 추가" 버튼을 클릭하세요.'}
) : (
{placements.map((placement) => { const configured = isConfigured(placement); const isSelected = selectedPlacement?.id === placement.id; return (
handleSelectPlacement(placement)} >
{configured ? ( <>
{placement.material_name}
수량: {placement.quantity} {placement.unit}
) : ( <>
요소 #{placement.id}
데이터 바인딩 설정 필요
)}
); })}
)}
)}
{/* 저장 결과 Dialog */} setSaveResultDialog((prev) => ({ ...prev, open }))}> e.stopPropagation()}> {saveResultDialog.success ? ( <> 저장 완료 ) : ( <> 저장 실패 )} {saveResultDialog.message}
{/* 삭제 확인 Dialog */} !open && setDeleteConfirmDialog({ open: false, placementId: null })} > e.stopPropagation()}> 요소 삭제 확인 이 요소를 삭제하시겠습니까?
저장 버튼을 눌러야 최종적으로 삭제됩니다.
{/* 레이아웃 편집 Dialog */} !open && setEditLayoutDialog({ open: false, name: "" })} > e.stopPropagation()}> 야드 레이아웃 정보 수정
setEditLayoutDialog((prev) => ({ ...prev, name: e.target.value }))} placeholder="레이아웃 이름을 입력하세요" />
); }