From 31d25268ce2a564a0c76f6e60364faf02f8379f7 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 1 Sep 2025 17:57:52 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/screenManagementController.ts | 23 ++ .../src/routes/screenManagementRoutes.ts | 4 + .../src/services/screenManagementService.ts | 39 +++ docs/화면관리_시스템_설계.md | 30 +- .../components/screen/CreateScreenModal.tsx | 158 ++++++++++ frontend/components/screen/ScreenDesigner.tsx | 271 +++++++++++++++--- frontend/components/screen/ScreenList.tsx | 94 +++--- frontend/lib/api/screen.ts | 29 +- 8 files changed, 544 insertions(+), 104 deletions(-) create mode 100644 frontend/components/screen/CreateScreenModal.tsx diff --git a/backend-node/src/controllers/screenManagementController.ts b/backend-node/src/controllers/screenManagementController.ts index 4aa13bdb..c44c2833 100644 --- a/backend-node/src/controllers/screenManagementController.ts +++ b/backend-node/src/controllers/screenManagementController.ts @@ -150,3 +150,26 @@ export const getLayout = async (req: AuthenticatedRequest, res: Response) => { .json({ success: false, message: "레이아웃 조회에 실패했습니다." }); } }; + +// 화면 코드 자동 생성 +export const generateScreenCode = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { companyCode: paramCompanyCode } = req.params; + const { companyCode: userCompanyCode } = req.user as any; + + // 사용자의 회사 코드 또는 파라미터의 회사 코드 사용 + const targetCompanyCode = paramCompanyCode || userCompanyCode; + + const generatedCode = + await screenManagementService.generateScreenCode(targetCompanyCode); + res.json({ success: true, data: { screenCode: generatedCode } }); + } catch (error) { + console.error("화면 코드 생성 실패:", error); + res + .status(500) + .json({ success: false, message: "화면 코드 생성에 실패했습니다." }); + } +}; diff --git a/backend-node/src/routes/screenManagementRoutes.ts b/backend-node/src/routes/screenManagementRoutes.ts index 10459f7c..2317d734 100644 --- a/backend-node/src/routes/screenManagementRoutes.ts +++ b/backend-node/src/routes/screenManagementRoutes.ts @@ -9,6 +9,7 @@ import { getTableColumns, saveLayout, getLayout, + generateScreenCode, } from "../controllers/screenManagementController"; const router = express.Router(); @@ -22,6 +23,9 @@ router.post("/screens", createScreen); router.put("/screens/:id", updateScreen); router.delete("/screens/:id", deleteScreen); +// 화면 코드 자동 생성 +router.get("/generate-screen-code/:companyCode", generateScreenCode); + // 테이블 관리 router.get("/tables", getTables); router.get("/tables/:tableName/columns", getTableColumns); diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 88e4b406..65650035 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -804,6 +804,45 @@ export class ScreenManagementService { createdDate: data.created_date, }; } + + /** + * 화면 코드 자동 생성 (회사코드 + '_' + 순번) + */ + async generateScreenCode(companyCode: string): Promise { + // 해당 회사의 기존 화면 코드들 조회 + const existingScreens = await prisma.screen_definitions.findMany({ + where: { + company_code: companyCode, + screen_code: { + startsWith: companyCode, + }, + }, + select: { screen_code: true }, + orderBy: { screen_code: "desc" }, + }); + + // 회사 코드 뒤의 숫자 부분 추출하여 최대값 찾기 + let maxNumber = 0; + const pattern = new RegExp( + `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:_)?(\\d+)$` + ); + + for (const screen of existingScreens) { + const match = screen.screen_code.match(pattern); + if (match) { + const number = parseInt(match[1], 10); + if (number > maxNumber) { + maxNumber = number; + } + } + } + + // 다음 순번으로 화면 코드 생성 (3자리 패딩) + const nextNumber = maxNumber + 1; + const paddedNumber = nextNumber.toString().padStart(3, "0"); + + return `${companyCode}_${paddedNumber}`; + } } // 서비스 인스턴스 export diff --git a/docs/화면관리_시스템_설계.md b/docs/화면관리_시스템_설계.md index 67e82fba..8dbb6508 100644 --- a/docs/화면관리_시스템_설계.md +++ b/docs/화면관리_시스템_설계.md @@ -40,6 +40,7 @@ - **왼쪽 목록 UX**: 검색·페이징 도입으로 대량 테이블 로딩 지연 완화 - **Undo/Redo**: 최대 50단계, 단축키(Ctrl/Cmd+Z, Ctrl/Cmd+Y) - **위젯 타입 렌더링 보강**: code/entity/file 포함 실제 위젯 형태로 표시 +- **복사/삭제/붙여넣기 범용화**: 단일/다중/그룹 선택 모두에서 동작. 우클릭 위치 붙여넣기, 단축키(Ctrl/Cmd+C, Ctrl/Cmd+V, Delete) 지원, 상단 툴바 버튼 제공. 클립보드 상태 배지 표시(예: "3개 복사됨", "그룹 복사됨"). ### 🎯 **현재 테이블 구조와 100% 호환** @@ -657,6 +658,33 @@ return
Unknown component
; ```` +### 4. 복사/삭제/붙여넣기 규칙 (구현 완료) + +- 대상 범위 + - 단일 선택: 선택된 1개 컴포넌트에 대해 복사/삭제/붙여넣기 지원 + - 다중 선택: Shift+클릭 또는 마키 선택으로 선택된 여러 컴포넌트 일괄 복사/삭제/붙여넣기 지원 + - 그룹 선택: 그룹과 모든 자식 컴포넌트가 하나의 덩어리로 동작 + +- 동작 규칙 + - 복사 시 선택된 컴포넌트들의 바운딩 박스를 계산하여 상대 좌표 유지 + - 붙여넣기 시 새 ID로 재생성하고 부모-자식(parentId) 관계 보존 + - 기본 붙여넣기 위치는 원본 바운딩 박스 + 오프셋(+20px, +20px) + - 캔버스 우클릭 시 해당 좌표로 붙여넣기 수행(정확 위치 지정) + +- UI/피드백 + - 상단 툴바에 복사/삭제/붙여넣기 버튼 제공(선택 상황에 따라 표시/활성화) + - 클립보드 상태 배지 표시: 단일(“컴포넌트 복사됨”), 다중(“N개 복사됨”), 그룹(“그룹 복사됨”) + +- 단축키 + - 복사: Ctrl/Cmd + C + - 붙여넣기: Ctrl/Cmd + V + - 삭제: Delete 또는 Backspace + - 실행 취소/다시 실행: Ctrl/Cmd + Z, Ctrl/Cmd + Y + +- 예외 처리 + - 선택 없음 상태에서 복사/삭제는 무시 + - 클립보드가 비어있는 경우 붙여넣기 무시 + ## 🔗 테이블 타입 연계 ### 1. 웹 타입 설정 방법 @@ -2299,7 +2327,7 @@ export class TableTypeIntegrationService { - [x] 그룹 단위 이동 - [x] 그룹 UI 단순화(헤더/박스 제거) - [x] 그룹 내 정렬/균등 분배 도구(아이콘 UI) -- [ ] 그룹 단위 삭제/복사/붙여넣기 +- [x] 그룹 단위 삭제/복사/붙여넣기 ### 2. 레이아웃 저장/로드 diff --git a/frontend/components/screen/CreateScreenModal.tsx b/frontend/components/screen/CreateScreenModal.tsx new file mode 100644 index 00000000..7a221cd7 --- /dev/null +++ b/frontend/components/screen/CreateScreenModal.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { screenApi, tableTypeApi } from "@/lib/api/screen"; +import { ScreenDefinition } from "@/types/screen"; +import { useAuth } from "@/hooks/useAuth"; + +interface CreateScreenModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onCreated?: (screen: ScreenDefinition) => void; +} + +export default function CreateScreenModal({ open, onOpenChange, onCreated }: CreateScreenModalProps) { + const { user } = useAuth(); + + const [screenName, setScreenName] = useState(""); + const [screenCode, setScreenCode] = useState(""); + const [tableName, setTableName] = useState(""); + const [description, setDescription] = useState(""); + const [tables, setTables] = useState>([]); + const [submitting, setSubmitting] = useState(false); + // 화면 코드 자동 생성 + const generateCode = async () => { + try { + const companyCode = (user as any)?.company_code || (user as any)?.companyCode || "*"; + const generatedCode = await screenApi.generateScreenCode(companyCode); + setScreenCode(generatedCode); + } catch (e) { + console.error("화면 코드 생성 실패", e); + } + }; + + useEffect(() => { + if (!open) return; + let abort = false; + const loadTables = async () => { + try { + const list = await tableTypeApi.getTables(); + if (abort) return; + setTables(list.map((t) => ({ tableName: t.tableName, displayName: t.displayName || t.tableName }))); + } catch (e) { + console.error("테이블 목록 조회 실패", e); + setTables([]); + } + }; + loadTables(); + return () => { + abort = true; + }; + }, [open]); + + // 모달이 열릴 때 자동으로 화면 코드 생성 + useEffect(() => { + if (open && !screenCode) { + generateCode(); + } + }, [open, screenCode]); + + const isValid = useMemo(() => { + return screenName.trim().length > 0 && screenCode.trim().length > 0 && tableName.trim().length > 0; + }, [screenName, screenCode, tableName]); + + const handleSubmit = async () => { + if (!isValid || submitting) return; + try { + setSubmitting(true); + const companyCode = (user as any)?.company_code || (user as any)?.companyCode || "*"; + const created = await screenApi.createScreen({ + screenName: screenName.trim(), + screenCode: screenCode.trim(), + tableName: tableName.trim(), + companyCode, + description: description.trim() || undefined, + createdBy: (user as any)?.userId, + } as any); + + // 날짜 필드 보정 + const mapped: ScreenDefinition = { + ...created, + createdDate: created.createdDate ? new Date(created.createdDate as any) : new Date(), + updatedDate: created.updatedDate ? new Date(created.updatedDate as any) : new Date(), + } as ScreenDefinition; + + onCreated?.(mapped); + onOpenChange(false); + setScreenName(""); + setScreenCode(""); + setTableName(""); + setDescription(""); + } catch (e) { + console.error("화면 생성 실패", e); + // 필요 시 토스트 추가 가능 + } finally { + setSubmitting(false); + } + }; + + return ( + + + + 새 화면 생성 + + +
+
+ + setScreenName(e.target.value)} /> +
+
+ + +
+
+ + +
+
+ + setDescription(e.target.value)} /> +
+
+ + + + + +
+
+ ); +} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 2af7f679..6162163c 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -27,6 +27,8 @@ import { List, AlignLeft, ChevronRight, + Copy, + Clipboard, } from "lucide-react"; import { ScreenDefinition, @@ -58,13 +60,6 @@ interface ScreenDesignerProps { onBackToList: () => void; } -interface ComponentMoveState { - isMoving: boolean; - movingComponent: ComponentData | null; - originalPosition: { x: number; y: number }; - currentPosition: { x: number; y: number }; -} - export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenDesignerProps) { const [layout, setLayout] = useState({ components: [], @@ -81,6 +76,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD ]); const [historyIndex, setHistoryIndex] = useState(0); + // 클립보드 상태 (복사/붙여넣기용) + const [clipboard, setClipboard] = useState<{ + type: "single" | "multiple" | "group"; + data: ComponentData[]; + offset: { x: number; y: number }; + boundingBox?: { x: number; y: number; width: number; height: number }; + } | null>(null); + // 히스토리에 상태 저장 const saveToHistory = useCallback( (newLayout: LayoutData) => { @@ -114,31 +117,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } }, [historyIndex, history]); - // 키보드 단축키 지원 - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.ctrlKey || e.metaKey) { - switch (e.key) { - case "z": - e.preventDefault(); - if (e.shiftKey) { - redo(); // Ctrl+Shift+Z 또는 Cmd+Shift+Z - } else { - undo(); // Ctrl+Z 또는 Cmd+Z - } - break; - case "y": - e.preventDefault(); - redo(); // Ctrl+Y 또는 Cmd+Y - break; - } - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [undo, redo]); - const [dragState, setDragState] = useState({ isDragging: false, draggedComponent: null as ComponentData | null, @@ -610,22 +588,190 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } }, []); - // 컴포넌트 제거 함수 - const removeComponent = useCallback( - (componentId: string) => { + // 범용 복사 함수 + const copyComponents = useCallback(() => { + if (!selectedComponent && groupState.selectedComponents.length === 0) return; + + let componentsToCopy: ComponentData[] = []; + let copyType: "single" | "multiple" | "group" = "single"; + + if (selectedComponent?.type === "group") { + // 그룹 복사 + const children = getGroupChildren(layout.components, selectedComponent.id); + componentsToCopy = [selectedComponent, ...children]; + copyType = "group"; + } else if (groupState.selectedComponents.length > 1) { + // 다중 선택 복사 + componentsToCopy = layout.components.filter((comp) => groupState.selectedComponents.includes(comp.id)); + copyType = "multiple"; + } else if (selectedComponent) { + // 단일 컴포넌트 복사 + componentsToCopy = [selectedComponent]; + copyType = "single"; + } + + if (componentsToCopy.length === 0) return; + + // 바운딩 박스 계산 + const positions = componentsToCopy.map((comp) => ({ + x: comp.position.x, + y: comp.position.y, + width: comp.size.width, + height: comp.size.height, + })); + + const minX = Math.min(...positions.map((p) => p.x)); + const minY = Math.min(...positions.map((p) => p.y)); + const maxX = Math.max(...positions.map((p) => p.x + p.width)); + const maxY = Math.max(...positions.map((p) => p.y + p.height)); + + setClipboard({ + type: copyType, + data: componentsToCopy, + offset: { x: 20, y: 20 }, + boundingBox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }, + }); + }, [selectedComponent, groupState.selectedComponents, layout.components]); + + // 범용 삭제 함수 + const deleteComponents = useCallback(() => { + if (!selectedComponent && groupState.selectedComponents.length === 0) return; + + let idsToRemove: string[] = []; + + if (selectedComponent?.type === "group") { + // 그룹 삭제 (자식 컴포넌트 포함) + const childrenIds = getGroupChildren(layout.components, selectedComponent.id).map((child) => child.id); + idsToRemove = [selectedComponent.id, ...childrenIds]; + } else if (groupState.selectedComponents.length > 1) { + // 다중 선택 삭제 + idsToRemove = [...groupState.selectedComponents]; + } else if (selectedComponent) { + // 단일 컴포넌트 삭제 + idsToRemove = [selectedComponent.id]; + } + + if (idsToRemove.length === 0) return; + + const newLayout = { + ...layout, + components: layout.components.filter((comp) => !idsToRemove.includes(comp.id)), + }; + setLayout(newLayout); + saveToHistory(newLayout); + + // 선택 상태 초기화 + setSelectedComponent(null); + setGroupState((prev) => ({ ...prev, selectedComponents: [] })); + }, [selectedComponent, groupState.selectedComponents, layout, saveToHistory, setGroupState]); + + // 범용 붙여넣기 함수 + const pasteComponents = useCallback( + (pastePosition?: { x: number; y: number }) => { + if (!clipboard || clipboard.data.length === 0) return; + + const idMap = new Map(); + const newComponents: ComponentData[] = []; + + // 붙여넣기 위치 결정 + let targetPosition = pastePosition; + if (!targetPosition && clipboard.boundingBox) { + targetPosition = { + x: clipboard.boundingBox.x + clipboard.offset.x, + y: clipboard.boundingBox.y + clipboard.offset.y, + }; + } + + const offsetX = targetPosition ? targetPosition.x - (clipboard.boundingBox?.x || 0) : clipboard.offset.x; + const offsetY = targetPosition ? targetPosition.y - (clipboard.boundingBox?.y || 0) : clipboard.offset.y; + + // 모든 컴포넌트에 대해 새 ID 생성 + clipboard.data.forEach((comp) => { + const newId = generateComponentId(); + idMap.set(comp.id, newId); + }); + + // 컴포넌트 복사 및 ID/위치 업데이트 + clipboard.data.forEach((comp) => { + const newComp: ComponentData = { + ...comp, + id: idMap.get(comp.id)!, + position: { + x: comp.position.x + offsetX, + y: comp.position.y + offsetY, + }, + // 부모 ID가 있고 매핑되는 경우 업데이트 + parentId: comp.parentId && idMap.has(comp.parentId) ? idMap.get(comp.parentId)! : undefined, + }; + newComponents.push(newComp); + }); + const newLayout = { ...layout, - components: layout.components.filter((comp) => comp.id !== componentId), + components: [...layout.components, ...newComponents], }; setLayout(newLayout); saveToHistory(newLayout); - if (selectedComponent?.id === componentId) { - setSelectedComponent(null); + }, + [clipboard, layout, saveToHistory], + ); + + // 캔버스 우클릭 컨텍스트 메뉴 + const handleCanvasContextMenu = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + // 우클릭 시 붙여넣기 (클립보드에 데이터가 있는 경우) + if (clipboard && clipboard.data.length > 0) { + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + pasteComponents({ x, y }); } }, - [layout, selectedComponent, saveToHistory], + [clipboard, pasteComponents], ); + // 키보드 단축키 지원 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case "z": + e.preventDefault(); + if (e.shiftKey) { + redo(); // Ctrl+Shift+Z 또는 Cmd+Shift+Z + } else { + undo(); // Ctrl+Z 또는 Cmd+Z + } + break; + case "y": + e.preventDefault(); + redo(); // Ctrl+Y 또는 Cmd+Y + break; + case "c": + e.preventDefault(); + // 선택된 컴포넌트(들) 복사 + copyComponents(); + break; + case "v": + e.preventDefault(); + // 클립보드 내용 붙여넣기 + if (clipboard && clipboard.data.length > 0) { + pasteComponents(); + } + break; + } + } else if (e.key === "Delete") { + e.preventDefault(); + // 선택된 컴포넌트(들) 삭제 + deleteComponents(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [undo, redo, copyComponents, pasteComponents, deleteComponents, clipboard]); + // 컴포넌트 속성 업데이트 함수 const updateComponentProperty = useCallback( (componentId: string, propertyPath: string, value: any) => { @@ -1032,6 +1178,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD {selectedScreen.tableName} + {clipboard && clipboard.data.length > 0 && ( + + + {clipboard.type === "group" + ? "그룹 복사됨" + : clipboard.type === "multiple" + ? `${clipboard.data.length}개 복사됨` + : "컴포넌트 복사됨"} + + )}
+ + {/* 복사/붙여넣기/삭제 버튼들 */} + {(selectedComponent || groupState.selectedComponents.length > 0) && ( + <> + + + + )} + + {/* 붙여넣기 버튼 */} + {clipboard && clipboard.data.length > 0 && ( + + )} + diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index c2999cee..8c943e2b 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -14,6 +14,8 @@ import { } from "@/components/ui/dropdown-menu"; import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette } from "lucide-react"; import { ScreenDefinition } from "@/types/screen"; +import { screenApi } from "@/lib/api/screen"; +import CreateScreenModal from "./CreateScreenModal"; interface ScreenListProps { onScreenSelect: (screen: ScreenDefinition) => void; @@ -27,61 +29,34 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr const [searchTerm, setSearchTerm] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); + const [isCreateOpen, setIsCreateOpen] = useState(false); - // 샘플 데이터 (실제로는 API에서 가져옴) + // 화면 목록 로드 (실제 API) useEffect(() => { - const mockScreens: ScreenDefinition[] = [ - { - screenId: 1, - screenName: "사용자 관리 화면", - screenCode: "USER_MANAGEMENT", - tableName: "user_info", - companyCode: "COMP001", - description: "사용자 정보를 관리하는 화면", - isActive: "Y", - createdDate: new Date("2024-01-15"), - updatedDate: new Date("2024-01-15"), - createdBy: "admin", - updatedBy: "admin", - }, - { - screenId: 2, - screenName: "부서 관리 화면", - screenCode: "DEPT_MANAGEMENT", - tableName: "dept_info", - companyCode: "COMP001", - description: "부서 정보를 관리하는 화면", - isActive: "Y", - createdDate: new Date("2024-01-16"), - updatedDate: new Date("2024-01-16"), - createdBy: "admin", - updatedBy: "admin", - }, - { - screenId: 3, - screenName: "제품 관리 화면", - screenCode: "PRODUCT_MANAGEMENT", - tableName: "product_info", - companyCode: "COMP001", - description: "제품 정보를 관리하는 화면", - isActive: "Y", - createdDate: new Date("2024-01-17"), - updatedDate: new Date("2024-01-17"), - createdBy: "admin", - updatedBy: "admin", - }, - ]; + let abort = false; + const load = async () => { + try { + setLoading(true); + const resp = await screenApi.getScreens({ page: currentPage, size: 20, searchTerm }); + if (abort) return; + // 응답 표준: { success, data, total } + setScreens(resp.data || []); + setTotalPages(Math.max(1, Math.ceil((resp.total || 0) / 20))); + } catch (e) { + console.error("화면 목록 조회 실패", e); + setScreens([]); + setTotalPages(1); + } finally { + if (!abort) setLoading(false); + } + }; + load(); + return () => { + abort = true; + }; + }, [currentPage, searchTerm]); - setScreens(mockScreens); - setLoading(false); - }, []); - - const filteredScreens = screens.filter( - (screen) => - screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) || - screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase()) || - screen.tableName.toLowerCase().includes(searchTerm.toLowerCase()), - ); + const filteredScreens = screens; // 서버 필터 기준 사용 const handleScreenSelect = (screen: ScreenDefinition) => { onScreenSelect(screen); @@ -132,7 +107,7 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr />
- @@ -141,7 +116,7 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr - 화면 목록 ({filteredScreens.length}) + 화면 목록 ({screens.length}) @@ -157,7 +132,7 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr - {filteredScreens.map((screen) => ( + {screens.map((screen) => ( )} + {/* 새 화면 생성 모달 */} + { + // 목록에 즉시 반영 (첫 페이지 기준 상단 추가) + setScreens((prev) => [created, ...prev]); + }} + /> ); } diff --git a/frontend/lib/api/screen.ts b/frontend/lib/api/screen.ts index ce61be97..a7167c30 100644 --- a/frontend/lib/api/screen.ts +++ b/frontend/lib/api/screen.ts @@ -18,13 +18,32 @@ export const screenApi = { searchTerm?: string; }): Promise> => { const response = await apiClient.get("/screen-management/screens", { params }); - return response.data; + const raw = response.data || {}; + const items: any[] = (raw.data ?? raw.items ?? []) as any[]; + const mapped: ScreenDefinition[] = items.map((it) => ({ + ...it, + // 문자열로 온 날짜를 Date 객체로 변환 (이미 Date이면 그대로 유지) + createdDate: it.createdDate ? new Date(it.createdDate) : undefined, + updatedDate: it.updatedDate ? new Date(it.updatedDate) : undefined, + })); + + const page = raw.page ?? params.page ?? 1; + const size = raw.size ?? params.size ?? mapped.length; + const total = raw.total ?? mapped.length; + const totalPages = raw.totalPages ?? Math.max(1, Math.ceil(total / (size || 1))); + + return { data: mapped, total, page, size, totalPages }; }, // 화면 상세 조회 getScreen: async (screenId: number): Promise => { const response = await apiClient.get(`/screen-management/screens/${screenId}`); - return response.data.data; + const raw = response.data?.data || response.data; + return { + ...raw, + createdDate: raw?.createdDate ? new Date(raw.createdDate) : undefined, + updatedDate: raw?.updatedDate ? new Date(raw.updatedDate) : undefined, + } as ScreenDefinition; }, // 화면 생성 @@ -33,6 +52,12 @@ export const screenApi = { return response.data.data; }, + // 화면 코드 자동 생성 (회사코드 + 순번) + generateScreenCode: async (companyCode: string): Promise => { + const response = await apiClient.get(`/screen-management/generate-screen-code/${companyCode}`); + return response.data.data.screenCode; + }, + // 화면 수정 updateScreen: async (screenId: number, screenData: UpdateScreenRequest): Promise => { const response = await apiClient.put(`/screen-management/screens/${screenId}`, screenData);