From d57756189f6ee7952e6c294628c1edc68b94ee32 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 15:53:17 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=20=ED=9A=8C=EC=82=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/DashboardController.ts | 27 ++++++- backend-node/src/services/DashboardService.ts | 74 ++++++++++++++----- 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/backend-node/src/controllers/DashboardController.ts b/backend-node/src/controllers/DashboardController.ts index 7d710110..601e035c 100644 --- a/backend-node/src/controllers/DashboardController.ts +++ b/backend-node/src/controllers/DashboardController.ts @@ -24,6 +24,8 @@ export class DashboardController { ): Promise { try { const userId = req.user?.userId; + const companyCode = req.user?.companyCode; + if (!userId) { res.status(401).json({ success: false, @@ -89,7 +91,8 @@ export class DashboardController { const savedDashboard = await DashboardService.createDashboard( dashboardData, - userId + userId, + companyCode ); // console.log('대시보드 생성 성공:', { id: savedDashboard.id, title: savedDashboard.title }); @@ -121,6 +124,7 @@ export class DashboardController { async getDashboards(req: AuthenticatedRequest, res: Response): Promise { try { const userId = req.user?.userId; + const companyCode = req.user?.companyCode; const query: DashboardListQuery = { page: parseInt(req.query.page as string) || 1, @@ -145,7 +149,11 @@ export class DashboardController { return; } - const result = await DashboardService.getDashboards(query, userId); + const result = await DashboardService.getDashboards( + query, + userId, + companyCode + ); res.json({ success: true, @@ -173,6 +181,7 @@ export class DashboardController { try { const { id } = req.params; const userId = req.user?.userId; + const companyCode = req.user?.companyCode; if (!id) { res.status(400).json({ @@ -182,7 +191,11 @@ export class DashboardController { return; } - const dashboard = await DashboardService.getDashboardById(id, userId); + const dashboard = await DashboardService.getDashboardById( + id, + userId, + companyCode + ); if (!dashboard) { res.status(404).json({ @@ -393,6 +406,8 @@ export class DashboardController { return; } + const companyCode = req.user?.companyCode; + const query: DashboardListQuery = { page: parseInt(req.query.page as string) || 1, limit: Math.min(parseInt(req.query.limit as string) || 20, 100), @@ -401,7 +416,11 @@ export class DashboardController { createdBy: userId, // 본인이 만든 대시보드만 }; - const result = await DashboardService.getDashboards(query, userId); + const result = await DashboardService.getDashboards( + query, + userId, + companyCode + ); res.json({ success: true, diff --git a/backend-node/src/services/DashboardService.ts b/backend-node/src/services/DashboardService.ts index c7650df2..68cc582f 100644 --- a/backend-node/src/services/DashboardService.ts +++ b/backend-node/src/services/DashboardService.ts @@ -18,7 +18,8 @@ export class DashboardService { */ static async createDashboard( data: CreateDashboardRequest, - userId: string + userId: string, + companyCode?: string ): Promise { const dashboardId = uuidv4(); const now = new Date(); @@ -31,8 +32,8 @@ export class DashboardService { ` INSERT INTO dashboards ( id, title, description, is_public, created_by, - created_at, updated_at, tags, category, view_count, settings - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + created_at, updated_at, tags, category, view_count, settings, company_code + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) `, [ dashboardId, @@ -46,6 +47,7 @@ export class DashboardService { data.category || null, 0, JSON.stringify(data.settings || {}), + companyCode || "DEFAULT", ] ); @@ -143,7 +145,11 @@ export class DashboardService { /** * 대시보드 목록 조회 */ - static async getDashboards(query: DashboardListQuery, userId?: string) { + static async getDashboards( + query: DashboardListQuery, + userId?: string, + companyCode?: string + ) { const { page = 1, limit = 20, @@ -161,6 +167,13 @@ export class DashboardService { let params: any[] = []; let paramIndex = 1; + // 회사 코드 필터링 (최우선) + if (companyCode) { + whereConditions.push(`d.company_code = $${paramIndex}`); + params.push(companyCode); + paramIndex++; + } + // 권한 필터링 if (userId) { whereConditions.push( @@ -278,7 +291,8 @@ export class DashboardService { */ static async getDashboardById( dashboardId: string, - userId?: string + userId?: string, + companyCode?: string ): Promise { try { // 1. 대시보드 기본 정보 조회 (권한 체크 포함) @@ -286,21 +300,43 @@ export class DashboardService { let dashboardParams: any[]; if (userId) { - dashboardQuery = ` - SELECT d.* - FROM dashboards d - WHERE d.id = $1 AND d.deleted_at IS NULL - AND (d.created_by = $2 OR d.is_public = true) - `; - dashboardParams = [dashboardId, userId]; + if (companyCode) { + dashboardQuery = ` + SELECT d.* + FROM dashboards d + WHERE d.id = $1 AND d.deleted_at IS NULL + AND d.company_code = $2 + AND (d.created_by = $3 OR d.is_public = true) + `; + dashboardParams = [dashboardId, companyCode, userId]; + } else { + dashboardQuery = ` + SELECT d.* + FROM dashboards d + WHERE d.id = $1 AND d.deleted_at IS NULL + AND (d.created_by = $2 OR d.is_public = true) + `; + dashboardParams = [dashboardId, userId]; + } } else { - dashboardQuery = ` - SELECT d.* - FROM dashboards d - WHERE d.id = $1 AND d.deleted_at IS NULL - AND d.is_public = true - `; - dashboardParams = [dashboardId]; + if (companyCode) { + dashboardQuery = ` + SELECT d.* + FROM dashboards d + WHERE d.id = $1 AND d.deleted_at IS NULL + AND d.company_code = $2 + AND d.is_public = true + `; + dashboardParams = [dashboardId, companyCode]; + } else { + dashboardQuery = ` + SELECT d.* + FROM dashboards d + WHERE d.id = $1 AND d.deleted_at IS NULL + AND d.is_public = true + `; + dashboardParams = [dashboardId]; + } } const dashboardResult = await PostgreSQLService.query( -- 2.43.0 From 5ca0a6b6dc6877fc22007344b8aa31f63a7ff5b2 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 16:23:34 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/dashboard/page.tsx | 292 +++++++++---------- 1 file changed, 146 insertions(+), 146 deletions(-) diff --git a/frontend/app/(main)/admin/dashboard/page.tsx b/frontend/app/(main)/admin/dashboard/page.tsx index d1ca6125..97c1036c 100644 --- a/frontend/app/(main)/admin/dashboard/page.tsx +++ b/frontend/app/(main)/admin/dashboard/page.tsx @@ -1,20 +1,14 @@ "use client"; -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { dashboardApi } from "@/lib/api/dashboard"; import { Dashboard } from "@/lib/api/dashboard"; import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { AlertDialog, AlertDialogAction, @@ -25,8 +19,8 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Plus, Search, MoreVertical, Edit, Trash2, Copy, CheckCircle2 } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import { Plus, Search, Edit, Trash2, Copy, LayoutDashboard, MoreVertical } from "lucide-react"; /** * 대시보드 관리 페이지 @@ -35,27 +29,28 @@ import { Plus, Search, MoreVertical, Edit, Trash2, Copy, CheckCircle2 } from "lu */ export default function DashboardListPage() { const router = useRouter(); + const { toast } = useToast(); const [dashboards, setDashboards] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); - const [error, setError] = useState(null); // 모달 상태 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null); - const [successDialogOpen, setSuccessDialogOpen] = useState(false); - const [successMessage, setSuccessMessage] = useState(""); // 대시보드 목록 로드 const loadDashboards = async () => { try { setLoading(true); - setError(null); const result = await dashboardApi.getMyDashboards({ search: searchTerm }); setDashboards(result.dashboards); } catch (err) { console.error("Failed to load dashboards:", err); - setError("대시보드 목록을 불러오는데 실패했습니다."); + toast({ + title: "오류", + description: "대시보드 목록을 불러오는데 실패했습니다.", + variant: "destructive", + }); } finally { setLoading(false); } @@ -63,6 +58,7 @@ export default function DashboardListPage() { useEffect(() => { loadDashboards(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchTerm]); // 대시보드 삭제 확인 모달 열기 @@ -79,37 +75,48 @@ export default function DashboardListPage() { await dashboardApi.deleteDashboard(deleteTarget.id); setDeleteDialogOpen(false); setDeleteTarget(null); - setSuccessMessage("대시보드가 삭제되었습니다."); - setSuccessDialogOpen(true); + toast({ + title: "성공", + description: "대시보드가 삭제되었습니다.", + }); loadDashboards(); } catch (err) { console.error("Failed to delete dashboard:", err); setDeleteDialogOpen(false); - setError("대시보드 삭제에 실패했습니다."); + toast({ + title: "오류", + description: "대시보드 삭제에 실패했습니다.", + variant: "destructive", + }); } }; // 대시보드 복사 const handleCopy = async (dashboard: Dashboard) => { try { - // 전체 대시보드 정보(요소 포함)를 가져오기 const fullDashboard = await dashboardApi.getDashboard(dashboard.id); - const newDashboard = await dashboardApi.createDashboard({ + await dashboardApi.createDashboard({ title: `${fullDashboard.title} (복사본)`, description: fullDashboard.description, elements: fullDashboard.elements || [], isPublic: false, tags: fullDashboard.tags, category: fullDashboard.category, - settings: (fullDashboard as any).settings, // 해상도와 배경색 설정도 복사 + settings: fullDashboard.settings as { resolution?: string; backgroundColor?: string }, + }); + toast({ + title: "성공", + description: "대시보드가 복사되었습니다.", }); - setSuccessMessage("대시보드가 복사되었습니다."); - setSuccessDialogOpen(true); loadDashboards(); } catch (err) { console.error("Failed to copy dashboard:", err); - setError("대시보드 복사에 실패했습니다."); + toast({ + title: "오류", + description: "대시보드 복사에 실패했습니다.", + variant: "destructive", + }); } }; @@ -119,120 +126,125 @@ export default function DashboardListPage() { year: "numeric", month: "2-digit", day: "2-digit", - hour: "2-digit", - minute: "2-digit", }); }; - if (loading) { - return ( -
-
-
로딩 중...
-
대시보드 목록을 불러오고 있습니다
-
-
- ); - } - return ( -
-
- {/* 헤더 */} -
-

대시보드 관리

-

대시보드를 생성하고 관리할 수 있습니다

-
- - {/* 액션 바 */} -
-
- - setSearchTerm(e.target.value)} - className="pl-9" - /> +
+
+ {/* 페이지 제목 */} +
+
+

대시보드 관리

+

대시보드를 생성하고 관리할 수 있습니다

-
- {/* 에러 메시지 */} - {error && ( - -

{error}

-
- )} + {/* 검색 및 필터 */} + + +
+
+ + setSearchTerm(e.target.value)} + className="w-64 pl-10" + /> +
+ +
+
+
{/* 대시보드 목록 */} - {dashboards.length === 0 ? ( - -
- -
-

대시보드가 없습니다

-

첫 번째 대시보드를 생성하여 데이터 시각화를 시작하세요

- + {loading ? ( +
+
로딩 중...
+
+ ) : dashboards.length === 0 ? ( + + +
+ +

등록된 대시보드가 없습니다

+

첫 번째 대시보드를 생성하여 데이터 시각화를 시작하세요.

+ +
+
) : ( - - - - - 제목 - 설명 - 생성일 - 수정일 - 작업 - - - - {dashboards.map((dashboard) => ( - - {dashboard.title} - - {dashboard.description || "-"} - - {formatDate(dashboard.createdAt)} - {formatDate(dashboard.updatedAt)} - - - - - - - router.push(`/admin/dashboard/edit/${dashboard.id}`)} - className="gap-2" - > - - 편집 - - handleCopy(dashboard)} className="gap-2"> - - 복사 - - handleDeleteClick(dashboard.id, dashboard.title)} - className="gap-2 text-red-600 focus:text-red-600" - > - - 삭제 - - - - + + +
+ + + 제목 + 설명 + 생성일 + 작업 - ))} - -
+ + + {dashboards.map((dashboard) => ( + + +
{dashboard.title}
+
+ + {dashboard.description || "-"} + + {formatDate(dashboard.createdAt)} + + + + + + +
+ + + +
+
+
+
+
+ ))} +
+ +
)}
@@ -241,36 +253,24 @@ export default function DashboardListPage() { - 대시보드 삭제 + 대시보드 삭제 확인 "{deleteTarget?.title}" 대시보드를 삭제하시겠습니까? -
이 작업은 되돌릴 수 없습니다. +
+ 이 작업은 되돌릴 수 없습니다.
취소 - + 삭제
- - {/* 성공 모달 */} - - - -
- -
- 완료 - {successMessage} -
-
- -
-
-
); } -- 2.43.0 From eac43cfb31c312e798233ffc32942be298385cfe Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 16:28:03 +0900 Subject: [PATCH 03/10] =?UTF-8?q?more=20=EB=B2=84=ED=8A=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/dashboard/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/(main)/admin/dashboard/page.tsx b/frontend/app/(main)/admin/dashboard/page.tsx index 97c1036c..a20c58f9 100644 --- a/frontend/app/(main)/admin/dashboard/page.tsx +++ b/frontend/app/(main)/admin/dashboard/page.tsx @@ -20,7 +20,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { useToast } from "@/hooks/use-toast"; -import { Plus, Search, Edit, Trash2, Copy, LayoutDashboard, MoreVertical } from "lucide-react"; +import { Plus, Search, Edit, Trash2, Copy, LayoutDashboard, MoreHorizontal } from "lucide-react"; /** * 대시보드 관리 페이지 @@ -204,7 +204,7 @@ export default function DashboardListPage() { -- 2.43.0 From ec853fb45d5239c5c7352d014d3046c3b08283d0 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 16:33:34 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/dashboard/page.tsx | 47 +++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/frontend/app/(main)/admin/dashboard/page.tsx b/frontend/app/(main)/admin/dashboard/page.tsx index a20c58f9..94892c0d 100644 --- a/frontend/app/(main)/admin/dashboard/page.tsx +++ b/frontend/app/(main)/admin/dashboard/page.tsx @@ -20,6 +20,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { useToast } from "@/hooks/use-toast"; +import { Pagination, PaginationInfo } from "@/components/common/Pagination"; import { Plus, Search, Edit, Trash2, Copy, LayoutDashboard, MoreHorizontal } from "lucide-react"; /** @@ -34,6 +35,11 @@ export default function DashboardListPage() { const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); + // 페이지네이션 상태 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [totalCount, setTotalCount] = useState(0); + // 모달 상태 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null); @@ -42,8 +48,13 @@ export default function DashboardListPage() { const loadDashboards = async () => { try { setLoading(true); - const result = await dashboardApi.getMyDashboards({ search: searchTerm }); + const result = await dashboardApi.getMyDashboards({ + search: searchTerm, + page: currentPage, + limit: pageSize, + }); setDashboards(result.dashboards); + setTotalCount(result.pagination.total); } catch (err) { console.error("Failed to load dashboards:", err); toast({ @@ -59,7 +70,28 @@ export default function DashboardListPage() { useEffect(() => { loadDashboards(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchTerm]); + }, [searchTerm, currentPage, pageSize]); + + // 페이지네이션 정보 계산 + const paginationInfo: PaginationInfo = { + currentPage, + totalPages: Math.ceil(totalCount / pageSize), + totalItems: totalCount, + itemsPerPage: pageSize, + startItem: totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1, + endItem: Math.min(currentPage * pageSize, totalCount), + }; + + // 페이지 변경 핸들러 + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // 페이지 크기 변경 핸들러 + const handlePageSizeChange = (size: number) => { + setPageSize(size); + setCurrentPage(1); // 페이지 크기 변경 시 첫 페이지로 + }; // 대시보드 삭제 확인 모달 열기 const handleDeleteClick = (id: string, title: string) => { @@ -247,6 +279,17 @@ export default function DashboardListPage() { )} + + {/* 페이지네이션 */} + {!loading && dashboards.length > 0 && ( + + )}
{/* 삭제 확인 모달 */} -- 2.43.0 From 55601481d74175f64cc3ecfc7ae25e890c94dc85 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 16:35:45 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=B5=9C=EC=86=8C=EB=86=92=EC=9D=B4=20?= =?UTF-8?q?=EC=9E=AC=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/dashboard/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/(main)/admin/dashboard/page.tsx b/frontend/app/(main)/admin/dashboard/page.tsx index 94892c0d..55f1a291 100644 --- a/frontend/app/(main)/admin/dashboard/page.tsx +++ b/frontend/app/(main)/admin/dashboard/page.tsx @@ -162,7 +162,7 @@ export default function DashboardListPage() { }; return ( -
+
{/* 페이지 제목 */}
-- 2.43.0 From 71111ce0727b91e89a151ebd74f656baea494ff8 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 16:45:04 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=95=BC=EB=93=9C=20=EC=9D=B4=EB=A6=84=20z-index=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/yard-3d/Yard3DCanvas.tsx | 3 ++- .../widgets/yard-3d/Yard3DViewer.tsx | 21 +++++++++++++------ frontend/components/layout/MainHeader.tsx | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx index d55e8ad3..29c15ca9 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx @@ -7,6 +7,7 @@ import * as THREE from "three"; interface YardPlacement { id: number; + yard_layout_id?: number; material_code?: string | null; material_name?: string | null; quantity?: number | null; @@ -26,7 +27,7 @@ interface YardPlacement { interface Yard3DCanvasProps { placements: YardPlacement[]; selectedPlacementId: number | null; - onPlacementClick: (placement: YardPlacement) => void; + onPlacementClick: (placement: YardPlacement | null) => void; onPlacementDrag?: (id: number, position: { x: number; y: number; z: number }) => void; } diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx index ead548f1..a4dab504 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx @@ -7,7 +7,7 @@ import { Loader2 } from "lucide-react"; interface YardPlacement { id: number; - yard_layout_id: number; + yard_layout_id?: number; material_code?: string | null; material_name?: string | null; quantity?: number | null; @@ -20,12 +20,20 @@ interface YardPlacement { size_z: number; color: string; data_source_type?: string | null; - data_source_config?: any; - data_binding?: any; + data_source_config?: Record | null; + data_binding?: Record | null; status?: string; memo?: string; } +interface YardLayout { + id: number; + name: string; + description?: string; + created_at?: string; + updated_at?: string; +} + interface Yard3DViewerProps { layoutId: number; } @@ -58,13 +66,14 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) { // 야드 레이아웃 정보 조회 const layoutResponse = await yardLayoutApi.getLayoutById(layoutId); if (layoutResponse.success) { - setLayoutName(layoutResponse.data.name); + const layout = layoutResponse.data as YardLayout; + setLayoutName(layout.name); } // 배치 데이터 조회 const placementsResponse = await yardLayoutApi.getPlacementsByLayoutId(layoutId); if (placementsResponse.success) { - setPlacements(placementsResponse.data); + setPlacements(placementsResponse.data as YardPlacement[]); } else { setError("배치 데이터를 불러올 수 없습니다."); } @@ -123,7 +132,7 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) { {/* 야드 이름 (좌측 상단) */} {layoutName && ( -
+

{layoutName}

)} diff --git a/frontend/components/layout/MainHeader.tsx b/frontend/components/layout/MainHeader.tsx index cfad594e..f04dcca3 100644 --- a/frontend/components/layout/MainHeader.tsx +++ b/frontend/components/layout/MainHeader.tsx @@ -14,7 +14,7 @@ interface MainHeaderProps { */ export function MainHeader({ user, onSidebarToggle, onProfileClick, onLogout }: MainHeaderProps) { return ( -
+
{/* Left side - Side Menu + Logo */}
-- 2.43.0 From 8a2aa49910ab22bfac490b2e75b26ea42a70f253 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 16:57:19 +0900 Subject: [PATCH 07/10] =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=EC=97=90=20=ED=97=A4=EB=8D=94=EB=A7=8C?= =?UTF-8?q?=ED=81=BC=20=ED=8C=A8=EB=94=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/layout/AppLayout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index b6e0139b..81aab11b 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -401,7 +401,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { onLogout={handleLogout} /> -
+
{/* 모바일 사이드바 오버레이 */} {sidebarOpen && isMobile && (
setSidebarOpen(false)} /> @@ -413,7 +413,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { isMobile ? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-14 left-0 z-40" : "relative top-0 z-auto translate-x-0" - } flex h-full w-72 max-w-72 min-w-72 flex-col border-r border-slate-200 bg-white transition-transform duration-300`} + } flex h-[calc(100vh-3.5rem)] w-72 max-w-72 min-w-72 flex-col border-r border-slate-200 bg-white transition-transform duration-300`} > {/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */} {(user as ExtendedUserInfo)?.userType === "admin" && ( -- 2.43.0 From d3c9a425252503e530d2c982d3480c4985c21212 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 17:14:04 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 22 ++-- .../admin/dashboard/ElementConfigModal.tsx | 116 +++++++++--------- .../admin/dashboard/widgets/ClockSettings.tsx | 7 +- 3 files changed, 73 insertions(+), 72 deletions(-) diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 7b5453f9..942a2783 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -105,6 +105,8 @@ import { CalendarWidget } from "./widgets/CalendarWidget"; // 기사 관리 위젯 임포트 import { DriverManagementWidget } from "./widgets/DriverManagementWidget"; import { ListWidget } from "./widgets/ListWidget"; +import { MoreHorizontal, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; // 야드 관리 3D 위젯 const YardManagement3DWidget = dynamic(() => import("./widgets/YardManagement3DWidget"), { @@ -546,22 +548,26 @@ export function CanvasElement({
{/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */} {onConfigure && !(element.type === "widget" && element.subtype === "driver-management") && ( - + + )} {/* 삭제 버튼 */} - + +
diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx index 6aba88db..b168fb2c 100644 --- a/frontend/components/admin/dashboard/ElementConfigModal.tsx +++ b/frontend/components/admin/dashboard/ElementConfigModal.tsx @@ -53,9 +53,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element element.subtype === "driver-management" || element.subtype === "work-history" || // 작업 이력 위젯 (쿼리 필요) element.subtype === "transport-stats"; // 커스텀 통계 카드 위젯 (쿼리 필요) - + // 자체 기능 위젯 (DB 연결 불필요, 헤더 설정만 가능) - const isSelfContainedWidget = + const isSelfContainedWidget = element.subtype === "weather" || // 날씨 위젯 (외부 API) element.subtype === "exchange" || // 환율 위젯 (외부 API) element.subtype === "calculator"; // 계산기 위젯 (자체 기능) @@ -150,11 +150,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element if (!isOpen) return null; // 시계, 달력, 날씨, 환율, 계산기 위젯은 헤더 설정만 가능 - const isHeaderOnlyWidget = - element.type === "widget" && - (element.subtype === "clock" || - element.subtype === "calendar" || - isSelfContainedWidget); + const isHeaderOnlyWidget = + element.type === "widget" && + (element.subtype === "clock" || element.subtype === "calendar" || isSelfContainedWidget); // 기사관리 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음 if (element.type === "widget" && element.subtype === "driver-management") { @@ -172,7 +170,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element // customTitle이 변경되었는지 확인 const isTitleChanged = customTitle.trim() !== (element.customTitle || ""); - + // showHeader가 변경되었는지 확인 const isHeaderChanged = showHeader !== (element.showHeader !== false); @@ -214,13 +212,6 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element

{element.title} 설정

-

- {isSimpleWidget - ? "데이터 소스를 설정하세요" - : currentStep === 1 - ? "데이터 소스를 선택하세요" - : "쿼리를 실행하고 차트를 설정하세요"} -

{/* 헤더 표시 옵션 */} @@ -251,7 +244,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element id="showHeader" checked={showHeader} onChange={(e) => setShowHeader(e.target.checked)} - className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" + className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300" />
)}
)} @@ -376,4 +373,3 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
); } - diff --git a/frontend/components/admin/dashboard/widgets/ClockSettings.tsx b/frontend/components/admin/dashboard/widgets/ClockSettings.tsx index dd28c3af..43a452fe 100644 --- a/frontend/components/admin/dashboard/widgets/ClockSettings.tsx +++ b/frontend/components/admin/dashboard/widgets/ClockSettings.tsx @@ -44,9 +44,9 @@ export function ClockSettings({ config, onSave, onClose }: ClockSettingsProps) {
{[ - { value: "digital", label: "디지털", icon: "🔢" }, - { value: "analog", label: "아날로그", icon: "🕐" }, - { value: "both", label: "둘 다", icon: "⏰" }, + { value: "digital", label: "디지털" }, + { value: "analog", label: "아날로그" }, + { value: "both", label: "둘 다" }, ].map((style) => ( ))} -- 2.43.0 From 2305b8dfaeae808c42bc8c301e78efffe1a489db Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 17:16:03 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=EC=9A=94=EC=86=8C=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=20=EB=B0=B0=EA=B2=BD=EC=9D=84=20=ED=88=AC=EB=AA=85=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/admin/dashboard/CanvasElement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 942a2783..a4b0fc6f 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -543,7 +543,7 @@ export function CanvasElement({ onMouseDown={handleMouseDown} > {/* 헤더 */} -
+
{element.customTitle || element.title}
{/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */} -- 2.43.0 From 8c18555305ca40d500ba40ed1a527a015114c544 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 17:18:28 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/admin/dashboard/charts/ChartRenderer.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/components/admin/dashboard/charts/ChartRenderer.tsx b/frontend/components/admin/dashboard/charts/ChartRenderer.tsx index 092c2b0a..9a5a51a6 100644 --- a/frontend/components/admin/dashboard/charts/ChartRenderer.tsx +++ b/frontend/components/admin/dashboard/charts/ChartRenderer.tsx @@ -51,7 +51,7 @@ export function ChartRenderer({ element, data, width = 250, height = 200 }: Char if (element.dataSource.queryParams) { Object.entries(element.dataSource.queryParams).forEach(([key, value]) => { if (key && value) { - params.append(key, value); + params.append(key, String(value)); } }); } @@ -158,11 +158,15 @@ export function ChartRenderer({ element, data, width = 250, height = 200 }: Char const interval = setInterval(fetchData, refreshInterval); return () => clearInterval(interval); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ element.dataSource?.query, element.dataSource?.connectionType, element.dataSource?.externalConnectionId, element.dataSource?.refreshInterval, + element.dataSource?.type, + element.dataSource?.endpoint, + element.dataSource?.jsonPath, element.chartConfig, data, ]); @@ -201,9 +205,7 @@ export function ChartRenderer({ element, data, width = 250, height = 200 }: Char return (
-
📊
데이터를 설정해주세요
-
⚙️ 버튼을 클릭하여 설정
); -- 2.43.0