diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 29528ebc..520c8f42 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -1,4 +1,5 @@ import { Request, Response } from "express"; +import { Client } from "pg"; import { logger } from "../utils/logger"; import { AuthenticatedRequest } from "../types/auth"; import { ApiResponse } from "../types/common"; @@ -404,36 +405,25 @@ export async function updateColumnWebType( return; } - // PostgreSQL 클라이언트 생성 - const client = new Client({ - connectionString: process.env.DATABASE_URL, - }); + const tableManagementService = new TableManagementService(); + await tableManagementService.updateColumnWebType( + tableName, + columnName, + webType, + detailSettings + ); - await client.connect(); + logger.info( + `컬럼 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}` + ); - try { - const tableManagementService = new TableManagementService(client); - await tableManagementService.updateColumnWebType( - tableName, - columnName, - webType, - detailSettings - ); + const response: ApiResponse = { + success: true, + message: "컬럼 웹 타입이 성공적으로 설정되었습니다.", + data: null, + }; - logger.info( - `컬럼 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}` - ); - - const response: ApiResponse = { - success: true, - message: "컬럼 웹 타입이 성공적으로 설정되었습니다.", - data: null, - }; - - res.status(200).json(response); - } finally { - await client.end(); - } + res.status(200).json(response); } catch (error) { logger.error("컬럼 웹 타입 설정 중 오류 발생:", error); diff --git a/backend-node/src/services/authService.ts b/backend-node/src/services/authService.ts index 60ccb263..8a1a9720 100644 --- a/backend-node/src/services/authService.ts +++ b/backend-node/src/services/authService.ts @@ -171,8 +171,8 @@ export class AuthService { // 권한명들을 쉼표로 연결 const authNames = authInfo - .filter((auth) => auth.authority_master?.auth_name) - .map((auth) => auth.authority_master!.auth_name!) + .filter((auth: any) => auth.authority_master?.auth_name) + .map((auth: any) => auth.authority_master!.auth_name!) .join(","); // 회사 정보 조회 (Prisma ORM 사용으로 변경) diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index d7fbab1f..df1246a6 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -368,48 +368,40 @@ export class TableManagementService { }; // column_labels 테이블에 해당 컬럼이 있는지 확인 - const checkQuery = ` - SELECT COUNT(*) as count - FROM column_labels - WHERE table_name = $1 AND column_name = $2 - `; + const existingColumn = await prisma.column_labels.findFirst({ + where: { + table_name: tableName, + column_name: columnName, + }, + }); - const checkResult = await this.client.query(checkQuery, [ - tableName, - columnName, - ]); - - if (checkResult.rows[0].count > 0) { + if (existingColumn) { // 기존 컬럼 라벨 업데이트 - const updateQuery = ` - UPDATE column_labels - SET web_type = $3, detail_settings = $4, updated_date = NOW() - WHERE table_name = $1 AND column_name = $2 - `; - - await this.client.query(updateQuery, [ - tableName, - columnName, - webType, - JSON.stringify(finalDetailSettings), - ]); + await prisma.column_labels.update({ + where: { + id: existingColumn.id, + }, + data: { + web_type: webType, + detail_settings: JSON.stringify(finalDetailSettings), + updated_date: new Date(), + }, + }); logger.info( `컬럼 웹 타입 업데이트 완료: ${tableName}.${columnName} = ${webType}` ); } else { // 새로운 컬럼 라벨 생성 - const insertQuery = ` - INSERT INTO column_labels ( - table_name, column_name, web_type, detail_settings, created_date, updated_date - ) VALUES ($1, $2, $3, $4, NOW(), NOW()) - `; - - await this.client.query(insertQuery, [ - tableName, - columnName, - webType, - JSON.stringify(finalDetailSettings), - ]); + await prisma.column_labels.create({ + data: { + table_name: tableName, + column_name: columnName, + web_type: webType, + detail_settings: JSON.stringify(finalDetailSettings), + created_date: new Date(), + updated_date: new Date(), + }, + }); logger.info( `컬럼 라벨 생성 및 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}` ); diff --git a/docs/화면관리_시스템_설계.md b/docs/화면관리_시스템_설계.md index 105ff998..1c72c952 100644 --- a/docs/화면관리_시스템_설계.md +++ b/docs/화면관리_시스템_설계.md @@ -1101,28 +1101,74 @@ Body: { ## 🎭 프론트엔드 구현 -### 1. 화면 설계기 컴포넌트 +### 1. 화면 설계기 컴포넌트 (구현 완료) ```typescript -// ScreenDesigner.tsx -export default function ScreenDesigner() { +// ScreenDesigner.tsx - 현재 구현된 버전 +export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenDesignerProps) { const [layout, setLayout] = useState({ components: [] }); - const [selectedComponent, setSelectedComponent] = useState( - null - ); - const [dragState, setDragState] = useState({ - isDragging: false, - draggedItem: null, - dragSource: "toolbox", - dropTarget: null, - }); - const [groupState, setGroupState] = useState({ - isGrouping: false, - selectedComponents: [], - groupTarget: null, - groupMode: "create", - }); - const [userCompanyCode, setUserCompanyCode] = useState(""); + const [selectedComponent, setSelectedComponent] = useState(null); + + // 실행취소/다시실행을 위한 히스토리 상태 + const [history, setHistory] = useState([{ + components: [], + gridSettings: { columns: 12, gap: 16, padding: 16 }, + }]); + const [historyIndex, setHistoryIndex] = useState(0); + + // 히스토리에 상태 저장 + const saveToHistory = useCallback((newLayout: LayoutData) => { + setHistory(prevHistory => { + const newHistory = prevHistory.slice(0, historyIndex + 1); + newHistory.push(JSON.parse(JSON.stringify(newLayout))); // 깊은 복사 + return newHistory.slice(-50); // 최대 50개 히스토리 유지 + }); + setHistoryIndex(prevIndex => Math.min(prevIndex + 1, 49)); + }, [historyIndex]); + + // 실행취소/다시실행 함수 + const undo = useCallback(() => { + if (historyIndex > 0) { + const newIndex = historyIndex - 1; + setHistoryIndex(newIndex); + setLayout(JSON.parse(JSON.stringify(history[newIndex]))); + setSelectedComponent(null); + } + }, [historyIndex, history]); + + const redo = useCallback(() => { + if (historyIndex < history.length - 1) { + const newIndex = historyIndex + 1; + setHistoryIndex(newIndex); + setLayout(JSON.parse(JSON.stringify(history[newIndex]))); + setSelectedComponent(null); + } + }, [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]); ``` // 컴포넌트 추가 @@ -1269,24 +1315,280 @@ export function useDragAndDrop() { } ```` -### 3. 그리드 시스템 +### 3. 실시간 미리보기 시스템 (구현 완료) ```typescript -// GridSystem.tsx -export default function GridSystem({ children, columns = 12 }) { - const gridStyle = { - display: "grid", - gridTemplateColumns: `repeat(${columns}, 1fr)`, - gap: "16px", - padding: "16px", - }; +// RealtimePreview.tsx - 현재 구현된 버전 +export const RealtimePreview: React.FC = ({ + component, + isSelected = false, + onClick, + onDragStart, + onDragEnd, +}) => { + const { type, label, tableName, columnName, widgetType, size, style } = + component; return ( -
- {children} +
+ {type === "container" && ( +
+
+ +
+
{label}
+
{tableName}
+
+
+
+ )} + + {type === "widget" && ( +
+ {/* 위젯 헤더 */} +
+ {getWidgetIcon(widgetType)} +
+ +
+
+ + {/* 위젯 본체 */} +
{renderWidget(component)}
+ + {/* 위젯 정보 */} +
+ {columnName} ({widgetType}) +
+
+ )}
); -} +}; + +// 웹 타입에 따른 위젯 렌더링 +const renderWidget = (component: ComponentData) => { + const { widgetType, label, placeholder, required, readonly, columnName } = + component; + + const commonProps = { + placeholder: placeholder || `입력하세요...`, + disabled: readonly, + required: required, + className: "w-full", + }; + + switch (widgetType) { + case "text": + case "email": + case "tel": + return ( + + ); + + case "number": + case "decimal": + return ( + + ); + + case "date": + case "datetime": + return ( + + ); + + case "select": + case "dropdown": + return ( + + ); + + case "textarea": + case "text_area": + return