From 8932f61298814a1948b4081e520f26e6296bfcd2 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 20 Oct 2025 09:53:31 +0900 Subject: [PATCH] =?UTF-8?q?Phase=201-4=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/app.ts | 2 - .../src/controllers/MaterialController.ts | 68 --- backend-node/src/routes/materialRoutes.ts | 15 - backend-node/src/services/MaterialService.ts | 111 ----- .../src/services/YardLayoutService.ts | 75 ++- .../dashboard/widgets/yard-3d/YardEditor.tsx | 453 +++++++----------- .../admin/dashboard/widgets/yard-3d/types.ts | 86 ++++ frontend/lib/api/yardLayoutApi.ts | 25 - 8 files changed, 317 insertions(+), 518 deletions(-) delete mode 100644 backend-node/src/controllers/MaterialController.ts delete mode 100644 backend-node/src/routes/materialRoutes.ts delete mode 100644 backend-node/src/services/MaterialService.ts create mode 100644 frontend/components/admin/dashboard/widgets/yard-3d/types.ts diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 37965d00..0e41697f 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -56,7 +56,6 @@ import todoRoutes from "./routes/todoRoutes"; // To-Do 관리 import bookingRoutes from "./routes/bookingRoutes"; // 예약 요청 관리 import mapDataRoutes from "./routes/mapDataRoutes"; // 지도 데이터 관리 import yardLayoutRoutes from "./routes/yardLayoutRoutes"; // 야드 관리 3D -import materialRoutes from "./routes/materialRoutes"; // 자재 관리 import { BatchSchedulerService } from "./services/batchSchedulerService"; // import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석 // import batchRoutes from "./routes/batchRoutes"; // 임시 주석 @@ -207,7 +206,6 @@ app.use("/api/todos", todoRoutes); // To-Do 관리 app.use("/api/bookings", bookingRoutes); // 예약 요청 관리 app.use("/api/map-data", mapDataRoutes); // 지도 데이터 조회 app.use("/api/yard-layouts", yardLayoutRoutes); // 야드 관리 3D -app.use("/api/materials", materialRoutes); // 자재 관리 // app.use("/api/collections", collectionRoutes); // 임시 주석 // app.use("/api/batch", batchRoutes); // 임시 주석 // app.use('/api/users', userRoutes); diff --git a/backend-node/src/controllers/MaterialController.ts b/backend-node/src/controllers/MaterialController.ts deleted file mode 100644 index bcac72d4..00000000 --- a/backend-node/src/controllers/MaterialController.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Request, Response } from "express"; -import MaterialService from "../services/MaterialService"; - -export class MaterialController { - // 임시 자재 마스터 목록 조회 - async getTempMaterials(req: Request, res: Response) { - try { - const { search, category, page, limit } = req.query; - - const result = await MaterialService.getTempMaterials({ - search: search as string, - category: category as string, - page: page ? parseInt(page as string) : 1, - limit: limit ? parseInt(limit as string) : 20, - }); - - return res.json({ success: true, ...result }); - } catch (error: any) { - console.error("Error fetching temp materials:", error); - return res.status(500).json({ - success: false, - message: "자재 목록 조회 중 오류가 발생했습니다.", - error: error.message, - }); - } - } - - // 특정 자재 상세 조회 - async getTempMaterialByCode(req: Request, res: Response) { - try { - const { code } = req.params; - const material = await MaterialService.getTempMaterialByCode(code); - - if (!material) { - return res.status(404).json({ - success: false, - message: "자재를 찾을 수 없습니다.", - }); - } - - return res.json({ success: true, data: material }); - } catch (error: any) { - console.error("Error fetching temp material:", error); - return res.status(500).json({ - success: false, - message: "자재 조회 중 오류가 발생했습니다.", - error: error.message, - }); - } - } - - // 카테고리 목록 조회 - async getCategories(req: Request, res: Response) { - try { - const categories = await MaterialService.getCategories(); - return res.json({ success: true, data: categories }); - } catch (error: any) { - console.error("Error fetching categories:", error); - return res.status(500).json({ - success: false, - message: "카테고리 목록 조회 중 오류가 발생했습니다.", - error: error.message, - }); - } - } -} - -export default new MaterialController(); diff --git a/backend-node/src/routes/materialRoutes.ts b/backend-node/src/routes/materialRoutes.ts deleted file mode 100644 index a85e10f6..00000000 --- a/backend-node/src/routes/materialRoutes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import express from "express"; -import MaterialController from "../controllers/MaterialController"; -import { authenticateToken } from "../middleware/authMiddleware"; - -const router = express.Router(); - -// 모든 라우트에 인증 미들웨어 적용 -router.use(authenticateToken); - -// 임시 자재 마스터 관리 -router.get("/temp", MaterialController.getTempMaterials); -router.get("/temp/categories", MaterialController.getCategories); -router.get("/temp/:code", MaterialController.getTempMaterialByCode); - -export default router; diff --git a/backend-node/src/services/MaterialService.ts b/backend-node/src/services/MaterialService.ts deleted file mode 100644 index 0f316cdc..00000000 --- a/backend-node/src/services/MaterialService.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { getPool } from "../database/db"; - -export class MaterialService { - // 임시 자재 마스터 목록 조회 - async getTempMaterials(params: { - search?: string; - category?: string; - page?: number; - limit?: number; - }) { - const { search, category, page = 1, limit = 20 } = params; - const offset = (page - 1) * limit; - - let whereConditions: string[] = ["is_active = true"]; - const queryParams: any[] = []; - let paramIndex = 1; - - if (search) { - whereConditions.push( - `(material_code ILIKE $${paramIndex} OR material_name ILIKE $${paramIndex})` - ); - queryParams.push(`%${search}%`); - paramIndex++; - } - - if (category) { - whereConditions.push(`category = $${paramIndex}`); - queryParams.push(category); - paramIndex++; - } - - const whereClause = - whereConditions.length > 0 - ? `WHERE ${whereConditions.join(" AND ")}` - : ""; - - const pool = getPool(); - - // 전체 개수 조회 - const countQuery = `SELECT COUNT(*) as total FROM temp_material_master ${whereClause}`; - const countResult = await pool.query(countQuery, queryParams); - const total = parseInt(countResult.rows[0].total); - - // 데이터 조회 - const dataQuery = ` - SELECT - id, - material_code, - material_name, - category, - unit, - default_color, - description, - created_at - FROM temp_material_master - ${whereClause} - ORDER BY material_code ASC - LIMIT $${paramIndex} OFFSET $${paramIndex + 1} - `; - - queryParams.push(limit, offset); - const dataResult = await pool.query(dataQuery, queryParams); - - return { - data: dataResult.rows, - pagination: { - page, - limit, - total, - totalPages: Math.ceil(total / limit), - }, - }; - } - - // 특정 자재 상세 조회 - async getTempMaterialByCode(materialCode: string) { - const query = ` - SELECT - id, - material_code, - material_name, - category, - unit, - default_color, - description, - created_at - FROM temp_material_master - WHERE material_code = $1 AND is_active = true - `; - - const pool = getPool(); - const result = await pool.query(query, [materialCode]); - return result.rows[0] || null; - } - - // 카테고리 목록 조회 - async getCategories() { - const query = ` - SELECT DISTINCT category - FROM temp_material_master - WHERE is_active = true AND category IS NOT NULL - ORDER BY category ASC - `; - - const pool = getPool(); - const result = await pool.query(query); - return result.rows.map((row) => row.category); - } -} - -export default new MaterialService(); diff --git a/backend-node/src/services/YardLayoutService.ts b/backend-node/src/services/YardLayoutService.ts index 6d077915..7a814333 100644 --- a/backend-node/src/services/YardLayoutService.ts +++ b/backend-node/src/services/YardLayoutService.ts @@ -101,7 +101,6 @@ export class YardLayoutService { SELECT id, yard_layout_id, - external_material_id, material_code, material_name, quantity, @@ -113,6 +112,9 @@ export class YardLayoutService { size_y, size_z, color, + data_source_type, + data_source_config, + data_binding, memo, created_at, updated_at @@ -126,12 +128,11 @@ export class YardLayoutService { return result.rows; } - // 야드에 자재 배치 추가 + // 야드에 자재 배치 추가 (빈 요소 또는 설정된 요소) async addMaterialPlacement(layoutId: number, data: any) { const query = ` INSERT INTO yard_material_placement ( yard_layout_id, - external_material_id, material_code, material_name, quantity, @@ -143,52 +144,68 @@ export class YardLayoutService { size_y, size_z, color, + data_source_type, + data_source_config, + data_binding, memo - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING * `; const pool = getPool(); const result = await pool.query(query, [ layoutId, - data.external_material_id, - data.material_code, - data.material_name, - data.quantity, - data.unit, + data.material_code || null, + data.material_name || null, + data.quantity || null, + data.unit || null, data.position_x || 0, data.position_y || 0, data.position_z || 0, data.size_x || 5, data.size_y || 5, data.size_z || 5, - data.color || "#3b82f6", + data.color || "#9ca3af", // 미설정 시 회색 + data.data_source_type || null, + data.data_source_config ? JSON.stringify(data.data_source_config) : null, + data.data_binding ? JSON.stringify(data.data_binding) : null, data.memo || null, ]); return result.rows[0]; } - // 배치 정보 수정 (위치, 크기, 색상, 메모만) + // 배치 정보 수정 (위치, 크기, 색상, 데이터 바인딩 등) async updatePlacement(placementId: number, data: any) { const query = ` UPDATE yard_material_placement SET - position_x = COALESCE($1, position_x), - position_y = COALESCE($2, position_y), - position_z = COALESCE($3, position_z), - size_x = COALESCE($4, size_x), - size_y = COALESCE($5, size_y), - size_z = COALESCE($6, size_z), - color = COALESCE($7, color), - memo = COALESCE($8, memo), + material_code = COALESCE($1, material_code), + material_name = COALESCE($2, material_name), + quantity = COALESCE($3, quantity), + unit = COALESCE($4, unit), + position_x = COALESCE($5, position_x), + position_y = COALESCE($6, position_y), + position_z = COALESCE($7, position_z), + size_x = COALESCE($8, size_x), + size_y = COALESCE($9, size_y), + size_z = COALESCE($10, size_z), + color = COALESCE($11, color), + data_source_type = COALESCE($12, data_source_type), + data_source_config = COALESCE($13, data_source_config), + data_binding = COALESCE($14, data_binding), + memo = COALESCE($15, memo), updated_at = CURRENT_TIMESTAMP - WHERE id = $9 + WHERE id = $16 RETURNING * `; const pool = getPool(); const result = await pool.query(query, [ + data.material_code, + data.material_name, + data.quantity, + data.unit, data.position_x, data.position_y, data.position_z, @@ -196,6 +213,9 @@ export class YardLayoutService { data.size_y, data.size_z, data.color, + data.data_source_type, + data.data_source_config ? JSON.stringify(data.data_source_config) : null, + data.data_binding ? JSON.stringify(data.data_binding) : null, data.memo, placementId, ]); @@ -230,8 +250,9 @@ export class YardLayoutService { size_x = $4, size_y = $5, size_z = $6, + color = $7, updated_at = CURRENT_TIMESTAMP - WHERE id = $7 AND yard_layout_id = $8 + WHERE id = $8 AND yard_layout_id = $9 RETURNING * `; @@ -242,6 +263,7 @@ export class YardLayoutService { placement.size_x, placement.size_y, placement.size_z, + placement.color, placement.id, layoutId, ]); @@ -299,14 +321,14 @@ export class YardLayoutService { await client.query( ` INSERT INTO yard_material_placement ( - yard_layout_id, external_material_id, material_code, material_name, + yard_layout_id, material_code, material_name, quantity, unit, position_x, position_y, position_z, - size_x, size_y, size_z, color, memo - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + size_x, size_y, size_z, color, + data_source_type, data_source_config, data_binding, memo + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) `, [ newLayout.id, - placement.external_material_id, placement.material_code, placement.material_name, placement.quantity, @@ -318,6 +340,9 @@ export class YardLayoutService { placement.size_y, placement.size_z, placement.color, + placement.data_source_type, + placement.data_source_config, + placement.data_binding, placement.memo, ] ); diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/YardEditor.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/YardEditor.tsx index 8dd82e5d..3a620841 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/YardEditor.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/YardEditor.tsx @@ -2,11 +2,12 @@ import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Save, Loader2, X } from "lucide-react"; -import { yardLayoutApi, materialApi } from "@/lib/api/yardLayoutApi"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { ArrowLeft, Save, Loader2, Plus, Settings, Trash2 } 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 } from "lucide-react"; const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), { ssr: false, @@ -17,41 +18,11 @@ const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), { ), }); -interface TempMaterial { - id: number; - material_code: string; - material_name: string; - category: string; - unit: string; - default_color: string; - description: string; -} - -interface YardLayout { - id: number; - name: string; - description: string; - placement_count?: number; - updated_at: string; -} - -interface YardPlacement { - id: number; - yard_layout_id: number; - external_material_id: string; - material_code: string; - material_name: string; - quantity: number; - unit: string; - position_x: number; - position_y: number; - position_z: number; - size_x: number; - size_y: number; - size_z: number; - color: string; - memo?: string; -} +// 나중에 구현할 데이터 바인딩 패널 +const YardElementConfigPanel = dynamic(() => import("./YardElementConfigPanel"), { + ssr: false, + loading: () =>
로딩 중...
, +}); interface YardEditorProps { layout: YardLayout; @@ -60,90 +31,88 @@ interface YardEditorProps { export default function YardEditor({ layout, onBack }: YardEditorProps) { const [placements, setPlacements] = useState([]); - const [materials, setMaterials] = useState([]); const [selectedPlacement, setSelectedPlacement] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - const [editValues, setEditValues] = useState({ - size_x: 5, - size_y: 5, - size_z: 5, - color: "#3b82f6", - }); + const [showConfigPanel, setShowConfigPanel] = useState(false); + const [error, setError] = useState(null); - // 배치 목록 & 자재 목록 로드 + // 배치 목록 로드 useEffect(() => { - const loadData = async () => { + const loadPlacements = async () => { try { setIsLoading(true); - const [placementsRes, materialsRes] = await Promise.all([ - yardLayoutApi.getPlacementsByLayoutId(layout.id), - materialApi.getTempMaterials({ limit: 100 }), - ]); - - if (placementsRes.success) { - setPlacements(placementsRes.data as YardPlacement[]); - } - if (materialsRes.success) { - setMaterials(materialsRes.data as TempMaterial[]); + const response = await yardLayoutApi.getPlacementsByLayoutId(layout.id); + if (response.success) { + setPlacements(response.data as YardPlacement[]); } } catch (error) { - console.error("데이터 로드 실패:", error); + console.error("배치 목록 로드 실패:", error); + setError("배치 목록을 불러올 수 없습니다."); } finally { setIsLoading(false); } }; - loadData(); + loadPlacements(); }, [layout.id]); - // selectedPlacement 변경 시 editValues 업데이트 - useEffect(() => { - if (selectedPlacement) { - setEditValues({ - size_x: selectedPlacement.size_x, - size_y: selectedPlacement.size_y, - size_z: selectedPlacement.size_z, - color: selectedPlacement.color, - }); - } - }, [selectedPlacement]); + // 빈 요소 추가 + const handleAddElement = async () => { + try { + const newPlacementData = { + position_x: 0, + position_y: 0, + position_z: 0, + size_x: 5, + size_y: 5, + size_z: 5, + color: "#9ca3af", // 회색 (미설정 상태) + }; - // 자재 클릭 → 배치 추가 - const handleMaterialClick = async (material: TempMaterial) => { - // 이미 배치되었는지 확인 - const alreadyPlaced = placements.find((p) => p.material_code === material.material_code); - if (alreadyPlaced) { - alert("이미 배치된 자재입니다."); + const response = await yardLayoutApi.addMaterialPlacement(layout.id, newPlacementData); + if (response.success) { + const newPlacement = response.data as YardPlacement; + setPlacements((prev) => [...prev, newPlacement]); + setSelectedPlacement(newPlacement); + setShowConfigPanel(true); // 자동으로 설정 패널 표시 + } + } catch (error) { + console.error("요소 추가 실패:", error); + setError("요소 추가에 실패했습니다."); + } + }; + + // 요소 선택 (3D 캔버스 또는 목록에서) + const handleSelectPlacement = (placement: YardPlacement) => { + setSelectedPlacement(placement); + setShowConfigPanel(false); // 선택 시에는 설정 패널 닫기 + }; + + // 설정 버튼 클릭 + const handleConfigClick = (placement: YardPlacement) => { + setSelectedPlacement(placement); + setShowConfigPanel(true); + }; + + // 요소 삭제 + const handleDeletePlacement = async (placementId: number) => { + if (!confirm("이 요소를 삭제하시겠습니까?")) { return; } - // 기본 위치에 배치 - const placementData = { - external_material_id: `TEMP-${material.id}`, - material_code: material.material_code, - material_name: material.material_name, - quantity: 1, - unit: material.unit, - position_x: 0, - position_y: 0, - position_z: 0, - size_x: 5, - size_y: 5, - size_z: 5, - color: material.default_color, - }; - try { - const response = await yardLayoutApi.addMaterialPlacement(layout.id, placementData); + const response = await yardLayoutApi.removePlacement(placementId); if (response.success) { - setPlacements((prev) => [...prev, response.data as YardPlacement]); - setSelectedPlacement(response.data as YardPlacement); + setPlacements((prev) => prev.filter((p) => p.id !== placementId)); + if (selectedPlacement?.id === placementId) { + setSelectedPlacement(null); + setShowConfigPanel(false); + } } } catch (error) { - console.error("자재 배치 실패:", error); - alert("자재 배치에 실패했습니다."); + console.error("요소 삭제 실패:", error); + setError("요소 삭제에 실패했습니다."); } }; @@ -155,49 +124,13 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) { position_z: Math.round(position.z * 2) / 2, }; - setPlacements((prev) => - prev.map((p) => - p.id === id - ? { - ...p, - ...updatedPosition, - } - : p, - ), - ); + setPlacements((prev) => prev.map((p) => (p.id === id ? { ...p, ...updatedPosition } : p))); - // 선택된 자재도 업데이트 if (selectedPlacement?.id === id) { - setSelectedPlacement((prev) => - prev - ? { - ...prev, - ...updatedPosition, - } - : null, - ); + setSelectedPlacement((prev) => (prev ? { ...prev, ...updatedPosition } : null)); } }; - // 자재 배치 해제 - const handlePlacementRemove = async (id: number): Promise => { - try { - const response = await yardLayoutApi.removePlacement(id); - if (response.success) { - setPlacements((prev) => prev.filter((p) => p.id !== id)); - setSelectedPlacement(null); - } - } catch (error) { - console.error("배치 해제 실패:", error); - alert("배치 해제에 실패했습니다."); - } - }; - - // 위치/크기/색상 업데이트 - const handlePlacementUpdate = (id: number, updates: Partial) => { - setPlacements((prev) => prev.map((p) => (p.id === id ? { ...p, ...updates } : p))); - }; - // 저장 const handleSave = async () => { setIsSaving(true); @@ -227,12 +160,28 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) { } }; - // 필터링된 자재 목록 - const filteredMaterials = materials.filter( - (m) => - m.material_name.toLowerCase().includes(searchTerm.toLowerCase()) || - m.material_code.toLowerCase().includes(searchTerm.toLowerCase()), - ); + // 설정 패널에서 저장 + const handleSaveConfig = async (updatedData: Partial) => { + if (!selectedPlacement) return; + + try { + const response = await yardLayoutApi.updatePlacement(selectedPlacement.id, updatedData); + if (response.success) { + const updated = response.data as YardPlacement; + setPlacements((prev) => prev.map((p) => (p.id === updated.id ? updated : p))); + setSelectedPlacement(updated); + setShowConfigPanel(false); + } + } catch (error) { + console.error("설정 저장 실패:", error); + setError("설정 저장에 실패했습니다."); + } + }; + + // 요소가 설정되었는지 확인 + const isConfigured = (placement: YardPlacement): boolean => { + return !!(placement.material_name && placement.quantity && placement.unit); + }; return (
@@ -264,6 +213,14 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
+ {/* 에러 메시지 */} + {error && ( + + + {error} + + )} + {/* 메인 컨텐츠 영역 */}
{/* 좌측: 3D 캔버스 */} @@ -276,152 +233,104 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) { setSelectedPlacement(placement as YardPlacement)} + onPlacementClick={(placement) => handleSelectPlacement(placement as YardPlacement)} onPlacementDrag={handlePlacementDrag} /> )}
- {/* 우측: 자재 목록 또는 편집 패널 */} + {/* 우측: 요소 목록 또는 설정 패널 */}
- {selectedPlacement ? ( - // 선택된 자재 편집 패널 -
-
-

자재 정보

- -
- -
-
- {/* 읽기 전용 정보 */} -
- -
{selectedPlacement.material_code}
-
- -
- -
{selectedPlacement.material_name}
-
- -
- -
- {selectedPlacement.quantity} {selectedPlacement.unit} -
-
- - {/* 편집 가능 정보 */} -
- - -
-
- - { - const newValue = parseFloat(e.target.value) || 1; - setEditValues((prev) => ({ ...prev, size_x: newValue })); - handlePlacementUpdate(selectedPlacement.id, { size_x: newValue }); - }} - /> -
-
- - { - const newValue = parseFloat(e.target.value) || 1; - setEditValues((prev) => ({ ...prev, size_y: newValue })); - handlePlacementUpdate(selectedPlacement.id, { size_y: newValue }); - }} - /> -
-
- - { - const newValue = parseFloat(e.target.value) || 1; - setEditValues((prev) => ({ ...prev, size_z: newValue })); - handlePlacementUpdate(selectedPlacement.id, { size_z: newValue }); - }} - /> -
-
- -
- - { - setEditValues((prev) => ({ ...prev, color: e.target.value })); - handlePlacementUpdate(selectedPlacement.id, { color: e.target.value }); - }} - /> -
-
- - -
-
-
+ {showConfigPanel && selectedPlacement ? ( + // 설정 패널 + setShowConfigPanel(false)} + /> ) : ( - // 자재 목록 + // 요소 목록
-

자재 목록

- setSearchTerm(e.target.value)} - className="text-sm" - /> +
+

요소 목록

+ +
+

총 {placements.length}개

-
- {filteredMaterials.length === 0 ? ( +
+ {placements.length === 0 ? (
- 검색 결과가 없습니다 + 요소가 없습니다. +
+ 위의 "요소 추가" 버튼을 클릭하세요.
) : ( -
- {filteredMaterials.map((material) => { - const isPlaced = placements.some((p) => p.material_code === material.material_code); +
+ {placements.map((placement) => { + const configured = isConfigured(placement); + const isSelected = selectedPlacement?.id === placement.id; + return ( - +
+
+ {configured ? ( + <> +
{placement.material_name}
+
+ 수량: {placement.quantity} {placement.unit} +
+ + ) : ( + <> +
요소 #{placement.id}
+
데이터 바인딩 설정 필요
+ + )} +
+
+ +
+ + +
+
); })}
diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/types.ts b/frontend/components/admin/dashboard/widgets/yard-3d/types.ts new file mode 100644 index 00000000..3438ae37 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/yard-3d/types.ts @@ -0,0 +1,86 @@ +// 야드 관리 3D - 타입 정의 + +import { ChartDataSource } from "../../types"; + +// 야드 레이아웃 +export interface YardLayout { + id: number; + name: string; + description?: string; + placement_count?: number; + created_by?: string; + created_at: string; + updated_at: string; +} + +// 데이터 소스 설정 +export interface YardDataSourceConfig { + type: "database" | "external_db" | "rest_api"; + + // type === 'database' (현재 DB) + query?: string; + + // type === 'external_db' (외부 DB) + connectionId?: number; + + // type === 'rest_api' + url?: string; + method?: "GET" | "POST"; + headers?: Record; + queryParams?: Record; + body?: string; + dataPath?: string; // 응답에서 데이터 배열 경로 +} + +// 데이터 바인딩 설정 +export interface YardDataBinding { + // 데이터 소스의 특정 행 선택 + selectedRowIndex?: number; + + // 필드 매핑 (데이터 소스에서 선택) + materialNameField?: string; // 자재명이 들어있는 컬럼명 + quantityField?: string; // 수량이 들어있는 컬럼명 + + // 단위는 사용자가 직접 입력 + unit: string; // 예: "EA", "BOX", "KG", "M" 등 +} + +// 자재 배치 정보 +export interface YardPlacement { + id: number; + yard_layout_id: number; + material_code?: string | null; + material_name?: string | null; + quantity?: number | null; + unit?: string | null; + position_x: number; + position_y: number; + position_z: number; + size_x: number; + size_y: number; + size_z: number; + color: string; + data_source_type?: string | null; + data_source_config?: YardDataSourceConfig | null; + data_binding?: YardDataBinding | null; + memo?: string | null; + created_at?: string; + updated_at?: string; +} + +// 쿼리 결과 (데이터 바인딩용) +export interface QueryResult { + columns: string[]; + rows: Record[]; + totalRows: number; +} + +// 요소 설정 단계 +export type ConfigStep = "data-source" | "field-mapping" | "placement-settings"; + +// 설정 모달 props +export interface YardElementConfigPanelProps { + placement: YardPlacement; + onSave: (updatedPlacement: Partial) => Promise; + onCancel: () => void; +} diff --git a/frontend/lib/api/yardLayoutApi.ts b/frontend/lib/api/yardLayoutApi.ts index 2dbd9f4c..fec77bfa 100644 --- a/frontend/lib/api/yardLayoutApi.ts +++ b/frontend/lib/api/yardLayoutApi.ts @@ -57,28 +57,3 @@ export const yardLayoutApi = { return apiCall("PUT", `/yard-layouts/${layoutId}/placements/batch`, { placements }); }, }; - -// 자재 관리 API -export const materialApi = { - // 임시 자재 마스터 목록 조회 - async getTempMaterials(params?: { search?: string; category?: string; page?: number; limit?: number }) { - const queryParams = new URLSearchParams(); - if (params?.search) queryParams.append("search", params.search); - if (params?.category) queryParams.append("category", params.category); - if (params?.page) queryParams.append("page", params.page.toString()); - if (params?.limit) queryParams.append("limit", params.limit.toString()); - - const queryString = queryParams.toString(); - return apiCall("GET", `/materials/temp${queryString ? `?${queryString}` : ""}`); - }, - - // 특정 자재 상세 조회 - async getTempMaterialByCode(code: string) { - return apiCall("GET", `/materials/temp/${code}`); - }, - - // 카테고리 목록 조회 - async getCategories() { - return apiCall("GET", "/materials/temp/categories"); - }, -};