Merge pull request '야드관리 수정' (#109) from feat/dashboard into main

Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/109
This commit is contained in:
hyeonsu 2025-10-20 11:55:08 +09:00
commit 16a1c459fc
5 changed files with 352 additions and 178 deletions

View File

@ -153,18 +153,35 @@ export class YardLayoutService {
`; `;
const pool = getPool(); const pool = getPool();
// NaN 방지를 위한 안전한 변환 함수
const safeParseInt = (
value: any,
defaultValue: number | null = null
): number | null => {
if (!value && value !== 0) return defaultValue;
const parsed = parseInt(String(value), 10);
return isNaN(parsed) ? defaultValue : parsed;
};
const safeParseFloat = (value: any, defaultValue: number): number => {
if (!value && value !== 0) return defaultValue;
const parsed = parseFloat(String(value));
return isNaN(parsed) ? defaultValue : parsed;
};
const result = await pool.query(query, [ const result = await pool.query(query, [
layoutId, layoutId,
data.material_code || null, data.material_code || null,
data.material_name || null, data.material_name || null,
data.quantity || null, safeParseInt(data.quantity, null),
data.unit || null, data.unit || null,
data.position_x || 0, safeParseFloat(data.position_x, 0),
data.position_y || 0, safeParseFloat(data.position_y, 0),
data.position_z || 0, safeParseFloat(data.position_z, 0),
data.size_x || 5, safeParseFloat(data.size_x, 5),
data.size_y || 5, safeParseFloat(data.size_y, 5),
data.size_z || 5, safeParseFloat(data.size_z, 5),
data.color || "#9ca3af", // 미설정 시 회색 data.color || "#9ca3af", // 미설정 시 회색
data.data_source_type || null, data.data_source_type || null,
data.data_source_config ? JSON.stringify(data.data_source_config) : null, data.data_source_config ? JSON.stringify(data.data_source_config) : null,
@ -201,17 +218,31 @@ export class YardLayoutService {
`; `;
const pool = getPool(); const pool = getPool();
// NaN 방지를 위한 안전한 변환 함수
const safeParseInt = (value: any): number | null => {
if (value === null || value === undefined) return null;
const parsed = parseInt(String(value), 10);
return isNaN(parsed) ? null : parsed;
};
const safeParseFloat = (value: any): number | null => {
if (value === null || value === undefined) return null;
const parsed = parseFloat(String(value));
return isNaN(parsed) ? null : parsed;
};
const result = await pool.query(query, [ const result = await pool.query(query, [
data.material_code !== undefined ? data.material_code : null, data.material_code !== undefined ? data.material_code : null,
data.material_name !== undefined ? data.material_name : null, data.material_name !== undefined ? data.material_name : null,
data.quantity !== undefined ? data.quantity : null, data.quantity !== undefined ? safeParseInt(data.quantity) : null,
data.unit !== undefined ? data.unit : null, data.unit !== undefined ? data.unit : null,
data.position_x !== undefined ? data.position_x : null, data.position_x !== undefined ? safeParseFloat(data.position_x) : null,
data.position_y !== undefined ? data.position_y : null, data.position_y !== undefined ? safeParseFloat(data.position_y) : null,
data.position_z !== undefined ? data.position_z : null, data.position_z !== undefined ? safeParseFloat(data.position_z) : null,
data.size_x !== undefined ? data.size_x : null, data.size_x !== undefined ? safeParseFloat(data.size_x) : null,
data.size_y !== undefined ? data.size_y : null, data.size_y !== undefined ? safeParseFloat(data.size_y) : null,
data.size_z !== undefined ? data.size_z : null, data.size_z !== undefined ? safeParseFloat(data.size_z) : null,
data.color !== undefined ? data.color : null, data.color !== undefined ? data.color : null,
data.data_source_type !== undefined ? data.data_source_type : null, data.data_source_type !== undefined ? data.data_source_type : null,
data.data_source_config !== undefined data.data_source_config !== undefined

View File

@ -6,6 +6,7 @@ import { DashboardCanvas } from "./DashboardCanvas";
import { DashboardTopMenu } from "./DashboardTopMenu"; import { DashboardTopMenu } from "./DashboardTopMenu";
import { ElementConfigModal } from "./ElementConfigModal"; import { ElementConfigModal } from "./ElementConfigModal";
import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal"; import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal";
import { YardWidgetConfigModal } from "./widgets/YardWidgetConfigModal";
import { DashboardSaveModal } from "./DashboardSaveModal"; import { DashboardSaveModal } from "./DashboardSaveModal";
import { DashboardElement, ElementType, ElementSubtype } from "./types"; import { DashboardElement, ElementType, ElementSubtype } from "./types";
import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils"; import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils";
@ -495,6 +496,13 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
onClose={closeConfigModal} onClose={closeConfigModal}
onSave={saveListWidgetConfig} onSave={saveListWidgetConfig}
/> />
) : configModalElement.type === "widget" && configModalElement.subtype === "yard-management-3d" ? (
<YardWidgetConfigModal
element={configModalElement}
isOpen={true}
onClose={closeConfigModal}
onSave={saveListWidgetConfig}
/>
) : ( ) : (
<ElementConfigModal <ElementConfigModal
element={configModalElement} element={configModalElement}

View File

@ -0,0 +1,79 @@
"use client";
import { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { DashboardElement } from "../types";
interface YardWidgetConfigModalProps {
element: DashboardElement;
isOpen: boolean;
onClose: () => void;
onSave: (updates: Partial<DashboardElement>) => void;
}
export function YardWidgetConfigModal({ element, isOpen, onClose, onSave }: YardWidgetConfigModalProps) {
const [customTitle, setCustomTitle] = useState(element.customTitle || "");
const [showHeader, setShowHeader] = useState(element.showHeader !== false);
useEffect(() => {
if (isOpen) {
setCustomTitle(element.customTitle || "");
setShowHeader(element.showHeader !== false);
}
}, [isOpen, element]);
const handleSave = () => {
onSave({
customTitle,
showHeader,
});
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent onPointerDown={(e) => e.stopPropagation()} className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
{/* 위젯 제목 */}
<div className="space-y-2">
<Label htmlFor="customTitle"> </Label>
<Input
id="customTitle"
value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)}
placeholder="제목을 입력하세요 (비워두면 기본 제목 사용)"
/>
<p className="text-xs text-gray-500"> 제목: 야드 3D</p>
</div>
{/* 헤더 표시 여부 */}
<div className="flex items-center space-x-2">
<Checkbox
id="showHeader"
checked={showHeader}
onCheckedChange={(checked) => setShowHeader(checked === true)}
/>
<Label htmlFor="showHeader" className="cursor-pointer text-sm font-normal">
</Label>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose}>
</Button>
<Button onClick={handleSave}></Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -7,7 +7,8 @@ import { yardLayoutApi } from "@/lib/api/yardLayoutApi";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { YardLayout, YardPlacement } from "./types"; import { YardLayout, YardPlacement } from "./types";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react"; import { AlertCircle, CheckCircle } from "lucide-react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), { const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), {
ssr: false, ssr: false,
@ -31,11 +32,19 @@ interface YardEditorProps {
export default function YardEditor({ layout, onBack }: YardEditorProps) { export default function YardEditor({ layout, onBack }: YardEditorProps) {
const [placements, setPlacements] = useState<YardPlacement[]>([]); const [placements, setPlacements] = useState<YardPlacement[]>([]);
const [originalPlacements, setOriginalPlacements] = useState<YardPlacement[]>([]); // 원본 데이터 보관
const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null); const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [showConfigPanel, setShowConfigPanel] = useState(false); const [showConfigPanel, setShowConfigPanel] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(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: "" });
// 배치 목록 로드 // 배치 목록 로드
useEffect(() => { useEffect(() => {
@ -44,7 +53,9 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
setIsLoading(true); setIsLoading(true);
const response = await yardLayoutApi.getPlacementsByLayoutId(layout.id); const response = await yardLayoutApi.getPlacementsByLayoutId(layout.id);
if (response.success) { if (response.success) {
setPlacements(response.data as YardPlacement[]); const loadedData = response.data as YardPlacement[];
setPlacements(loadedData);
setOriginalPlacements(JSON.parse(JSON.stringify(loadedData))); // 깊은 복사
} }
} catch (error) { } catch (error) {
console.error("배치 목록 로드 실패:", error); console.error("배치 목록 로드 실패:", error);
@ -57,36 +68,34 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
loadPlacements(); loadPlacements();
}, [layout.id]); }, [layout.id]);
// 빈 요소 추가 // 빈 요소 추가 (로컬 상태에만 추가, 저장 시 서버에 반영)
const handleAddElement = async () => { const handleAddElement = () => {
try { const newPlacement: YardPlacement = {
const newPlacementData = { id: nextPlacementId, // 임시 음수 ID
position_x: 0, yard_layout_id: layout.id,
position_y: 0, material_code: null,
position_z: 0, material_name: null,
size_x: 5, quantity: null,
size_y: 5, unit: null,
size_z: 5, position_x: 0,
color: "#9ca3af", // 회색 (미설정 상태) 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(),
};
console.log("요소 추가 요청:", { layoutId: layout.id, data: newPlacementData }); setPlacements((prev) => [...prev, newPlacement]);
const response = await yardLayoutApi.addMaterialPlacement(layout.id, newPlacementData); setSelectedPlacement(newPlacement);
console.log("요소 추가 응답:", response); setShowConfigPanel(true);
setHasUnsavedChanges(true);
if (response.success) { setNextPlacementId((prev) => prev - 1); // 다음 임시 ID
const newPlacement = response.data as YardPlacement;
setPlacements((prev) => [...prev, newPlacement]);
setSelectedPlacement(newPlacement);
setShowConfigPanel(true); // 자동으로 설정 패널 표시
} else {
console.error("요소 추가 실패 (응답):", response);
setError(response.message || "요소 추가에 실패했습니다.");
}
} catch (error) {
console.error("요소 추가 실패 (예외):", error);
setError(`요소 추가에 실패했습니다: ${error instanceof Error ? error.message : String(error)}`);
}
}; };
// 요소 선택 (3D 캔버스 또는 목록에서) // 요소 선택 (3D 캔버스 또는 목록에서)
@ -101,28 +110,21 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
setShowConfigPanel(true); setShowConfigPanel(true);
}; };
// 요소 삭제 // 요소 삭제 (로컬 상태에서만 삭제, 저장 시 서버에 반영)
const handleDeletePlacement = async (placementId: number) => { const handleDeletePlacement = (placementId: number) => {
if (!confirm("이 요소를 삭제하시겠습니까?")) { if (!confirm("이 요소를 삭제하시겠습니까?")) {
return; return;
} }
try { setPlacements((prev) => prev.filter((p) => p.id !== placementId));
const response = await yardLayoutApi.removePlacement(placementId); if (selectedPlacement?.id === placementId) {
if (response.success) { setSelectedPlacement(null);
setPlacements((prev) => prev.filter((p) => p.id !== placementId)); setShowConfigPanel(false);
if (selectedPlacement?.id === placementId) {
setSelectedPlacement(null);
setShowConfigPanel(false);
}
}
} catch (error) {
console.error("요소 삭제 실패:", error);
setError("요소 삭제에 실패했습니다.");
} }
setHasUnsavedChanges(true);
}; };
// 자재 드래그 (3D 캔버스에서) // 자재 드래그 (3D 캔버스에서, 로컬 상태에만 반영)
const handlePlacementDrag = (id: number, position: { x: number; y: number; z: number }) => { const handlePlacementDrag = (id: number, position: { x: number; y: number; z: number }) => {
const updatedPosition = { const updatedPosition = {
position_x: Math.round(position.x * 2) / 2, position_x: Math.round(position.x * 2) / 2,
@ -135,76 +137,119 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
if (selectedPlacement?.id === id) { if (selectedPlacement?.id === id) {
setSelectedPlacement((prev) => (prev ? { ...prev, ...updatedPosition } : null)); setSelectedPlacement((prev) => (prev ? { ...prev, ...updatedPosition } : null));
} }
setHasUnsavedChanges(true);
}; };
// 저장 // 전체 저장 (신규/수정/삭제 모두 처리)
const handleSave = async () => { const handleSave = async () => {
setIsSaving(true); setIsSaving(true);
try { try {
const response = await yardLayoutApi.batchUpdatePlacements( // 1. 삭제된 요소 처리 (원본에는 있지만 현재 state에 없는 경우)
layout.id, const deletedIds = originalPlacements
placements.map((p) => ({ .filter((orig) => !placements.find((p) => p.id === orig.id))
id: p.id, .map((p) => p.id);
position_x: p.position_x,
position_y: p.position_y,
position_z: p.position_z,
size_x: p.size_x,
size_y: p.size_y,
size_z: p.size_z,
color: p.color,
})),
);
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) { if (response.success) {
alert("저장되었습니다"); 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) { } catch (error) {
console.error("저장 실패:", error); console.error("저장 실패:", error);
alert("저장에 실패했습니다"); setSaveResultDialog({
open: true,
success: false,
message: `저장에 실패했습니다: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
});
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}; };
// 설정 패널에서 저장 // 설정 패널에서 데이터 업데이트 (로컬 상태에만 반영, 서버 저장 안함)
const handleSaveConfig = async (updatedData: Partial<YardPlacement>) => { const handleSaveConfig = (updatedData: Partial<YardPlacement>) => {
if (!selectedPlacement) return; if (!selectedPlacement) return;
try { // 로컬 상태만 업데이트
const response = await yardLayoutApi.updatePlacement(selectedPlacement.id, updatedData); setPlacements((prev) =>
if (response.success) { prev.map((p) => {
const updated = response.data as YardPlacement; if (p.id === selectedPlacement.id) {
return { ...p, ...updatedData };
}
return p;
}),
);
// 현재 위치 정보를 유지하면서 업데이트 // 선택된 요소도 업데이트
setPlacements((prev) => setSelectedPlacement((prev) => (prev ? { ...prev, ...updatedData } : null));
prev.map((p) => { setShowConfigPanel(false);
if (p.id === updated.id) { setHasUnsavedChanges(true);
// 현재 프론트엔드 상태의 위치를 유지
return {
...updated,
position_x: p.position_x,
position_y: p.position_y,
position_z: p.position_z,
};
}
return p;
}),
);
// 선택된 요소도 동일하게 업데이트
setSelectedPlacement({
...updated,
position_x: selectedPlacement.position_x,
position_y: selectedPlacement.position_y,
position_z: selectedPlacement.position_z,
});
setShowConfigPanel(false);
}
} catch (error) {
console.error("설정 저장 실패:", error);
setError("설정 저장에 실패했습니다.");
}
}; };
// 요소가 설정되었는지 확인 // 요소가 설정되었는지 확인
@ -227,19 +272,22 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
</div> </div>
</div> </div>
<Button size="sm" onClick={handleSave} disabled={isSaving}> <div className="flex items-center gap-2">
{isSaving ? ( {hasUnsavedChanges && <span className="text-sm font-medium text-orange-600"> </span>}
<> <Button size="sm" onClick={handleSave} disabled={isSaving || !hasUnsavedChanges}>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> {isSaving ? (
... <>
</> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : ( ...
<> </>
<Save className="mr-2 h-4 w-4" /> ) : (
<>
</> <Save className="mr-2 h-4 w-4" />
)}
</Button> </>
)}
</Button>
</div>
</div> </div>
{/* 에러 메시지 */} {/* 에러 메시지 */}
@ -369,6 +417,31 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
)} )}
</div> </div>
</div> </div>
{/* 저장 결과 Dialog */}
<Dialog open={saveResultDialog.open} onOpenChange={(open) => setSaveResultDialog((prev) => ({ ...prev, open }))}>
<DialogContent onPointerDown={(e) => e.stopPropagation()}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{saveResultDialog.success ? (
<>
<CheckCircle className="h-5 w-5 text-green-600" />
</>
) : (
<>
<AlertCircle className="h-5 w-5 text-red-600" />
</>
)}
</DialogTitle>
<DialogDescription className="pt-2">{saveResultDialog.message}</DialogDescription>
</DialogHeader>
<div className="flex justify-end">
<Button onClick={() => setSaveResultDialog((prev) => ({ ...prev, open: false }))}></Button>
</div>
</DialogContent>
</Dialog>
</div> </div>
); );
} }

View File

@ -18,7 +18,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
interface YardElementConfigPanelProps { interface YardElementConfigPanelProps {
placement: YardPlacement; placement: YardPlacement;
onSave: (updatedData: Partial<YardPlacement>) => Promise<void>; onSave: (updatedData: Partial<YardPlacement>) => void; // Promise 제거 (즉시 로컬 상태 업데이트)
onCancel: () => void; onCancel: () => void;
} }
@ -52,9 +52,8 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
const [sizeY, setSizeY] = useState(placement.size_y); const [sizeY, setSizeY] = useState(placement.size_y);
const [sizeZ, setSizeZ] = useState(placement.size_z); const [sizeZ, setSizeZ] = useState(placement.size_z);
// 에러 및 로딩 // 에러
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
// 외부 DB 커넥션 목록 로드 // 외부 DB 커넥션 목록 로드
useEffect(() => { useEffect(() => {
@ -180,8 +179,8 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
} }
}; };
// 저장 // 적용 (로컬 상태만 업데이트, 서버 저장은 나중에 일괄 처리)
const handleSave = async () => { const handleApply = () => {
// 검증 // 검증
if (!queryResult) { if (!queryResult) {
setError("먼저 데이터를 조회해주세요."); setError("먼저 데이터를 조회해주세요.");
@ -203,49 +202,40 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
return; return;
} }
setIsSaving(true); const selectedRow = queryResult.rows[selectedRowIndex];
const materialName = selectedRow[materialNameField];
const quantity = selectedRow[quantityField];
try { const dataSourceConfig: YardDataSourceConfig = {
const selectedRow = queryResult.rows[selectedRowIndex]; type: dataSourceType,
const materialName = selectedRow[materialNameField]; query: dataSourceType !== "rest_api" ? query : undefined,
const quantity = selectedRow[quantityField]; connectionId: dataSourceType === "external_db" ? parseInt(externalConnectionId) : undefined,
url: dataSourceType === "rest_api" ? apiUrl : undefined,
method: dataSourceType === "rest_api" ? apiMethod : undefined,
dataPath: dataSourceType === "rest_api" && apiDataPath ? apiDataPath : undefined,
};
const dataSourceConfig: YardDataSourceConfig = { const dataBinding: YardDataBinding = {
type: dataSourceType, selectedRowIndex,
query: dataSourceType !== "rest_api" ? query : undefined, materialNameField,
connectionId: dataSourceType === "external_db" ? parseInt(externalConnectionId) : undefined, quantityField,
url: dataSourceType === "rest_api" ? apiUrl : undefined, unit: unit.trim(),
method: dataSourceType === "rest_api" ? apiMethod : undefined, };
dataPath: dataSourceType === "rest_api" && apiDataPath ? apiDataPath : undefined,
};
const dataBinding: YardDataBinding = { const updatedData: Partial<YardPlacement> = {
selectedRowIndex, material_name: String(materialName),
materialNameField, quantity: Number(quantity),
quantityField, unit: unit.trim(),
unit: unit.trim(), color,
}; size_x: sizeX,
size_y: sizeY,
size_z: sizeZ,
data_source_type: dataSourceType,
data_source_config: dataSourceConfig,
data_binding: dataBinding,
};
const updatedData: Partial<YardPlacement> = { onSave(updatedData); // 동기적으로 즉시 로컬 상태 업데이트
material_name: String(materialName),
quantity: Number(quantity),
unit: unit.trim(),
color,
size_x: sizeX,
size_y: sizeY,
size_z: sizeZ,
data_source_type: dataSourceType,
data_source_config: dataSourceConfig,
data_binding: dataBinding,
};
await onSave(updatedData);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "저장 중 오류가 발생했습니다.";
setError(errorMessage);
} finally {
setIsSaving(false);
}
}; };
return ( return (
@ -537,15 +527,8 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
<Button variant="outline" onClick={onCancel} className="flex-1"> <Button variant="outline" onClick={onCancel} className="flex-1">
</Button> </Button>
<Button onClick={handleSave} disabled={isSaving || !queryResult} className="flex-1"> <Button onClick={handleApply} disabled={!queryResult} className="flex-1">
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
"저장"
)}
</Button> </Button>
</div> </div>
</div> </div>