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:
commit
16a1c459fc
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue