diff --git a/backend-node/src/controllers/authController.ts b/backend-node/src/controllers/authController.ts index ba9dcdc1..374015ee 100644 --- a/backend-node/src/controllers/authController.ts +++ b/backend-node/src/controllers/authController.ts @@ -59,12 +59,56 @@ export class AuthController { logger.info(`- userName: ${userInfo.userName}`); logger.info(`- companyCode: ${userInfo.companyCode}`); + // 사용자의 첫 번째 접근 가능한 메뉴 조회 + let firstMenuPath: string | null = null; + try { + const { AdminService } = await import("../services/adminService"); + const paramMap = { + userId: loginResult.userInfo.userId, + userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN", + userType: loginResult.userInfo.userType, + userLang: "ko", + }; + + const menuList = await AdminService.getUserMenuList(paramMap); + logger.info(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`); + + // 접근 가능한 첫 번째 메뉴 찾기 + // 조건: + // 1. LEV (레벨)이 2 이상 (최상위 폴더 제외) + // 2. MENU_URL이 있고 비어있지 않음 + // 3. 이미 PATH, SEQ로 정렬되어 있으므로 첫 번째로 찾은 것이 첫 번째 메뉴 + const firstMenu = menuList.find((menu: any) => { + const level = menu.lev || menu.level; + const url = menu.menu_url || menu.url; + + return level >= 2 && url && url.trim() !== "" && url !== "#"; + }); + + if (firstMenu) { + firstMenuPath = firstMenu.menu_url || firstMenu.url; + logger.info(`✅ 첫 번째 접근 가능한 메뉴 발견:`, { + name: firstMenu.menu_name_kor || firstMenu.translated_name, + url: firstMenuPath, + level: firstMenu.lev || firstMenu.level, + seq: firstMenu.seq, + }); + } else { + logger.info( + "⚠️ 접근 가능한 메뉴가 없습니다. 메인 페이지로 이동합니다." + ); + } + } catch (menuError) { + logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError); + } + res.status(200).json({ success: true, message: "로그인 성공", data: { userInfo, token: loginResult.token, + firstMenuPath, // 첫 번째 접근 가능한 메뉴 경로 추가 }, }); } else { diff --git a/backend-node/src/controllers/dynamicFormController.ts b/backend-node/src/controllers/dynamicFormController.ts index a5b2f225..47ee4e94 100644 --- a/backend-node/src/controllers/dynamicFormController.ts +++ b/backend-node/src/controllers/dynamicFormController.ts @@ -216,7 +216,7 @@ export const deleteFormData = async ( ): Promise => { try { const { id } = req.params; - const { companyCode } = req.user as any; + const { companyCode, userId } = req.user as any; const { tableName } = req.body; if (!tableName) { @@ -226,7 +226,7 @@ export const deleteFormData = async ( }); } - await dynamicFormService.deleteFormData(id, tableName); // parseInt 제거 - 문자열 ID 지원 + await dynamicFormService.deleteFormData(id, tableName, companyCode, userId); // userId 추가 res.json({ success: true, diff --git a/backend-node/src/services/dataflowControlService.ts b/backend-node/src/services/dataflowControlService.ts index d4ffcffe..75ff0a5c 100644 --- a/backend-node/src/services/dataflowControlService.ts +++ b/backend-node/src/services/dataflowControlService.ts @@ -64,7 +64,8 @@ export class DataflowControlService { relationshipId: string, triggerType: "insert" | "update" | "delete", sourceData: Record, - tableName: string + tableName: string, + userId: string = "system" ): Promise<{ success: boolean; message: string; @@ -78,6 +79,7 @@ export class DataflowControlService { triggerType, sourceData, tableName, + userId, }); // 관계도 정보 조회 @@ -238,7 +240,8 @@ export class DataflowControlService { const actionResult = await this.executeMultiConnectionAction( action, sourceData, - targetPlan.sourceTable + targetPlan.sourceTable, + userId ); executedActions.push({ @@ -288,7 +291,8 @@ export class DataflowControlService { private async executeMultiConnectionAction( action: ControlAction, sourceData: Record, - sourceTable: string + sourceTable: string, + userId: string = "system" ): Promise { try { const extendedAction = action as any; // redesigned UI 구조 접근 @@ -321,7 +325,8 @@ export class DataflowControlService { targetTable, fromConnection.id, toConnection.id, - multiConnService + multiConnService, + userId ); case "update": @@ -332,7 +337,8 @@ export class DataflowControlService { targetTable, fromConnection.id, toConnection.id, - multiConnService + multiConnService, + userId ); case "delete": @@ -343,7 +349,8 @@ export class DataflowControlService { targetTable, fromConnection.id, toConnection.id, - multiConnService + multiConnService, + userId ); default: @@ -368,7 +375,8 @@ export class DataflowControlService { targetTable: string, fromConnectionId: number, toConnectionId: number, - multiConnService: any + multiConnService: any, + userId: string = "system" ): Promise { try { // 필드 매핑 적용 @@ -387,6 +395,14 @@ export class DataflowControlService { } } + // 🆕 변경자 정보 추가 + if (!mappedData.created_by) { + mappedData.created_by = userId; + } + if (!mappedData.updated_by) { + mappedData.updated_by = userId; + } + console.log(`📋 매핑된 데이터:`, mappedData); // 대상 연결에 데이터 삽입 @@ -421,11 +437,32 @@ export class DataflowControlService { targetTable: string, fromConnectionId: number, toConnectionId: number, - multiConnService: any + multiConnService: any, + userId: string = "system" ): Promise { try { - // UPDATE 로직 구현 (향후 확장) + // 필드 매핑 적용 + const mappedData: Record = {}; + + for (const mapping of action.fieldMappings) { + const sourceField = mapping.sourceField; + const targetField = mapping.targetField; + + if (mapping.defaultValue !== undefined) { + mappedData[targetField] = mapping.defaultValue; + } else if (sourceField && sourceData[sourceField] !== undefined) { + mappedData[targetField] = sourceData[sourceField]; + } + } + + // 🆕 변경자 정보 추가 + if (!mappedData.updated_by) { + mappedData.updated_by = userId; + } + + console.log(`📋 UPDATE 매핑된 데이터:`, mappedData); console.log(`⚠️ UPDATE 액션은 향후 구현 예정`); + return { success: true, message: "UPDATE 액션 실행됨 (향후 구현)", @@ -449,11 +486,11 @@ export class DataflowControlService { targetTable: string, fromConnectionId: number, toConnectionId: number, - multiConnService: any + multiConnService: any, + userId: string = "system" ): Promise { try { - // DELETE 로직 구현 (향후 확장) - console.log(`⚠️ DELETE 액션은 향후 구현 예정`); + console.log(`⚠️ DELETE 액션은 향후 구현 예정 (변경자: ${userId})`); return { success: true, message: "DELETE 액션 실행됨 (향후 구현)", @@ -941,7 +978,9 @@ export class DataflowControlService { sourceData: Record ): Promise { // 보안상 외부 DB에 대한 DELETE 작업은 비활성화 - throw new Error("보안상 외부 데이터베이스에 대한 DELETE 작업은 허용되지 않습니다. SELECT 쿼리만 사용해주세요."); + throw new Error( + "보안상 외부 데이터베이스에 대한 DELETE 작업은 허용되지 않습니다. SELECT 쿼리만 사용해주세요." + ); const results = []; diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 999ea6d2..8a943b96 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -220,8 +220,14 @@ export class DynamicFormService { console.log(`🔑 테이블 ${tableName}의 Primary Key:`, primaryKeys); // 메타데이터 제거 (실제 테이블 컬럼이 아님) - const { created_by, updated_by, company_code, screen_id, ...actualData } = - data; + const { + created_by, + updated_by, + writer, + company_code, + screen_id, + ...actualData + } = data; // 기본 데이터 준비 const dataToInsert: any = { ...actualData }; @@ -236,8 +242,17 @@ export class DynamicFormService { if (tableColumns.includes("regdate") && !dataToInsert.regdate) { dataToInsert.regdate = new Date(); } + if (tableColumns.includes("created_date") && !dataToInsert.created_date) { + dataToInsert.created_date = new Date(); + } + if (tableColumns.includes("updated_date") && !dataToInsert.updated_date) { + dataToInsert.updated_date = new Date(); + } - // 생성자/수정자 정보가 있고 해당 컬럼이 존재한다면 추가 + // 작성자 정보 추가 (writer 컬럼 우선, 없으면 created_by/updated_by) + if (writer && tableColumns.includes("writer")) { + dataToInsert.writer = writer; + } if (created_by && tableColumns.includes("created_by")) { dataToInsert.created_by = created_by; } @@ -579,7 +594,8 @@ export class DynamicFormService { screenId, tableName, insertedRecord as Record, - "insert" + "insert", + created_by || "system" ); } catch (controlError) { console.error("⚠️ 제어관리 실행 오류:", controlError); @@ -876,7 +892,8 @@ export class DynamicFormService { 0, // UPDATE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요) tableName, updatedRecord as Record, - "update" + "update", + updated_by || "system" ); } catch (controlError) { console.error("⚠️ 제어관리 실행 오류:", controlError); @@ -905,7 +922,8 @@ export class DynamicFormService { async deleteFormData( id: string | number, tableName: string, - companyCode?: string + companyCode?: string, + userId?: string ): Promise { try { console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", { @@ -1010,7 +1028,8 @@ export class DynamicFormService { 0, // DELETE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요) tableName, deletedRecord, - "delete" + "delete", + userId || "system" ); } } catch (controlError) { @@ -1315,7 +1334,8 @@ export class DynamicFormService { screenId: number, tableName: string, savedData: Record, - triggerType: "insert" | "update" | "delete" + triggerType: "insert" | "update" | "delete", + userId: string = "system" ): Promise { try { console.log(`🎯 제어관리 설정 확인 중... (screenId: ${screenId})`); @@ -1364,7 +1384,8 @@ export class DynamicFormService { relationshipId, triggerType, savedData, - tableName + tableName, + userId ); console.log(`🎯 제어관리 실행 결과:`, controlResult); diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index d365ebbd..353ee997 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -15,12 +15,17 @@ import { FlowButtonGroup } from "@/components/screen/widgets/FlowButtonGroup"; import { FlowVisibilityConfig } from "@/types/control-management"; import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; +import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; +import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보 export default function ScreenViewPage() { const params = useParams(); const router = useRouter(); const screenId = parseInt(params.screenId as string); + // 🆕 현재 로그인한 사용자 정보 + const { user, userName, companyCode } = useAuth(); + const [screen, setScreen] = useState(null); const [layout, setLayout] = useState(null); const [loading, setLoading] = useState(true); @@ -211,302 +216,314 @@ export default function ScreenViewPage() { const screenHeight = layout?.screenResolution?.height || 800; return ( -
- {/* 절대 위치 기반 렌더링 */} - {layout && layout.components.length > 0 ? ( -
- {/* 최상위 컴포넌트들 렌더링 */} - {(() => { - // 🆕 플로우 버튼 그룹 감지 및 처리 - const topLevelComponents = layout.components.filter((component) => !component.parentId); + +
+ {/* 절대 위치 기반 렌더링 */} + {layout && layout.components.length > 0 ? ( +
+ {/* 최상위 컴포넌트들 렌더링 */} + {(() => { + // 🆕 플로우 버튼 그룹 감지 및 처리 + const topLevelComponents = layout.components.filter((component) => !component.parentId); - const buttonGroups: Record = {}; - const processedButtonIds = new Set(); + const buttonGroups: Record = {}; + const processedButtonIds = new Set(); - topLevelComponents.forEach((component) => { - const isButton = - component.type === "button" || - (component.type === "component" && - ["button-primary", "button-secondary"].includes((component as any).componentType)); + topLevelComponents.forEach((component) => { + const isButton = + component.type === "button" || + (component.type === "component" && + ["button-primary", "button-secondary"].includes((component as any).componentType)); - if (isButton) { - const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as - | FlowVisibilityConfig - | undefined; + if (isButton) { + const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as + | FlowVisibilityConfig + | undefined; - if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { - if (!buttonGroups[flowConfig.groupId]) { - buttonGroups[flowConfig.groupId] = []; + if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { + if (!buttonGroups[flowConfig.groupId]) { + buttonGroups[flowConfig.groupId] = []; + } + buttonGroups[flowConfig.groupId].push(component); + processedButtonIds.add(component.id); } - buttonGroups[flowConfig.groupId].push(component); - processedButtonIds.add(component.id); } - } - }); + }); - const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); - return ( - <> - {/* 일반 컴포넌트들 */} - {regularComponents.map((component) => ( - {}} - screenId={screenId} - tableName={screen?.tableName} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(_, selectedData) => { - console.log("🔍 화면에서 선택된 행 데이터:", selectedData); - setSelectedRowsData(selectedData); - }} - flowSelectedData={flowSelectedData} - flowSelectedStepId={flowSelectedStepId} - onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { - console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", { - dataCount: selectedData.length, - selectedData, - stepId, - }); - setFlowSelectedData(selectedData); - setFlowSelectedStepId(stepId); - console.log("🔍 [page.tsx] 상태 업데이트 완료"); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - console.log("🔄 테이블 새로고침 요청됨"); - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); // 선택 해제 - }} - flowRefreshKey={flowRefreshKey} - onFlowRefresh={() => { - console.log("🔄 플로우 새로고침 요청됨"); - setFlowRefreshKey((prev) => prev + 1); - setFlowSelectedData([]); // 선택 해제 - setFlowSelectedStepId(null); - }} - formData={formData} - onFormDataChange={(fieldName, value) => { - console.log("📝 폼 데이터 변경:", fieldName, "=", value); - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - > - {/* 자식 컴포넌트들 */} - {(component.type === "group" || component.type === "container" || component.type === "area") && - layout.components - .filter((child) => child.parentId === component.id) - .map((child) => { - // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 - const relativeChildComponent = { - ...child, - position: { - x: child.position.x - component.position.x, - y: child.position.y - component.position.y, - z: child.position.z || 1, - }, - }; - - return ( - {}} - screenId={screenId} - tableName={screen?.tableName} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(_, selectedData) => { - console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); - setSelectedRowsData(selectedData); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - console.log("🔄 테이블 새로고침 요청됨 (자식)"); - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); // 선택 해제 - }} - formData={formData} - onFormDataChange={(fieldName, value) => { - console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value); - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - /> - ); - })} - - ))} - - {/* 🆕 플로우 버튼 그룹들 */} - {Object.entries(buttonGroups).map(([groupId, buttons]) => { - if (buttons.length === 0) return null; - - const firstButton = buttons[0]; - const groupConfig = (firstButton as any).webTypeConfig?.flowVisibilityConfig as FlowVisibilityConfig; - - // 그룹의 위치는 모든 버튼 중 가장 왼쪽/위쪽 버튼의 위치 사용 - const groupPosition = buttons.reduce( - (min, button) => ({ - x: Math.min(min.x, button.position.x), - y: Math.min(min.y, button.position.y), - z: min.z, - }), - { x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 }, - ); - - // 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산 - const direction = groupConfig.groupDirection || "horizontal"; - const gap = groupConfig.groupGap ?? 8; - - let groupWidth = 0; - let groupHeight = 0; - - if (direction === "horizontal") { - groupWidth = buttons.reduce((total, button, index) => { - const buttonWidth = button.size?.width || 100; - const gapWidth = index < buttons.length - 1 ? gap : 0; - return total + buttonWidth + gapWidth; - }, 0); - groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40)); - } else { - groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100)); - groupHeight = buttons.reduce((total, button, index) => { - const buttonHeight = button.size?.height || 40; - const gapHeight = index < buttons.length - 1 ? gap : 0; - return total + buttonHeight + gapHeight; - }, 0); - } - - return ( -
+ {/* 일반 컴포넌트들 */} + {regularComponents.map((component) => ( + {}} + screenId={screenId} + tableName={screen?.tableName} + userId={user?.userId} + userName={userName} + companyCode={companyCode} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + console.log("🔍 화면에서 선택된 행 데이터:", selectedData); + setSelectedRowsData(selectedData); + }} + flowSelectedData={flowSelectedData} + flowSelectedStepId={flowSelectedStepId} + onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { + console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", { + dataCount: selectedData.length, + selectedData, + stepId, + }); + setFlowSelectedData(selectedData); + setFlowSelectedStepId(stepId); + console.log("🔍 [page.tsx] 상태 업데이트 완료"); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + console.log("🔄 테이블 새로고침 요청됨"); + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); // 선택 해제 + }} + flowRefreshKey={flowRefreshKey} + onFlowRefresh={() => { + console.log("🔄 플로우 새로고침 요청됨"); + setFlowRefreshKey((prev) => prev + 1); + setFlowSelectedData([]); // 선택 해제 + setFlowSelectedStepId(null); + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + console.log("📝 폼 데이터 변경:", fieldName, "=", value); + setFormData((prev) => ({ ...prev, [fieldName]: value })); }} > - { - const relativeButton = { - ...button, - position: { x: 0, y: 0, z: button.position.z || 1 }, - }; + {/* 자식 컴포넌트들 */} + {(component.type === "group" || component.type === "container" || component.type === "area") && + layout.components + .filter((child) => child.parentId === component.id) + .map((child) => { + // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 + const relativeChildComponent = { + ...child, + position: { + x: child.position.x - component.position.x, + y: child.position.y - component.position.y, + z: child.position.z || 1, + }, + }; - return ( -
-
- {}} - screenId={screenId} - tableName={screen?.tableName} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(_, selectedData) => { - setSelectedRowsData(selectedData); - }} - flowSelectedData={flowSelectedData} - flowSelectedStepId={flowSelectedStepId} - onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { - setFlowSelectedData(selectedData); - setFlowSelectedStepId(stepId); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); - }} - flowRefreshKey={flowRefreshKey} - onFlowRefresh={() => { - setFlowRefreshKey((prev) => prev + 1); - setFlowSelectedData([]); - setFlowSelectedStepId(null); - }} - onFormDataChange={(fieldName, value) => { - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - /> -
-
- ); + return ( + {}} + screenId={screenId} + tableName={screen?.tableName} + userId={user?.userId} + userName={userName} + companyCode={companyCode} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); + setSelectedRowsData(selectedData); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + console.log("🔄 테이블 새로고침 요청됨 (자식)"); + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); // 선택 해제 + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value); + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} + /> + ); + })} + + ))} + + {/* 🆕 플로우 버튼 그룹들 */} + {Object.entries(buttonGroups).map(([groupId, buttons]) => { + if (buttons.length === 0) return null; + + const firstButton = buttons[0]; + const groupConfig = (firstButton as any).webTypeConfig + ?.flowVisibilityConfig as FlowVisibilityConfig; + + // 그룹의 위치는 모든 버튼 중 가장 왼쪽/위쪽 버튼의 위치 사용 + const groupPosition = buttons.reduce( + (min, button) => ({ + x: Math.min(min.x, button.position.x), + y: Math.min(min.y, button.position.y), + z: min.z, + }), + { x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 }, + ); + + // 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산 + const direction = groupConfig.groupDirection || "horizontal"; + const gap = groupConfig.groupGap ?? 8; + + let groupWidth = 0; + let groupHeight = 0; + + if (direction === "horizontal") { + groupWidth = buttons.reduce((total, button, index) => { + const buttonWidth = button.size?.width || 100; + const gapWidth = index < buttons.length - 1 ? gap : 0; + return total + buttonWidth + gapWidth; + }, 0); + groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40)); + } else { + groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100)); + groupHeight = buttons.reduce((total, button, index) => { + const buttonHeight = button.size?.height || 40; + const gapHeight = index < buttons.length - 1 ? gap : 0; + return total + buttonHeight + gapHeight; + }, 0); + } + + return ( +
-
- ); - })} - - ); - })()} -
- ) : ( - // 빈 화면일 때 -
-
-
- 📄 -
-

화면이 비어있습니다

-

이 화면에는 아직 설계된 컴포넌트가 없습니다.

-
-
- )} + > + { + const relativeButton = { + ...button, + position: { x: 0, y: 0, z: button.position.z || 1 }, + }; - {/* 편집 모달 */} - { - setEditModalOpen(false); - setEditModalConfig({}); - }} - screenId={editModalConfig.screenId} - modalSize={editModalConfig.modalSize} - editData={editModalConfig.editData} - onSave={editModalConfig.onSave} - modalTitle={editModalConfig.modalTitle} - modalDescription={editModalConfig.modalDescription} - onDataChange={(changedFormData) => { - console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData); - // 변경된 데이터를 메인 폼에 반영 - setFormData((prev) => { - const updatedFormData = { - ...prev, - ...changedFormData, // 변경된 필드들만 업데이트 - }; - console.log("📊 메인 폼 데이터 업데이트:", updatedFormData); - return updatedFormData; - }); - }} - /> -
+ return ( +
+
+ {}} + screenId={screenId} + tableName={screen?.tableName} + userId={user?.userId} + userName={userName} + companyCode={companyCode} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + setSelectedRowsData(selectedData); + }} + flowSelectedData={flowSelectedData} + flowSelectedStepId={flowSelectedStepId} + onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { + setFlowSelectedData(selectedData); + setFlowSelectedStepId(stepId); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); + }} + flowRefreshKey={flowRefreshKey} + onFlowRefresh={() => { + setFlowRefreshKey((prev) => prev + 1); + setFlowSelectedData([]); + setFlowSelectedStepId(null); + }} + onFormDataChange={(fieldName, value) => { + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} + /> +
+
+ ); + }} + /> +
+ ); + })} + + ); + })()} +
+ ) : ( + // 빈 화면일 때 +
+
+
+ 📄 +
+

화면이 비어있습니다

+

이 화면에는 아직 설계된 컴포넌트가 없습니다.

+
+
+ )} + + {/* 편집 모달 */} + { + setEditModalOpen(false); + setEditModalConfig({}); + }} + screenId={editModalConfig.screenId} + modalSize={editModalConfig.modalSize} + editData={editModalConfig.editData} + onSave={editModalConfig.onSave} + modalTitle={editModalConfig.modalTitle} + modalDescription={editModalConfig.modalDescription} + onDataChange={(changedFormData) => { + console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData); + // 변경된 데이터를 메인 폼에 반영 + setFormData((prev) => { + const updatedFormData = { + ...prev, + ...changedFormData, // 변경된 필드들만 업데이트 + }; + console.log("📊 메인 폼 데이터 업데이트:", updatedFormData); + return updatedFormData; + }); + }} + /> +
+ ); } diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index bc483325..843e88e6 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -35,6 +35,8 @@ export const ScreenModal: React.FC = ({ className }) => { const [screenDimensions, setScreenDimensions] = useState<{ width: number; height: number; + offsetX?: number; + offsetY?: number; } | null>(null); // 폼 데이터 상태 추가 @@ -42,11 +44,20 @@ export const ScreenModal: React.FC = ({ className }) => { // 화면의 실제 크기 계산 함수 const calculateScreenDimensions = (components: ComponentData[]) => { + if (components.length === 0) { + return { + width: 400, + height: 300, + offsetX: 0, + offsetY: 0, + }; + } + // 모든 컴포넌트의 경계 찾기 let minX = Infinity; let minY = Infinity; - let maxX = 0; - let maxY = 0; + let maxX = -Infinity; + let maxY = -Infinity; components.forEach((component) => { const x = parseFloat(component.position?.x?.toString() || "0"); @@ -60,17 +71,22 @@ export const ScreenModal: React.FC = ({ className }) => { maxY = Math.max(maxY, y + height); }); - // 컨텐츠 실제 크기 + 넉넉한 여백 (양쪽 각 64px) + // 실제 컨텐츠 크기 계산 const contentWidth = maxX - minX; const contentHeight = maxY - minY; - const padding = 128; // 좌우 또는 상하 합계 여백 - const finalWidth = Math.max(contentWidth + padding, 400); // 최소 400px - const finalHeight = Math.max(contentHeight + padding, 300); // 최소 300px + // 적절한 여백 추가 + const paddingX = 40; + const paddingY = 40; + + const finalWidth = Math.max(contentWidth + paddingX, 400); + const finalHeight = Math.max(contentHeight + paddingY, 300); return { - width: Math.min(finalWidth, window.innerWidth * 0.98), - height: Math.min(finalHeight, window.innerHeight * 0.95), + width: Math.min(finalWidth, window.innerWidth * 0.95), + height: Math.min(finalHeight, window.innerHeight * 0.9), + offsetX: Math.max(0, minX - paddingX / 2), // 좌측 여백 고려 + offsetY: Math.max(0, minY - paddingY / 2), // 상단 여백 고려 }; }; @@ -172,20 +188,20 @@ export const ScreenModal: React.FC = ({ className }) => { const getModalStyle = () => { if (!screenDimensions) { return { - className: "w-fit min-w-[400px] max-w-4xl max-h-[80vh] overflow-hidden", + className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0", style: {}, }; } - // 헤더 높이만 고려 (패딩 제거) - const headerHeight = 73; // DialogHeader 실제 높이 (border-b px-6 py-4 포함) + // 헤더 높이를 최소화 (제목 영역만) + const headerHeight = 60; // DialogHeader 최소 높이 (타이틀 + 최소 패딩) const totalHeight = screenDimensions.height + headerHeight; return { className: "overflow-hidden p-0", style: { - width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`, // 화면 크기 그대로 - height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, // 헤더 + 화면 높이 + width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`, + height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, maxWidth: "98vw", maxHeight: "95vh", }, @@ -197,12 +213,14 @@ export const ScreenModal: React.FC = ({ className }) => { return ( - - {modalState.title} - {loading ? "화면을 불러오는 중입니다..." : "화면 내용을 표시합니다."} + + {modalState.title} + {loading && ( + {loading ? "화면을 불러오는 중입니다..." : ""} + )} -
+
{loading ? (
@@ -216,35 +234,50 @@ export const ScreenModal: React.FC = ({ className }) => { style={{ width: screenDimensions?.width || 800, height: screenDimensions?.height || 600, - transformOrigin: 'center center', - maxWidth: '100%', - maxHeight: '100%', + transformOrigin: "center center", + maxWidth: "100%", + maxHeight: "100%", }} > - {screenData.components.map((component) => ( - { - console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`); - console.log("📋 현재 formData:", formData); - setFormData((prev) => { - const newFormData = { - ...prev, - [fieldName]: value, - }; - console.log("📝 ScreenModal 업데이트된 formData:", newFormData); - return newFormData; - }); - }} - screenInfo={{ - id: modalState.screenId!, - tableName: screenData.screenInfo?.tableName, - }} - /> - ))} + {screenData.components.map((component) => { + // 컴포넌트 위치를 offset만큼 조정 (왼쪽 상단으로 정렬) + const offsetX = screenDimensions?.offsetX || 0; + const offsetY = screenDimensions?.offsetY || 0; + + const adjustedComponent = { + ...component, + position: { + ...component.position, + x: parseFloat(component.position?.x?.toString() || "0") - offsetX, + y: parseFloat(component.position?.y?.toString() || "0") - offsetY, + }, + }; + + return ( + { + console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`); + console.log("📋 현재 formData:", formData); + setFormData((prev) => { + const newFormData = { + ...prev, + [fieldName]: value, + }; + console.log("📝 ScreenModal 업데이트된 formData:", newFormData); + return newFormData; + }); + }} + screenInfo={{ + id: modalState.screenId!, + tableName: screenData.screenInfo?.tableName, + }} + /> + ); + })}
) : (
diff --git a/frontend/components/dataflow/node-editor/FlowToolbar.tsx b/frontend/components/dataflow/node-editor/FlowToolbar.tsx index 5d0064ea..c086e922 100644 --- a/frontend/components/dataflow/node-editor/FlowToolbar.tsx +++ b/frontend/components/dataflow/node-editor/FlowToolbar.tsx @@ -13,12 +13,14 @@ import { useReactFlow } from "reactflow"; import { SaveConfirmDialog } from "./dialogs/SaveConfirmDialog"; import { validateFlow, summarizeValidations } from "@/lib/utils/flowValidation"; import type { FlowValidation } from "@/lib/utils/flowValidation"; +import { useToast } from "@/hooks/use-toast"; interface FlowToolbarProps { validations?: FlowValidation[]; } export function FlowToolbar({ validations = [] }: FlowToolbarProps) { + const { toast } = useToast(); const { zoomIn, zoomOut, fitView } = useReactFlow(); const { flowName, @@ -56,9 +58,17 @@ export function FlowToolbar({ validations = [] }: FlowToolbarProps) { const performSave = async () => { const result = await saveFlow(); if (result.success) { - alert(`✅ ${result.message}\nFlow ID: ${result.flowId}`); + toast({ + title: "✅ 플로우 저장 완료", + description: `${result.message}\nFlow ID: ${result.flowId}`, + variant: "default", + }); } else { - alert(`❌ 저장 실패\n\n${result.message}`); + toast({ + title: "❌ 저장 실패", + description: result.message, + variant: "destructive", + }); } setShowSaveDialog(false); }; @@ -72,18 +82,30 @@ export function FlowToolbar({ validations = [] }: FlowToolbarProps) { a.download = `${flowName || "flow"}.json`; a.click(); URL.revokeObjectURL(url); - alert("✅ JSON 파일로 내보내기 완료!"); + toast({ + title: "✅ 내보내기 완료", + description: "JSON 파일로 저장되었습니다.", + variant: "default", + }); }; const handleDelete = () => { if (selectedNodes.length === 0) { - alert("삭제할 노드를 선택해주세요."); + toast({ + title: "⚠️ 선택된 노드 없음", + description: "삭제할 노드를 선택해주세요.", + variant: "default", + }); return; } if (confirm(`선택된 ${selectedNodes.length}개 노드를 삭제하시겠습니까?`)) { removeNodes(selectedNodes); - alert(`✅ ${selectedNodes.length}개 노드가 삭제되었습니다.`); + toast({ + title: "✅ 노드 삭제 완료", + description: `${selectedNodes.length}개 노드가 삭제되었습니다.`, + variant: "default", + }); } }; diff --git a/frontend/components/dataflow/node-editor/ValidationNotification.tsx b/frontend/components/dataflow/node-editor/ValidationNotification.tsx index 54a04625..39ff2253 100644 --- a/frontend/components/dataflow/node-editor/ValidationNotification.tsx +++ b/frontend/components/dataflow/node-editor/ValidationNotification.tsx @@ -18,189 +18,178 @@ interface ValidationNotificationProps { onClose?: () => void; } -export const ValidationNotification = memo( - ({ validations, onNodeClick, onClose }: ValidationNotificationProps) => { - const [isExpanded, setIsExpanded] = useState(false); - const summary = summarizeValidations(validations); +export const ValidationNotification = memo(({ validations, onNodeClick, onClose }: ValidationNotificationProps) => { + const [isExpanded, setIsExpanded] = useState(false); + const summary = summarizeValidations(validations); - if (validations.length === 0) { - return null; - } + if (validations.length === 0) { + return null; + } - const getTypeLabel = (type: string): string => { - const labels: Record = { - "parallel-conflict": "병렬 실행 충돌", - "missing-where": "WHERE 조건 누락", - "circular-reference": "순환 참조", - "data-source-mismatch": "데이터 소스 불일치", - "parallel-table-access": "병렬 테이블 접근", - }; - return labels[type] || type; + const getTypeLabel = (type: string): string => { + const labels: Record = { + "disconnected-node": "연결되지 않은 노드", + "parallel-conflict": "병렬 실행 충돌", + "missing-where": "WHERE 조건 누락", + "circular-reference": "순환 참조", + "data-source-mismatch": "데이터 소스 불일치", + "parallel-table-access": "병렬 테이블 접근", }; + return labels[type] || type; + }; - // 타입별로 그룹화 - const groupedValidations = validations.reduce((acc, validation) => { + // 타입별로 그룹화 + const groupedValidations = validations.reduce( + (acc, validation) => { if (!acc[validation.type]) { acc[validation.type] = []; } acc[validation.type].push(validation); return acc; - }, {} as Record); + }, + {} as Record, + ); - return ( -
+ return ( +
+
0 + ? "border-yellow-500" + : "border-blue-500", + )} + > + {/* 헤더 */}
0 - ? "border-yellow-500" - : "border-blue-500" + "flex cursor-pointer items-center justify-between p-3", + summary.hasBlockingIssues ? "bg-red-50" : summary.warningCount > 0 ? "bg-yellow-50" : "bg-blue-50", )} + onClick={() => setIsExpanded(!isExpanded)} > - {/* 헤더 */} -
0 - ? "bg-yellow-50" - : "bg-blue-50" +
+ {summary.hasBlockingIssues ? ( + + ) : summary.warningCount > 0 ? ( + + ) : ( + )} - onClick={() => setIsExpanded(!isExpanded)} - > -
- {summary.hasBlockingIssues ? ( - - ) : summary.warningCount > 0 ? ( - - ) : ( - - )} - - 플로우 검증 - -
- {summary.errorCount > 0 && ( - - {summary.errorCount} - - )} - {summary.warningCount > 0 && ( - - {summary.warningCount} - - )} - {summary.infoCount > 0 && ( - - {summary.infoCount} - - )} -
-
+ 플로우 검증
- {isExpanded ? ( - - ) : ( - + {summary.errorCount > 0 && ( + + {summary.errorCount} + )} - {onClose && ( - + {summary.warningCount > 0 && ( + {summary.warningCount} + )} + {summary.infoCount > 0 && ( + + {summary.infoCount} + )}
- - {/* 확장된 내용 */} - {isExpanded && ( -
-
- {Object.entries(groupedValidations).map(([type, typeValidations]) => { - const firstValidation = typeValidations[0]; - const Icon = - firstValidation.severity === "error" - ? AlertCircle - : firstValidation.severity === "warning" - ? AlertTriangle - : Info; - - return ( -
- {/* 타입 헤더 */} -
- - {getTypeLabel(type)} - - {typeValidations.length}개 - -
- - {/* 검증 항목들 */} -
- {typeValidations.map((validation, index) => ( -
onNodeClick?.(validation.nodeId)} - > -

- {validation.message} -

- {validation.affectedNodes && validation.affectedNodes.length > 1 && ( -
- 영향받는 노드: {validation.affectedNodes.length}개 -
- )} -
- 클릭하여 노드 보기 → -
-
- ))} -
-
- ); - })} -
-
- )} - - {/* 요약 메시지 (닫혀있을 때) */} - {!isExpanded && ( -
-

- {summary.hasBlockingIssues - ? "⛔ 오류를 해결해야 저장할 수 있습니다" - : summary.warningCount > 0 - ? "⚠️ 경고 사항을 확인하세요" - : "ℹ️ 정보를 확인하세요"} -

-
- )} +
+ {isExpanded ? ( + + ) : ( + + )} + {onClose && ( + + )} +
+ + {/* 확장된 내용 */} + {isExpanded && ( +
+
+ {Object.entries(groupedValidations).map(([type, typeValidations]) => { + const firstValidation = typeValidations[0]; + const Icon = + firstValidation.severity === "error" + ? AlertCircle + : firstValidation.severity === "warning" + ? AlertTriangle + : Info; + + return ( +
+ {/* 타입 헤더 */} +
+ + {getTypeLabel(type)} + {typeValidations.length}개 +
+ + {/* 검증 항목들 */} +
+ {typeValidations.map((validation, index) => ( +
onNodeClick?.(validation.nodeId)} + > +

{validation.message}

+ {validation.affectedNodes && validation.affectedNodes.length > 1 && ( +
+ 영향받는 노드: {validation.affectedNodes.length}개 +
+ )} +
+ 클릭하여 노드 보기 → +
+
+ ))} +
+
+ ); + })} +
+
+ )} + + {/* 요약 메시지 (닫혀있을 때) */} + {!isExpanded && ( +
+

+ {summary.hasBlockingIssues + ? "⛔ 오류를 해결해야 저장할 수 있습니다" + : summary.warningCount > 0 + ? "⚠️ 경고 사항을 확인하세요" + : "ℹ️ 정보를 확인하세요"} +

+
+ )}
- ); - } -); +
+ ); +}); ValidationNotification.displayName = "ValidationNotification"; - diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 1a14c2d9..b54df6ad 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -49,6 +49,7 @@ import { toast } from "sonner"; import { FileUpload } from "@/components/screen/widgets/FileUpload"; import { AdvancedSearchFilters } from "./filters/AdvancedSearchFilters"; import { SaveModal } from "./SaveModal"; +import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; // 파일 데이터 타입 정의 (AttachedFileInfo와 호환) interface FileInfo { @@ -97,6 +98,7 @@ export const InteractiveDataTable: React.FC = ({ style = {}, onRefresh, }) => { + const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [searchValues, setSearchValues] = useState>({}); @@ -411,6 +413,29 @@ export const InteractiveDataTable: React.FC = ({ async (page: number = 1, searchParams: Record = {}) => { if (!component.tableName) return; + // 프리뷰 모드에서는 샘플 데이터만 표시 + if (isPreviewMode) { + const sampleData = Array.from({ length: 3 }, (_, i) => { + const sample: Record = { id: i + 1 }; + component.columns.forEach((col) => { + if (col.type === "number") { + sample[col.key] = Math.floor(Math.random() * 1000); + } else if (col.type === "boolean") { + sample[col.key] = i % 2 === 0 ? "Y" : "N"; + } else { + sample[col.key] = `샘플 ${col.label} ${i + 1}`; + } + }); + return sample; + }); + setData(sampleData); + setTotal(3); + setTotalPages(1); + setCurrentPage(1); + setLoading(false); + return; + } + setLoading(true); try { const result = await tableTypeApi.getTableData(component.tableName, { @@ -1792,21 +1817,53 @@ export const InteractiveDataTable: React.FC = ({ {/* CRUD 버튼들 */} {component.enableAdd && ( - )} {component.enableEdit && selectedRows.size === 1 && ( - )} {component.enableDelete && selectedRows.size > 0 && ( - diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index cafd611a..8b02211e 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -45,6 +45,7 @@ import { UnifiedColumnInfo as ColumnInfo } from "@/types"; import { isFileComponent } from "@/lib/utils/componentTypeUtils"; import { buildGridClasses } from "@/lib/constants/columnSpans"; import { cn } from "@/lib/utils"; +import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; interface InteractiveScreenViewerProps { component: ComponentData; @@ -86,6 +87,7 @@ export const InteractiveScreenViewer: React.FC = ( return
; } + const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName, user } = useAuth(); // 현재 로그인한 사용자명과 사용자 정보 가져오기 const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); @@ -211,6 +213,11 @@ export const InteractiveScreenViewer: React.FC = ( // 폼 데이터 업데이트 const updateFormData = (fieldName: string, value: any) => { + // 프리뷰 모드에서는 데이터 업데이트 하지 않음 + if (isPreviewMode) { + return; + } + // console.log(`🔄 updateFormData: ${fieldName} = "${value}" (외부콜백: ${!!onFormDataChange})`); // 항상 로컬 상태도 업데이트 @@ -837,6 +844,12 @@ export const InteractiveScreenViewer: React.FC = ( }); const handleFileChange = async (e: React.ChangeEvent) => { + // 프리뷰 모드에서는 파일 업로드 차단 + if (isPreviewMode) { + e.target.value = ""; // 파일 선택 취소 + return; + } + const files = e.target.files; const fieldName = widget.columnName || widget.id; @@ -1155,6 +1168,11 @@ export const InteractiveScreenViewer: React.FC = ( const config = widget.webTypeConfig as ButtonTypeConfig | undefined; const handleButtonClick = async () => { + // 프리뷰 모드에서는 버튼 동작 차단 + if (isPreviewMode) { + return; + } + const actionType = config?.actionType || "save"; try { @@ -1341,13 +1359,28 @@ export const InteractiveScreenViewer: React.FC = ( allComponents.find(c => c.columnName)?.tableName || "dynamic_form_data"; // 기본값 + // 🆕 자동으로 작성자 정보 추가 + const writerValue = user?.userId || userName || "unknown"; + console.log("👤 현재 사용자 정보:", { + userId: user?.userId, + userName: userName, + writerValue: writerValue, + }); + + const dataWithUserInfo = { + ...mappedData, + writer: writerValue, // 테이블 생성 시 자동 생성되는 컬럼 + created_by: writerValue, + updated_by: writerValue, + }; + const saveData: DynamicFormData = { screenId: screenInfo.id, tableName: tableName, - data: mappedData, + data: dataWithUserInfo, }; - // console.log("🚀 API 저장 요청:", saveData); + console.log("🚀 API 저장 요청:", saveData); const result = await dynamicFormApi.saveFormData(saveData); @@ -1841,12 +1874,12 @@ export const InteractiveScreenViewer: React.FC = ( setPopupScreen(null); setPopupFormData({}); // 팝업 닫을 때 formData도 초기화 }}> - - + + {popupScreen?.title || "상세 정보"} -
+
{popupLoading ? (
화면을 불러오는 중...
diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index bca6ca7a..7ed39353 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -17,6 +17,7 @@ import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/ import { FlowButtonGroup } from "./widgets/FlowButtonGroup"; import { FlowVisibilityConfig } from "@/types/control-management"; import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; +import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; // 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록 import "@/lib/registry/components/ButtonRenderer"; @@ -47,6 +48,7 @@ export const InteractiveScreenViewerDynamic: React.FC { + const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName, user } = useAuth(); const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); @@ -178,16 +180,6 @@ export const InteractiveScreenViewerDynamic: React.FC { - console.log("🔄 버튼에서 테이블 새로고침 요청됨"); // 테이블 컴포넌트는 자체적으로 loadData 호출 }} onClose={() => { @@ -405,7 +396,7 @@ export const InteractiveScreenViewerDynamic: React.FC { // console.log("📝 실제 화면 파일 업로드 완료:", data); @@ -486,50 +478,54 @@ export const InteractiveScreenViewerDynamic: React.FC { // console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)"); - window.dispatchEvent(new CustomEvent('globalFileStateChanged', { - detail: { ...eventDetail, delayed: true } - })); + window.dispatchEvent( + new CustomEvent("globalFileStateChanged", { + detail: { ...eventDetail, delayed: true }, + }), + ); }, 100); - + setTimeout(() => { // console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)"); - window.dispatchEvent(new CustomEvent('globalFileStateChanged', { - detail: { ...eventDetail, delayed: true, attempt: 2 } - })); + window.dispatchEvent( + new CustomEvent("globalFileStateChanged", { + detail: { ...eventDetail, delayed: true, attempt: 2 }, + }), + ); }, 500); } }} diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 31f493e7..df14084b 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -38,6 +38,9 @@ interface RealtimePreviewProps { // 버튼 액션을 위한 props screenId?: number; tableName?: string; + userId?: string; // 🆕 현재 사용자 ID + userName?: string; // 🆕 현재 사용자 이름 + companyCode?: string; // 🆕 현재 사용자의 회사 코드 selectedRowsData?: any[]; onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; flowSelectedData?: any[]; @@ -96,6 +99,9 @@ export const RealtimePreviewDynamic: React.FC = ({ onConfigChange, screenId, tableName, + userId, // 🆕 사용자 ID + userName, // 🆕 사용자 이름 + companyCode, // 🆕 회사 코드 selectedRowsData, onSelectedRowsChange, flowSelectedData, @@ -291,6 +297,9 @@ export const RealtimePreviewDynamic: React.FC = ({ onConfigChange={onConfigChange} screenId={screenId} tableName={tableName} + userId={userId} + userName={userName} + companyCode={companyCode} selectedRowsData={selectedRowsData} onSelectedRowsChange={onSelectedRowsChange} flowSelectedData={flowSelectedData} diff --git a/frontend/components/screen/SaveModal.tsx b/frontend/components/screen/SaveModal.tsx index ddd55237..271a83d9 100644 --- a/frontend/components/screen/SaveModal.tsx +++ b/frontend/components/screen/SaveModal.tsx @@ -10,6 +10,7 @@ import { InteractiveScreenViewer } from "./InteractiveScreenViewer"; import { screenApi } from "@/lib/api/screen"; import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm"; import { ComponentData } from "@/lib/types/screen"; +import { useAuth } from "@/hooks/useAuth"; interface SaveModalProps { isOpen: boolean; @@ -33,6 +34,7 @@ export const SaveModal: React.FC = ({ initialData, onSaveSuccess, }) => { + const { user, userName } = useAuth(); // 현재 사용자 정보 가져오기 const [formData, setFormData] = useState>(initialData || {}); const [originalData, setOriginalData] = useState>(initialData || {}); const [screenData, setScreenData] = useState(null); @@ -88,13 +90,13 @@ export const SaveModal: React.FC = ({ onClose(); }; - if (typeof window !== 'undefined') { - window.addEventListener('closeSaveModal', handleCloseSaveModal); + if (typeof window !== "undefined") { + window.addEventListener("closeSaveModal", handleCloseSaveModal); } return () => { - if (typeof window !== 'undefined') { - window.removeEventListener('closeSaveModal', handleCloseSaveModal); + if (typeof window !== "undefined") { + window.removeEventListener("closeSaveModal", handleCloseSaveModal); } }; }, [onClose]); @@ -127,16 +129,28 @@ export const SaveModal: React.FC = ({ // 저장할 데이터 준비 const dataToSave = initialData ? changedData : formData; + // 🆕 자동으로 작성자 정보 추가 + const writerValue = user?.userId || userName || "unknown"; + console.log("👤 현재 사용자 정보:", { + userId: user?.userId, + userName: userName, + writerValue: writerValue, + }); + + const dataWithUserInfo = { + ...dataToSave, + writer: writerValue, // 테이블 생성 시 자동 생성되는 컬럼 + created_by: writerValue, + updated_by: writerValue, + }; + // 테이블명 결정 - const tableName = - screenData.tableName || - components.find((c) => c.columnName)?.tableName || - "dynamic_form_data"; + const tableName = screenData.tableName || components.find((c) => c.columnName)?.tableName || "dynamic_form_data"; const saveData: DynamicFormData = { screenId: screenId, tableName: tableName, - data: dataToSave, + data: dataWithUserInfo, }; console.log("💾 저장 요청 데이터:", saveData); @@ -147,10 +161,10 @@ export const SaveModal: React.FC = ({ if (result.success) { // ✅ 저장 성공 toast.success(initialData ? "수정되었습니다!" : "저장되었습니다!"); - + // 모달 닫기 onClose(); - + // 테이블 새로고침 콜백 호출 if (onSaveSuccess) { setTimeout(() => { @@ -187,19 +201,12 @@ export const SaveModal: React.FC = ({ return ( !isSaving && !open && onClose()}> - - + +
- - {initialData ? "데이터 수정" : "데이터 등록"} - + {initialData ? "데이터 수정" : "데이터 등록"}
- -
@@ -227,7 +229,7 @@ export const SaveModal: React.FC = ({
{loading ? (
- +
) : screenData && components.length > 0 ? (
= ({
) : ( -
- 화면에 컴포넌트가 없습니다. -
+
화면에 컴포넌트가 없습니다.
)}
); }; - diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 7a0c6004..4814933b 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -70,6 +70,7 @@ import { findAllButtonGroups, } from "@/lib/utils/flowButtonGroupUtils"; import { FlowButtonGroupDialog } from "./dialogs/FlowButtonGroupDialog"; +import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; // 새로운 통합 UI 컴포넌트 import { LeftUnifiedToolbar, defaultToolbarButtons } from "./toolbar/LeftUnifiedToolbar"; @@ -86,21 +87,12 @@ interface ScreenDesignerProps { onBackToList: () => void; } -// 패널 설정 (컴포넌트와 편집 2개) +// 패널 설정 (통합 패널 1개) const panelConfigs: PanelConfig[] = [ - // 컴포넌트 패널 (테이블 + 컴포넌트 탭) + // 통합 패널 (컴포넌트 + 편집 탭) { - id: "components", - title: "컴포넌트", - defaultPosition: "left", - defaultWidth: 240, - defaultHeight: 700, - shortcutKey: "c", - }, - // 편집 패널 (속성 + 스타일 & 해상도 탭) - { - id: "properties", - title: "편집", + id: "unified", + title: "패널", defaultPosition: "left", defaultWidth: 240, defaultHeight: 700, @@ -140,14 +132,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const [selectedComponent, setSelectedComponent] = useState(null); - // 컴포넌트 선택 시 속성 패널 자동 열기 + // 컴포넌트 선택 시 통합 패널 자동 열기 const handleComponentSelect = useCallback( (component: ComponentData | null) => { setSelectedComponent(component); - // 컴포넌트가 선택되면 속성 패널 자동 열기 + // 컴포넌트가 선택되면 통합 패널 자동 열기 if (component) { - openPanel("properties"); + openPanel("unified"); } }, [openPanel], @@ -411,6 +403,33 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const targetComponent = prevLayout.components.find((comp) => comp.id === componentId); const isLayoutComponent = targetComponent?.type === "layout"; + // 🆕 그룹 설정 변경 시 같은 그룹의 모든 버튼에 일괄 적용 + const isGroupSetting = path === "webTypeConfig.flowVisibilityConfig.groupAlign"; + + let affectedComponents: string[] = [componentId]; // 기본적으로 현재 컴포넌트만 + + if (isGroupSetting && targetComponent) { + const flowConfig = (targetComponent as any).webTypeConfig?.flowVisibilityConfig; + const currentGroupId = flowConfig?.groupId; + + if (currentGroupId) { + // 같은 그룹의 모든 버튼 찾기 + affectedComponents = prevLayout.components + .filter((comp) => { + const compConfig = (comp as any).webTypeConfig?.flowVisibilityConfig; + return compConfig?.groupId === currentGroupId && compConfig?.enabled; + }) + .map((comp) => comp.id); + + console.log("🔄 그룹 설정 일괄 적용:", { + groupId: currentGroupId, + setting: path.split(".").pop(), + value, + affectedButtons: affectedComponents, + }); + } + } + // 레이아웃 컴포넌트의 위치가 변경되는 경우 존에 속한 컴포넌트들도 함께 이동 const positionDelta = { x: 0, y: 0 }; if (isLayoutComponent && (path === "position.x" || path === "position.y" || path === "position")) { @@ -439,7 +458,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const pathParts = path.split("."); const updatedComponents = prevLayout.components.map((comp) => { - if (comp.id !== componentId) { + // 🆕 그룹 설정이면 같은 그룹의 모든 버튼에 적용 + const shouldUpdate = isGroupSetting ? affectedComponents.includes(comp.id) : comp.id === componentId; + + if (!shouldUpdate) { // 레이아웃 이동 시 존에 속한 컴포넌트들도 함께 이동 if (isLayoutComponent && (positionDelta.x !== 0 || positionDelta.y !== 0)) { // 이 레이아웃의 존에 속한 컴포넌트인지 확인 @@ -3475,10 +3497,45 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 그룹 해제 const ungroupedButtons = ungroupButtons(buttons); - // 레이아웃 업데이트 - const updatedComponents = layout.components.map((comp) => { + // 레이아웃 업데이트 + 플로우 표시 제어 초기화 + const updatedComponents = layout.components.map((comp, index) => { const ungrouped = ungroupedButtons.find((ub) => ub.id === comp.id); - return ungrouped || comp; + + if (ungrouped) { + // 원래 위치 복원 또는 현재 위치 유지 + 간격 추가 + const buttonIndex = buttons.findIndex((b) => b.id === comp.id); + const basePosition = comp.position; + + // 버튼들을 오른쪽으로 조금씩 이동 (겹치지 않도록) + const offsetX = buttonIndex * 120; // 각 버튼당 120px 간격 + + // 그룹 해제된 버튼의 플로우 표시 제어를 끄고 설정 초기화 + return { + ...ungrouped, + position: { + x: basePosition.x + offsetX, + y: basePosition.y, + z: basePosition.z || 1, + }, + webTypeConfig: { + ...ungrouped.webTypeConfig, + flowVisibilityConfig: { + enabled: false, + targetFlowComponentId: null, + mode: "whitelist", + visibleSteps: [], + hiddenSteps: [], + layoutBehavior: "auto-compact", + groupId: null, + groupDirection: "horizontal", + groupGap: 8, + groupAlign: "start", + }, + }, + }; + } + + return comp; }); const newLayout = { @@ -3489,7 +3546,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD setLayout(newLayout); saveToHistory(newLayout); - toast.success(`${buttons.length}개의 버튼 그룹이 해제되었습니다`); + toast.success(`${buttons.length}개의 버튼 그룹이 해제되고 플로우 표시 제어가 비활성화되었습니다`); }, [layout, groupState.selectedComponents, saveToHistory]); // 그룹 생성 (임시 비활성화) @@ -4102,786 +4159,787 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } return ( -
- {/* 상단 슬림 툴바 */} - - {/* 메인 컨테이너 (좌측 툴바 + 패널들 + 캔버스) */} -
- {/* 좌측 통합 툴바 */} - + +
+ {/* 상단 슬림 툴바 */} + + {/* 메인 컨테이너 (좌측 툴바 + 패널들 + 캔버스) */} +
+ {/* 좌측 통합 툴바 */} + - {/* 열린 패널들 (좌측에서 우측으로 누적) */} - {panelStates.components?.isOpen && ( -
-
-

컴포넌트

- -
-
- { - const dragData = { - type: column ? "column" : "table", - table, - column, - }; - e.dataTransfer.setData("application/json", JSON.stringify(dragData)); - }} - selectedTableName={selectedScreen.tableName} - placedColumns={placedColumns} - /> -
-
- )} - - {panelStates.properties?.isOpen && ( -
-
-

속성

- -
-
- 0 ? tables[0] : undefined} - currentTableName={selectedScreen?.tableName} - dragState={dragState} - onStyleChange={(style) => { - if (selectedComponent) { - updateComponentProperty(selectedComponent.id, "style", style); - } - }} - currentResolution={screenResolution} - onResolutionChange={handleResolutionChange} - allComponents={layout.components} // 🆕 플로우 위젯 감지용 - /> -
-
- )} - - {/* 스타일과 해상도 패널은 속성 패널의 탭으로 통합됨 */} - - {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */} -
- {/* Pan 모드 안내 - 제거됨 */} - {/* 줌 레벨 표시 */} -
- 🔍 {Math.round(zoomLevel * 100)}% -
- {/* 🆕 플로우 버튼 그룹 제어 (버튼 선택 시 표시) */} - {(() => { - // 선택된 컴포넌트들 - const selectedComps = layout.components.filter((c) => groupState.selectedComponents.includes(c.id)); - - // 버튼 컴포넌트만 필터링 - const selectedButtons = selectedComps.filter((comp) => areAllButtons([comp])); - - // 플로우 그룹에 속한 버튼이 있는지 확인 - const hasFlowGroupButton = selectedButtons.some((btn) => { - const flowConfig = (btn as any).webTypeConfig?.flowVisibilityConfig; - return flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId; - }); - - // 버튼이 선택되었거나 플로우 그룹 버튼이 있으면 표시 - const shouldShow = selectedButtons.length >= 1 && (selectedButtons.length >= 2 || hasFlowGroupButton); - - if (!shouldShow) return null; - - return ( -
-
-
- - - - - - {selectedButtons.length}개 버튼 선택됨 -
- - {/* 그룹 생성 버튼 (2개 이상 선택 시) */} - {selectedButtons.length >= 2 && ( - - )} - - {/* 그룹 해제 버튼 (플로우 그룹 버튼이 있으면 항상 표시) */} - {hasFlowGroupButton && ( - - )} - - {/* 상태 표시 */} - {hasFlowGroupButton &&

✓ 플로우 그룹 버튼

} -
+ {/* 통합 패널 */} + {panelStates.unified?.isOpen && ( +
+
+

패널

+
- ); - })()} - {/* 🔥 줌 적용 시 스크롤 영역 확보를 위한 래퍼 */} -
- {/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */} +
+ + + + 컴포넌트 + + + 편집 + + + + + { + const dragData = { + type: column ? "column" : "table", + table, + column, + }; + e.dataTransfer.setData("application/json", JSON.stringify(dragData)); + }} + selectedTableName={selectedScreen.tableName} + placedColumns={placedColumns} + /> + + + + 0 ? tables[0] : undefined} + currentTableName={selectedScreen?.tableName} + dragState={dragState} + onStyleChange={(style) => { + if (selectedComponent) { + updateComponentProperty(selectedComponent.id, "style", style); + } + }} + currentResolution={screenResolution} + onResolutionChange={handleResolutionChange} + allComponents={layout.components} // 🆕 플로우 위젯 감지용 + /> + + +
+
+ )} + + {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */} +
+ {/* Pan 모드 안내 - 제거됨 */} + {/* 줌 레벨 표시 */} +
+ 🔍 {Math.round(zoomLevel * 100)}% +
+ {/* 🆕 플로우 버튼 그룹 제어 (버튼 선택 시 표시) */} + {(() => { + // 선택된 컴포넌트들 + const selectedComps = layout.components.filter((c) => groupState.selectedComponents.includes(c.id)); + + // 버튼 컴포넌트만 필터링 + const selectedButtons = selectedComps.filter((comp) => areAllButtons([comp])); + + // 플로우 그룹에 속한 버튼이 있는지 확인 + const hasFlowGroupButton = selectedButtons.some((btn) => { + const flowConfig = (btn as any).webTypeConfig?.flowVisibilityConfig; + return flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId; + }); + + // 버튼이 선택되었거나 플로우 그룹 버튼이 있으면 표시 + const shouldShow = selectedButtons.length >= 1 && (selectedButtons.length >= 2 || hasFlowGroupButton); + + if (!shouldShow) return null; + + return ( +
+
+
+ + + + + + {selectedButtons.length}개 버튼 선택됨 +
+ + {/* 그룹 생성 버튼 (2개 이상 선택 시) */} + {selectedButtons.length >= 2 && ( + + )} + + {/* 그룹 해제 버튼 (플로우 그룹 버튼이 있으면 항상 표시) */} + {hasFlowGroupButton && ( + + )} + + {/* 상태 표시 */} + {hasFlowGroupButton &&

✓ 플로우 그룹 버튼

} +
+
+ ); + })()} + {/* 🔥 줌 적용 시 스크롤 영역 확보를 위한 래퍼 */}
+ {/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */}
{ - if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) { - setSelectedComponent(null); - setGroupState((prev) => ({ ...prev, selectedComponents: [] })); - } - }} - onMouseDown={(e) => { - // Pan 모드가 아닐 때만 다중 선택 시작 - if (e.target === e.currentTarget && !isPanMode) { - startSelectionDrag(e); - } - }} - onDragOver={(e) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "copy"; - }} - onDrop={(e) => { - e.preventDefault(); - // console.log("🎯 캔버스 드롭 이벤트 발생"); - handleDrop(e); + className="bg-background border-border border shadow-lg" + style={{ + width: `${screenResolution.width}px`, + height: `${screenResolution.height}px`, + minWidth: `${screenResolution.width}px`, + maxWidth: `${screenResolution.width}px`, + minHeight: `${screenResolution.height}px`, + flexShrink: 0, + transform: `scale(${zoomLevel})`, + transformOrigin: "top center", }} > - {/* 격자 라인 */} - {gridLines.map((line, index) => ( -
- ))} - - {/* 컴포넌트들 */} - {(() => { - // 🆕 플로우 버튼 그룹 감지 및 처리 - const topLevelComponents = layout.components.filter((component) => !component.parentId); - - // auto-compact 모드의 버튼들을 그룹별로 묶기 - const buttonGroups: Record = {}; - const processedButtonIds = new Set(); - - topLevelComponents.forEach((component) => { - const isButton = - component.type === "button" || - (component.type === "component" && - ["button-primary", "button-secondary"].includes((component as any).componentType)); - - if (isButton) { - const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as - | FlowVisibilityConfig - | undefined; - - if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { - if (!buttonGroups[flowConfig.groupId]) { - buttonGroups[flowConfig.groupId] = []; - } - buttonGroups[flowConfig.groupId].push(component); - processedButtonIds.add(component.id); - } +
{ + if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) { + setSelectedComponent(null); + setGroupState((prev) => ({ ...prev, selectedComponents: [] })); } - }); + }} + onMouseDown={(e) => { + // Pan 모드가 아닐 때만 다중 선택 시작 + if (e.target === e.currentTarget && !isPanMode) { + startSelectionDrag(e); + } + }} + onDragOver={(e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + }} + onDrop={(e) => { + e.preventDefault(); + // console.log("🎯 캔버스 드롭 이벤트 발생"); + handleDrop(e); + }} + > + {/* 격자 라인 */} + {gridLines.map((line, index) => ( +
+ ))} - // 그룹에 속하지 않은 일반 컴포넌트들 - const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + {/* 컴포넌트들 */} + {(() => { + // 🆕 플로우 버튼 그룹 감지 및 처리 + const topLevelComponents = layout.components.filter((component) => !component.parentId); - return ( - <> - {/* 일반 컴포넌트들 */} - {regularComponents.map((component) => { - const children = - component.type === "group" - ? layout.components.filter((child) => child.parentId === component.id) - : []; + // auto-compact 모드의 버튼들을 그룹별로 묶기 + const buttonGroups: Record = {}; + const processedButtonIds = new Set(); - // 드래그 중 시각적 피드백 (다중 선택 지원) - const isDraggingThis = dragState.isDragging && dragState.draggedComponent?.id === component.id; - const isBeingDragged = - dragState.isDragging && - dragState.draggedComponents.some((dragComp) => dragComp.id === component.id); + topLevelComponents.forEach((component) => { + const isButton = + component.type === "button" || + (component.type === "component" && + ["button-primary", "button-secondary"].includes((component as any).componentType)); - let displayComponent = component; + if (isButton) { + const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as + | FlowVisibilityConfig + | undefined; - if (isBeingDragged) { - if (isDraggingThis) { - // 주 드래그 컴포넌트: 마우스 위치 기반으로 실시간 위치 업데이트 - displayComponent = { - ...component, - position: dragState.currentPosition, - style: { - ...component.style, - opacity: 0.8, - transform: "scale(1.02)", - transition: "none", - zIndex: 50, - }, - }; - } else { - // 다른 선택된 컴포넌트들: 상대적 위치로 실시간 업데이트 - const originalComponent = dragState.draggedComponents.find( - (dragComp) => dragComp.id === component.id, - ); - if (originalComponent) { - const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; - const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { + if (!buttonGroups[flowConfig.groupId]) { + buttonGroups[flowConfig.groupId] = []; + } + buttonGroups[flowConfig.groupId].push(component); + processedButtonIds.add(component.id); + } + } + }); + // 그룹에 속하지 않은 일반 컴포넌트들 + const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + + return ( + <> + {/* 일반 컴포넌트들 */} + {regularComponents.map((component) => { + const children = + component.type === "group" + ? layout.components.filter((child) => child.parentId === component.id) + : []; + + // 드래그 중 시각적 피드백 (다중 선택 지원) + const isDraggingThis = + dragState.isDragging && dragState.draggedComponent?.id === component.id; + const isBeingDragged = + dragState.isDragging && + dragState.draggedComponents.some((dragComp) => dragComp.id === component.id); + + let displayComponent = component; + + if (isBeingDragged) { + if (isDraggingThis) { + // 주 드래그 컴포넌트: 마우스 위치 기반으로 실시간 위치 업데이트 displayComponent = { ...component, - position: { - x: originalComponent.position.x + deltaX, - y: originalComponent.position.y + deltaY, - z: originalComponent.position.z || 1, - } as Position, + position: dragState.currentPosition, style: { ...component.style, opacity: 0.8, + transform: "scale(1.02)", transition: "none", - zIndex: 40, // 주 컴포넌트보다 약간 낮게 + zIndex: 50, }, }; + } else { + // 다른 선택된 컴포넌트들: 상대적 위치로 실시간 업데이트 + const originalComponent = dragState.draggedComponents.find( + (dragComp) => dragComp.id === component.id, + ); + if (originalComponent) { + const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; + const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + + displayComponent = { + ...component, + position: { + x: originalComponent.position.x + deltaX, + y: originalComponent.position.y + deltaY, + z: originalComponent.position.z || 1, + } as Position, + style: { + ...component.style, + opacity: 0.8, + transition: "none", + zIndex: 40, // 주 컴포넌트보다 약간 낮게 + }, + }; + } } } - } - // 전역 파일 상태도 key에 포함하여 실시간 리렌더링 - const globalFileState = - typeof window !== "undefined" ? (window as any).globalFileState || {} : {}; - const globalFiles = globalFileState[component.id] || []; - const componentFiles = (component as any).uploadedFiles || []; - const fileStateKey = `${globalFiles.length}-${JSON.stringify(globalFiles.map((f: any) => f.objid) || [])}-${componentFiles.length}`; + // 전역 파일 상태도 key에 포함하여 실시간 리렌더링 + const globalFileState = + typeof window !== "undefined" ? (window as any).globalFileState || {} : {}; + const globalFiles = globalFileState[component.id] || []; + const componentFiles = (component as any).uploadedFiles || []; + const fileStateKey = `${globalFiles.length}-${JSON.stringify(globalFiles.map((f: any) => f.objid) || [])}-${componentFiles.length}`; - return ( - handleComponentClick(component, e)} - onDoubleClick={(e) => handleComponentDoubleClick(component, e)} - onDragStart={(e) => startComponentDrag(component, e)} - onDragEnd={endDrag} - selectedScreen={selectedScreen} - // onZoneComponentDrop 제거 - onZoneClick={handleZoneClick} - // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) - onConfigChange={(config) => { - // console.log("📤 테이블 설정 변경을 상세설정에 반영:", config); + return ( + handleComponentClick(component, e)} + onDoubleClick={(e) => handleComponentDoubleClick(component, e)} + onDragStart={(e) => startComponentDrag(component, e)} + onDragEnd={endDrag} + selectedScreen={selectedScreen} + // onZoneComponentDrop 제거 + onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) + onConfigChange={(config) => { + // console.log("📤 테이블 설정 변경을 상세설정에 반영:", config); - // 컴포넌트의 componentConfig 업데이트 - const updatedComponents = layout.components.map((comp) => { - if (comp.id === component.id) { - return { - ...comp, - componentConfig: { - ...comp.componentConfig, - ...config, - }, - }; - } - return comp; - }); + // 컴포넌트의 componentConfig 업데이트 + const updatedComponents = layout.components.map((comp) => { + if (comp.id === component.id) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + ...config, + }, + }; + } + return comp; + }); - const newLayout = { - ...layout, - components: updatedComponents, - }; + const newLayout = { + ...layout, + components: updatedComponents, + }; - setLayout(newLayout); - saveToHistory(newLayout); + setLayout(newLayout); + saveToHistory(newLayout); - console.log("✅ 컴포넌트 설정 업데이트 완료:", { - componentId: component.id, - updatedConfig: config, - }); - }} - > - {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} - {(component.type === "group" || - component.type === "container" || - component.type === "area") && - layout.components - .filter((child) => child.parentId === component.id) - .map((child) => { - // 자식 컴포넌트에도 드래그 피드백 적용 - const isChildDraggingThis = - dragState.isDragging && dragState.draggedComponent?.id === child.id; - const isChildBeingDragged = + console.log("✅ 컴포넌트 설정 업데이트 완료:", { + componentId: component.id, + updatedConfig: config, + }); + }} + > + {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} + {(component.type === "group" || + component.type === "container" || + component.type === "area") && + layout.components + .filter((child) => child.parentId === component.id) + .map((child) => { + // 자식 컴포넌트에도 드래그 피드백 적용 + const isChildDraggingThis = + dragState.isDragging && dragState.draggedComponent?.id === child.id; + const isChildBeingDragged = + dragState.isDragging && + dragState.draggedComponents.some((dragComp) => dragComp.id === child.id); + + let displayChild = child; + + if (isChildBeingDragged) { + if (isChildDraggingThis) { + // 주 드래그 자식 컴포넌트 + displayChild = { + ...child, + position: dragState.currentPosition, + style: { + ...child.style, + opacity: 0.8, + transform: "scale(1.02)", + transition: "none", + zIndex: 50, + }, + }; + } else { + // 다른 선택된 자식 컴포넌트들 + const originalChildComponent = dragState.draggedComponents.find( + (dragComp) => dragComp.id === child.id, + ); + if (originalChildComponent) { + const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; + const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + + displayChild = { + ...child, + position: { + x: originalChildComponent.position.x + deltaX, + y: originalChildComponent.position.y + deltaY, + z: originalChildComponent.position.z || 1, + } as Position, + style: { + ...child.style, + opacity: 0.8, + transition: "none", + zIndex: 8888, + }, + }; + } + } + } + + // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 + const relativeChildComponent = { + ...displayChild, + position: { + x: displayChild.position.x - component.position.x, + y: displayChild.position.y - component.position.y, + z: displayChild.position.z || 1, + }, + }; + + return ( + f.objid) || [])}`} + component={relativeChildComponent} + isSelected={ + selectedComponent?.id === child.id || + groupState.selectedComponents.includes(child.id) + } + isDesignMode={true} // 편집 모드로 설정 + onClick={(e) => handleComponentClick(child, e)} + onDoubleClick={(e) => handleComponentDoubleClick(child, e)} + onDragStart={(e) => startComponentDrag(child, e)} + onDragEnd={endDrag} + selectedScreen={selectedScreen} + // onZoneComponentDrop 제거 + onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (자식 컴포넌트용) + onConfigChange={(config) => { + // console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} + /> + ); + })} + + ); + })} + + {/* 🆕 플로우 버튼 그룹들 */} + {Object.entries(buttonGroups).map(([groupId, buttons]) => { + if (buttons.length === 0) return null; + + const firstButton = buttons[0]; + const groupConfig = (firstButton as any).webTypeConfig + ?.flowVisibilityConfig as FlowVisibilityConfig; + + // 🔧 그룹의 위치 및 크기 계산 + // 모든 버튼이 같은 위치(groupX, groupY)에 배치되어 있으므로 + // 첫 번째 버튼의 위치를 그룹 시작점으로 사용 + const direction = groupConfig.groupDirection || "horizontal"; + const gap = groupConfig.groupGap ?? 8; + const align = groupConfig.groupAlign || "start"; + + const groupPosition = { + x: buttons[0].position.x, + y: buttons[0].position.y, + z: buttons[0].position.z || 2, + }; + + // 버튼들의 실제 크기 계산 + let groupWidth = 0; + let groupHeight = 0; + + if (direction === "horizontal") { + // 가로 정렬: 모든 버튼의 너비 + 간격 + groupWidth = buttons.reduce((total, button, index) => { + const buttonWidth = button.size?.width || 100; + const gapWidth = index < buttons.length - 1 ? gap : 0; + return total + buttonWidth + gapWidth; + }, 0); + groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40)); + } else { + // 세로 정렬 + groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100)); + groupHeight = buttons.reduce((total, button, index) => { + const buttonHeight = button.size?.height || 40; + const gapHeight = index < buttons.length - 1 ? gap : 0; + return total + buttonHeight + gapHeight; + }, 0); + } + + // 🆕 그룹 전체가 선택되었는지 확인 + const isGroupSelected = buttons.every( + (btn) => selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id), + ); + const hasAnySelected = buttons.some( + (btn) => selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id), + ); + + return ( +
+ { + // 드래그 피드백 + const isDraggingThis = + dragState.isDragging && dragState.draggedComponent?.id === button.id; + const isBeingDragged = dragState.isDragging && - dragState.draggedComponents.some((dragComp) => dragComp.id === child.id); + dragState.draggedComponents.some((dragComp) => dragComp.id === button.id); - let displayChild = child; + let displayButton = button; - if (isChildBeingDragged) { - if (isChildDraggingThis) { - // 주 드래그 자식 컴포넌트 - displayChild = { - ...child, + if (isBeingDragged) { + if (isDraggingThis) { + displayButton = { + ...button, position: dragState.currentPosition, style: { - ...child.style, + ...button.style, opacity: 0.8, transform: "scale(1.02)", transition: "none", zIndex: 50, }, }; - } else { - // 다른 선택된 자식 컴포넌트들 - const originalChildComponent = dragState.draggedComponents.find( - (dragComp) => dragComp.id === child.id, - ); - if (originalChildComponent) { - const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; - const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; - - displayChild = { - ...child, - position: { - x: originalChildComponent.position.x + deltaX, - y: originalChildComponent.position.y + deltaY, - z: originalChildComponent.position.z || 1, - } as Position, - style: { - ...child.style, - opacity: 0.8, - transition: "none", - zIndex: 8888, - }, - }; - } } } - // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 - const relativeChildComponent = { - ...displayChild, + // 🔧 그룹 내부에서는 상대 위치 사용 (wrapper로 처리) + const relativeButton = { + ...displayButton, position: { - x: displayChild.position.x - component.position.x, - y: displayChild.position.y - component.position.y, - z: displayChild.position.z || 1, + x: 0, + y: 0, + z: displayButton.position.z || 1, }, }; return ( - f.objid) || [])}`} - component={relativeChildComponent} - isSelected={ - selectedComponent?.id === child.id || - groupState.selectedComponents.includes(child.id) - } - isDesignMode={true} // 편집 모드로 설정 - onClick={(e) => handleComponentClick(child, e)} - onDoubleClick={(e) => handleComponentDoubleClick(child, e)} - onDragStart={(e) => startComponentDrag(child, e)} - onDragEnd={endDrag} - selectedScreen={selectedScreen} - // onZoneComponentDrop 제거 - onZoneClick={handleZoneClick} - // 설정 변경 핸들러 (자식 컴포넌트용) - onConfigChange={(config) => { - // console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); - // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 +
- ); - })} - - ); - })} + onMouseDown={(e) => { + // 클릭이 아닌 드래그인 경우에만 드래그 시작 + e.preventDefault(); + e.stopPropagation(); - {/* 🆕 플로우 버튼 그룹들 */} - {Object.entries(buttonGroups).map(([groupId, buttons]) => { - if (buttons.length === 0) return null; + const startX = e.clientX; + const startY = e.clientY; + let isDragging = false; + let dragStarted = false; - const firstButton = buttons[0]; - const groupConfig = (firstButton as any).webTypeConfig - ?.flowVisibilityConfig as FlowVisibilityConfig; + const handleMouseMove = (moveEvent: MouseEvent) => { + const deltaX = Math.abs(moveEvent.clientX - startX); + const deltaY = Math.abs(moveEvent.clientY - startY); - // 🔧 그룹의 위치 및 크기 계산 - // 모든 버튼이 같은 위치(groupX, groupY)에 배치되어 있으므로 - // 첫 번째 버튼의 위치를 그룹 시작점으로 사용 - const direction = groupConfig.groupDirection || "horizontal"; - const gap = groupConfig.groupGap ?? 8; - const align = groupConfig.groupAlign || "start"; + // 5픽셀 이상 움직이면 드래그로 간주 + if ((deltaX > 5 || deltaY > 5) && !dragStarted) { + isDragging = true; + dragStarted = true; - const groupPosition = { - x: buttons[0].position.x, - y: buttons[0].position.y, - z: buttons[0].position.z || 2, - }; + // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택 + if (!e.shiftKey) { + const buttonIds = buttons.map((b) => b.id); + setGroupState((prev) => ({ + ...prev, + selectedComponents: buttonIds, + })); + } - // 버튼들의 실제 크기 계산 - let groupWidth = 0; - let groupHeight = 0; - - if (direction === "horizontal") { - // 가로 정렬: 모든 버튼의 너비 + 간격 - groupWidth = buttons.reduce((total, button, index) => { - const buttonWidth = button.size?.width || 100; - const gapWidth = index < buttons.length - 1 ? gap : 0; - return total + buttonWidth + gapWidth; - }, 0); - groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40)); - } else { - // 세로 정렬 - groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100)); - groupHeight = buttons.reduce((total, button, index) => { - const buttonHeight = button.size?.height || 40; - const gapHeight = index < buttons.length - 1 ? gap : 0; - return total + buttonHeight + gapHeight; - }, 0); - } - - // 🆕 그룹 전체가 선택되었는지 확인 - const isGroupSelected = buttons.every( - (btn) => selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id), - ); - const hasAnySelected = buttons.some( - (btn) => selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id), - ); - - return ( -
- { - // 드래그 피드백 - const isDraggingThis = - dragState.isDragging && dragState.draggedComponent?.id === button.id; - const isBeingDragged = - dragState.isDragging && - dragState.draggedComponents.some((dragComp) => dragComp.id === button.id); - - let displayButton = button; - - if (isBeingDragged) { - if (isDraggingThis) { - displayButton = { - ...button, - position: dragState.currentPosition, - style: { - ...button.style, - opacity: 0.8, - transform: "scale(1.02)", - transition: "none", - zIndex: 50, - }, - }; - } - } - - // 🔧 그룹 내부에서는 상대 위치 사용 (wrapper로 처리) - const relativeButton = { - ...displayButton, - position: { - x: 0, - y: 0, - z: displayButton.position.z || 1, - }, - }; - - return ( -
{ - // 클릭이 아닌 드래그인 경우에만 드래그 시작 - e.preventDefault(); - e.stopPropagation(); - - const startX = e.clientX; - const startY = e.clientY; - let isDragging = false; - let dragStarted = false; - - const handleMouseMove = (moveEvent: MouseEvent) => { - const deltaX = Math.abs(moveEvent.clientX - startX); - const deltaY = Math.abs(moveEvent.clientY - startY); - - // 5픽셀 이상 움직이면 드래그로 간주 - if ((deltaX > 5 || deltaY > 5) && !dragStarted) { - isDragging = true; - dragStarted = true; - - // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택 - if (!e.shiftKey) { - const buttonIds = buttons.map((b) => b.id); - setGroupState((prev) => ({ - ...prev, - selectedComponents: buttonIds, - })); + // 드래그 시작 + startComponentDrag(button, e as any); } + }; - // 드래그 시작 - startComponentDrag(button, e as any); - } - }; + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - - // 드래그가 아니면 클릭으로 처리 - if (!isDragging) { - // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택 - if (!e.shiftKey) { - const buttonIds = buttons.map((b) => b.id); - setGroupState((prev) => ({ - ...prev, - selectedComponents: buttonIds, - })); + // 드래그가 아니면 클릭으로 처리 + if (!isDragging) { + // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택 + if (!e.shiftKey) { + const buttonIds = buttons.map((b) => b.id); + setGroupState((prev) => ({ + ...prev, + selectedComponents: buttonIds, + })); + } + handleComponentClick(button, e); } - handleComponentClick(button, e); - } - }; + }; - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - }} - onDoubleClick={(e) => { - e.stopPropagation(); - handleComponentDoubleClick(button, e); - }} - className={ - selectedComponent?.id === button.id || - groupState.selectedComponents.includes(button.id) - ? "outline-1 outline-offset-1 outline-blue-400" - : "" - } - > - {/* 그룹 내부에서는 DynamicComponentRenderer로 직접 렌더링 */} -
- {}} - /> + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }} + onDoubleClick={(e) => { + e.stopPropagation(); + handleComponentDoubleClick(button, e); + }} + className={ + selectedComponent?.id === button.id || + groupState.selectedComponents.includes(button.id) + ? "outline-1 outline-offset-1 outline-blue-400" + : "" + } + > + {/* 그룹 내부에서는 DynamicComponentRenderer로 직접 렌더링 */} +
+ {}} + /> +
-
- ); - }} - /> -
- ); - })} - - ); - })()} + ); + }} + /> +
+ ); + })} + + ); + })()} - {/* 드래그 선택 영역 */} - {selectionDrag.isSelecting && ( -
- )} + {/* 드래그 선택 영역 */} + {selectionDrag.isSelecting && ( +
+ )} - {/* 빈 캔버스 안내 */} - {layout.components.length === 0 && ( -
-
-
- -
-

캔버스가 비어있습니다

-

- 좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요 -

-
-

- 단축키: T(테이블), M(템플릿), P(속성), S(스타일), - R(격자), D(상세설정), E(해상도) -

-

- 편집: Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장), - Ctrl+Z(실행취소), Delete(삭제) -

-

- ⚠️ - 브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다 + {/* 빈 캔버스 안내 */} + {layout.components.length === 0 && ( +

+
+
+ +
+

캔버스가 비어있습니다

+

+ 좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요

+
+

+ 단축키: T(테이블), M(템플릿), P(속성), S(스타일), + R(격자), D(상세설정), E(해상도) +

+

+ 편집: Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장), + Ctrl+Z(실행취소), Delete(삭제) +

+

+ ⚠️ + 브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다 +

+
-
- )} + )} +
-
-
{" "} - {/* 🔥 줌 래퍼 닫기 */} -
-
{" "} - {/* 메인 컨테이너 닫기 */} - {/* 🆕 플로우 버튼 그룹 생성 다이얼로그 */} - - {/* 모달들 */} - {/* 메뉴 할당 모달 */} - {showMenuAssignmentModal && selectedScreen && ( - setShowMenuAssignmentModal(false)} - onAssignmentComplete={() => { - // 모달을 즉시 닫지 않고, MenuAssignmentModal이 3초 후 자동으로 닫히도록 함 - // setShowMenuAssignmentModal(false); - // toast.success("메뉴에 화면이 할당되었습니다."); - }} - onBackToList={onBackToList} +
{" "} + {/* 🔥 줌 래퍼 닫기 */} +
+
{" "} + {/* 메인 컨테이너 닫기 */} + {/* 🆕 플로우 버튼 그룹 생성 다이얼로그 */} + - )} - {/* 파일첨부 상세 모달 */} - {showFileAttachmentModal && selectedFileComponent && ( - { - setShowFileAttachmentModal(false); - setSelectedFileComponent(null); - }} - component={selectedFileComponent} - screenId={selectedScreen.screenId} - /> - )} -
+ {/* 모달들 */} + {/* 메뉴 할당 모달 */} + {showMenuAssignmentModal && selectedScreen && ( + setShowMenuAssignmentModal(false)} + onAssignmentComplete={() => { + // 모달을 즉시 닫지 않고, MenuAssignmentModal이 3초 후 자동으로 닫히도록 함 + // setShowMenuAssignmentModal(false); + // toast.success("메뉴에 화면이 할당되었습니다."); + }} + onBackToList={onBackToList} + /> + )} + {/* 파일첨부 상세 모달 */} + {showFileAttachmentModal && selectedFileComponent && ( + { + setShowFileAttachmentModal(false); + setSelectedFileComponent(null); + }} + component={selectedFileComponent} + screenId={selectedScreen.screenId} + /> + )} +
+ ); } diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index 382ae20c..75a1662c 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -448,10 +448,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr {screens.map((screen) => ( handleScreenSelect(screen)} + onClick={() => onDesignScreen(screen)} >
diff --git a/frontend/components/screen/StyleEditor.tsx b/frontend/components/screen/StyleEditor.tsx index ecd405d0..95523901 100644 --- a/frontend/components/screen/StyleEditor.tsx +++ b/frontend/components/screen/StyleEditor.tsx @@ -28,70 +28,17 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd }; return ( -
- {/* 여백 섹션 */} -
-
- -

여백

-
- -
-
-
- - handleStyleChange("margin", e.target.value)} - className="h-8" - /> -
-
- - handleStyleChange("padding", e.target.value)} - className="h-8" - /> -
-
- -
- - handleStyleChange("gap", e.target.value)} - className="h-8" - /> -
-
-
- +
{/* 테두리 섹션 */} -
+
- +

테두리

- -
-
-
+ +
+
+
@@ -101,10 +48,11 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="1px" value={localStyle.borderWidth || ""} onChange={(e) => handleStyleChange("borderWidth", e.target.value)} - className="h-8" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} />
-
+
@@ -112,42 +60,52 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.borderStyle || "solid"} onValueChange={(value) => handleStyleChange("borderStyle", value)} > - + - 실선 - 파선 - 점선 - 없음 + + 실선 + + + 파선 + + + 점선 + + + 없음 +
-
-
+
+
-
+
handleStyleChange("borderColor", e.target.value)} - className="h-8 w-14 p-1" + className="h-6 w-12 p-1" + style={{ fontSize: "12px" }} /> handleStyleChange("borderColor", e.target.value)} placeholder="#000000" - className="h-8 flex-1" + className="h-6 flex-1 text-xs" + style={{ fontSize: "12px" }} />
-
+
@@ -157,7 +115,8 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="5px" value={localStyle.borderRadius || ""} onChange={(e) => handleStyleChange("borderRadius", e.target.value)} - className="h-8" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} />
@@ -165,38 +124,40 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
{/* 배경 섹션 */} -
+
- +

배경

- -
-
+ +
+
-
+
handleStyleChange("backgroundColor", e.target.value)} - className="h-8 w-14 p-1" + className="h-6 w-12 p-1" + style={{ fontSize: "12px" }} /> handleStyleChange("backgroundColor", e.target.value)} placeholder="#ffffff" - className="h-8 flex-1" + className="h-6 flex-1 text-xs" + style={{ fontSize: "12px" }} />
-
+
handleStyleChange("backgroundImage", e.target.value)} - className="h-8" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} />
{/* 텍스트 섹션 */} -
+
- +

텍스트

- -
-
-
+ +
+
+
-
+
handleStyleChange("color", e.target.value)} - className="h-8 w-14 p-1" + className="h-6 w-12 p-1" + style={{ fontSize: "12px" }} /> handleStyleChange("color", e.target.value)} placeholder="#000000" - className="h-8 flex-1" + className="h-6 flex-1 text-xs" + style={{ fontSize: "12px" }} />
-
+
@@ -250,50 +214,73 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="14px" value={localStyle.fontSize || ""} onChange={(e) => handleStyleChange("fontSize", e.target.value)} - className="h-8" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} />
-
-
- {/* 🆕 플로우 단계별 표시 제어 섹션 */} -
- -
+ {/* 🆕 플로우 단계별 표시 제어 섹션 (플로우 위젯이 있을 때만 표시) */} + {hasFlowWidget && ( +
+ +
+ )}
); }; diff --git a/frontend/components/screen/config-panels/CheckboxConfigPanel.tsx b/frontend/components/screen/config-panels/CheckboxConfigPanel.tsx index 3a3db764..85a78b7a 100644 --- a/frontend/components/screen/config-panels/CheckboxConfigPanel.tsx +++ b/frontend/components/screen/config-panels/CheckboxConfigPanel.tsx @@ -117,7 +117,7 @@ export const CheckboxConfigPanel: React.FC = ({ return ( - + 체크박스 설정 @@ -173,7 +173,7 @@ export const CheckboxConfigPanel: React.FC = ({ value={localConfig.label || ""} onChange={(e) => updateConfig("label", e.target.value)} placeholder="체크박스 라벨" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -187,7 +187,7 @@ export const CheckboxConfigPanel: React.FC = ({ value={localConfig.checkedValue || ""} onChange={(e) => updateConfig("checkedValue", e.target.value)} placeholder="Y" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -199,7 +199,7 @@ export const CheckboxConfigPanel: React.FC = ({ value={localConfig.uncheckedValue || ""} onChange={(e) => updateConfig("uncheckedValue", e.target.value)} placeholder="N" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -232,7 +232,7 @@ export const CheckboxConfigPanel: React.FC = ({ value={localConfig.groupLabel || ""} onChange={(e) => updateConfig("groupLabel", e.target.value)} placeholder="체크박스 그룹 제목" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -244,19 +244,19 @@ export const CheckboxConfigPanel: React.FC = ({ value={newOptionLabel} onChange={(e) => setNewOptionLabel(e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> setNewOptionValue(e.target.value)} placeholder="값" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> @@ -277,13 +277,13 @@ export const CheckboxConfigPanel: React.FC = ({ value={option.label} onChange={(e) => updateOption(index, "label", e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> updateOption(index, "value", e.target.value)} placeholder="값" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> = ({ disabled={localConfig.readonly} required={localConfig.required} defaultChecked={localConfig.defaultChecked} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -308,7 +308,7 @@ export const CodeConfigPanel: React.FC = ({ value={localConfig.placeholder || ""} onChange={(e) => updateConfig("placeholder", e.target.value)} placeholder="코드를 입력하세요..." - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -330,7 +330,7 @@ export const CodeConfigPanel: React.FC = ({ value={localConfig.defaultValue || ""} onChange={(e) => updateConfig("defaultValue", e.target.value)} placeholder="기본 코드 내용" - className="font-mono text-xs" + className="font-mono text-xs" style={{ fontSize: "12px" }} rows={4} />
diff --git a/frontend/components/screen/config-panels/DateConfigPanel.tsx b/frontend/components/screen/config-panels/DateConfigPanel.tsx index bbea14d4..7fcacc57 100644 --- a/frontend/components/screen/config-panels/DateConfigPanel.tsx +++ b/frontend/components/screen/config-panels/DateConfigPanel.tsx @@ -75,7 +75,7 @@ export const DateConfigPanel: React.FC = ({ return ( - + 날짜 설정 @@ -95,7 +95,7 @@ export const DateConfigPanel: React.FC = ({ value={localConfig.placeholder || ""} onChange={(e) => updateConfig("placeholder", e.target.value)} placeholder="날짜를 선택하세요" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -149,7 +149,7 @@ export const DateConfigPanel: React.FC = ({ type={localConfig.showTime ? "datetime-local" : "date"} value={localConfig.minDate || ""} onChange={(e) => updateConfig("minDate", e.target.value)} - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> @@ -213,7 +213,7 @@ export const EntityConfigPanel: React.FC = ({ value={localConfig.apiEndpoint || ""} onChange={(e) => updateConfig("apiEndpoint", e.target.value)} placeholder="/api/entities/user" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -232,7 +232,7 @@ export const EntityConfigPanel: React.FC = ({ value={localConfig.valueField || ""} onChange={(e) => updateConfig("valueField", e.target.value)} placeholder="id" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -245,7 +245,7 @@ export const EntityConfigPanel: React.FC = ({ value={localConfig.labelField || ""} onChange={(e) => updateConfig("labelField", e.target.value)} placeholder="name" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -263,13 +263,13 @@ export const EntityConfigPanel: React.FC = ({ value={newFieldName} onChange={(e) => setNewFieldName(e.target.value)} placeholder="필드명" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> setNewFieldLabel(e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> updateDisplayField(index, "label", e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> { @@ -269,7 +275,7 @@ export const FlowVisibilityConfigPanel: React.FC setTimeout(() => applyConfig(), 0); }} > - + @@ -277,7 +283,7 @@ export const FlowVisibilityConfigPanel: React.FC const flowConfig = (fw as any).componentConfig || {}; const flowName = flowConfig.flowName || `플로우 ${fw.id}`; return ( - + {flowName} ); @@ -289,251 +295,106 @@ export const FlowVisibilityConfigPanel: React.FC {/* 플로우가 선택되면 스텝 목록 표시 */} {selectedFlowComponentId && flowSteps.length > 0 && ( <> - {/* 모드 선택 */} -
- - { - setMode(value); - setTimeout(() => applyConfig(), 0); - }} - > -
- - -
-
- - -
-
-
- - {/* 단계 선택 (all 모드가 아닐 때만) */} - {mode !== "all" && ( -
-
- -
- - - -
-
- - {/* 스텝 체크박스 목록 */} -
- {flowSteps.map((step) => { - const isChecked = visibleSteps.includes(step.id); - - return ( -
- toggleStep(step.id)} - /> - -
- ); - })} + {/* 단계 선택 */} +
+
+ +
+ + +
- )} - {/* 레이아웃 옵션 */} -
- - { - setLayoutBehavior(value); - setTimeout(() => applyConfig(), 0); - }} - > -
- - -
-
- - -
-
+ {/* 스텝 체크박스 목록 */} +
+ {flowSteps.map((step) => { + const isChecked = visibleSteps.includes(step.id); + + return ( +
+ toggleStep(step.id)} + /> + +
+ ); + })} +
- {/* 🆕 그룹 설정 (auto-compact 모드일 때만 표시) */} - {layoutBehavior === "auto-compact" && ( -
-
- - 그룹 설정 - -

같은 그룹 ID를 가진 버튼들이 자동으로 정렬됩니다

-
- - {/* 그룹 ID */} -
- - setGroupId(e.target.value)} - placeholder="group-1" - className="h-8 text-xs sm:h-9 sm:text-sm" - /> -

- 같은 그룹 ID를 가진 버튼들이 하나의 그룹으로 묶입니다 -

-
- - {/* 정렬 방향 */} -
- - { - setGroupDirection(value); - setTimeout(() => applyConfig(), 0); - }} - > -
- - -
-
- - -
-
-
- - {/* 버튼 간격 */} -
- -
- { - setGroupGap(Number(e.target.value)); - setTimeout(() => applyConfig(), 0); - }} - className="h-8 text-xs sm:h-9 sm:text-sm" - /> - - {groupGap}px - -
-
- - {/* 정렬 방식 */} -
- - -
-
- )} - - {/* 미리보기 */} - - - - {mode === "whitelist" && visibleSteps.length > 0 && ( -
-

표시 단계:

-
- {visibleSteps.map((stepId) => { - const step = flowSteps.find((s) => s.id === stepId); - return ( - - {step?.stepName || `Step ${stepId}`} - - ); - })} -
-
- )} - {mode === "blacklist" && hiddenSteps.length > 0 && ( -
-

숨김 단계:

-
- {hiddenSteps.map((stepId) => { - const step = flowSteps.find((s) => s.id === stepId); - return ( - - {step?.stepName || `Step ${stepId}`} - - ); - })} -
-
- )} - {mode === "all" &&

이 버튼은 모든 단계에서 표시됩니다.

} - {mode === "whitelist" && visibleSteps.length === 0 &&

표시할 단계를 선택해주세요.

} -
-
- - {/* 🆕 자동 저장 안내 */} - - - - 설정이 자동으로 저장됩니다. 화면 저장 시 함께 적용됩니다. - - + {/* 정렬 방식 */} +
+ + +
)} diff --git a/frontend/components/screen/config-panels/FlowWidgetConfigPanel.tsx b/frontend/components/screen/config-panels/FlowWidgetConfigPanel.tsx index cfe7a324..11ec5dd5 100644 --- a/frontend/components/screen/config-panels/FlowWidgetConfigPanel.tsx +++ b/frontend/components/screen/config-panels/FlowWidgetConfigPanel.tsx @@ -54,7 +54,7 @@ export function FlowWidgetConfigPanel({ config = {}, onChange }: FlowWidgetConfi {loading ? (
- 로딩 중... + 로딩 중...
) : ( <> diff --git a/frontend/components/screen/config-panels/NumberConfigPanel.tsx b/frontend/components/screen/config-panels/NumberConfigPanel.tsx index ef3cfa7b..718e2988 100644 --- a/frontend/components/screen/config-panels/NumberConfigPanel.tsx +++ b/frontend/components/screen/config-panels/NumberConfigPanel.tsx @@ -56,7 +56,7 @@ export const NumberConfigPanel: React.FC = ({ return ( - 숫자 설정 + 숫자 설정 숫자 입력 필드의 세부 설정을 관리합니다. @@ -73,7 +73,7 @@ export const NumberConfigPanel: React.FC = ({ value={localConfig.placeholder || ""} onChange={(e) => updateConfig("placeholder", e.target.value)} placeholder="숫자를 입력하세요" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -88,7 +88,7 @@ export const NumberConfigPanel: React.FC = ({ value={localConfig.min ?? ""} onChange={(e) => updateConfig("min", e.target.value ? parseFloat(e.target.value) : undefined)} placeholder="0" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -101,7 +101,7 @@ export const NumberConfigPanel: React.FC = ({ value={localConfig.max ?? ""} onChange={(e) => updateConfig("max", e.target.value ? parseFloat(e.target.value) : undefined)} placeholder="100" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -118,7 +118,7 @@ export const NumberConfigPanel: React.FC = ({ placeholder="1" min="0" step="0.01" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />

증가/감소 버튼 클릭 시 변경되는 값의 크기

@@ -158,7 +158,7 @@ export const NumberConfigPanel: React.FC = ({ placeholder="2" min="0" max="10" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
)} @@ -223,7 +223,7 @@ export const NumberConfigPanel: React.FC = ({ min={localConfig.min} max={localConfig.max} step={localConfig.step} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
{localConfig.format === "currency" && "통화 형식으로 표시됩니다."} diff --git a/frontend/components/screen/config-panels/RadioConfigPanel.tsx b/frontend/components/screen/config-panels/RadioConfigPanel.tsx index 6b9f3f6b..db8eb6f3 100644 --- a/frontend/components/screen/config-panels/RadioConfigPanel.tsx +++ b/frontend/components/screen/config-panels/RadioConfigPanel.tsx @@ -168,7 +168,7 @@ export const RadioConfigPanel: React.FC = ({ return ( - + 라디오버튼 설정 @@ -188,7 +188,7 @@ export const RadioConfigPanel: React.FC = ({ value={localConfig.groupLabel || ""} onChange={(e) => updateConfig("groupLabel", e.target.value)} placeholder="라디오버튼 그룹 제목" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -201,7 +201,7 @@ export const RadioConfigPanel: React.FC = ({ value={localConfig.groupName || ""} onChange={(e) => updateConfig("groupName", e.target.value)} placeholder="자동 생성 (필드명 기반)" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />

비워두면 필드명을 기반으로 자동 생성됩니다.

@@ -252,19 +252,19 @@ export const RadioConfigPanel: React.FC = ({ value={newOptionLabel} onChange={(e) => setNewOptionLabel(e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> setNewOptionValue(e.target.value)} placeholder="값" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> @@ -278,7 +278,7 @@ export const RadioConfigPanel: React.FC = ({ value={bulkOptions} onChange={(e) => setBulkOptions(e.target.value)} placeholder="한 줄당 하나씩 입력하세요. 라벨만 입력하면 값과 동일하게 설정됩니다. 라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다. 예시: 서울 부산 대구시|daegu" - className="h-20 text-xs" + className="h-20 text-xs" style={{ fontSize: "12px" }} />
@@ -186,7 +186,7 @@ export const SelectConfigPanel: React.FC = ({ value={localConfig.emptyMessage || ""} onChange={(e) => updateConfig("emptyMessage", e.target.value)} placeholder="선택 가능한 옵션이 없습니다" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -247,19 +247,19 @@ export const SelectConfigPanel: React.FC = ({ value={newOptionLabel} onChange={(e) => setNewOptionLabel(e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> setNewOptionValue(e.target.value)} placeholder="값" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> @@ -273,7 +273,7 @@ export const SelectConfigPanel: React.FC = ({ value={bulkOptions} onChange={(e) => setBulkOptions(e.target.value)} placeholder="한 줄당 하나씩 입력하세요. 라벨만 입력하면 값과 동일하게 설정됩니다. 라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다. 예시: 서울 부산 대구시|daegu" - className="h-20 text-xs" + className="h-20 text-xs" style={{ fontSize: "12px" }} />
@@ -88,7 +88,7 @@ export const TextConfigPanel: React.FC = ({ onChange={(e) => updateConfig("minLength", e.target.value ? parseInt(e.target.value) : undefined)} placeholder="0" min="0" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -102,7 +102,7 @@ export const TextConfigPanel: React.FC = ({ onChange={(e) => updateConfig("maxLength", e.target.value ? parseInt(e.target.value) : undefined)} placeholder="100" min="1" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -141,7 +141,7 @@ export const TextConfigPanel: React.FC = ({ value={localConfig.pattern || ""} onChange={(e) => updateConfig("pattern", e.target.value)} placeholder="예: [A-Za-z0-9]+" - className="font-mono text-xs" + className="font-mono text-xs" style={{ fontSize: "12px" }} />

JavaScript 정규식 패턴을 입력하세요.

@@ -219,7 +219,7 @@ export const TextConfigPanel: React.FC = ({ minLength={localConfig.minLength} pattern={localConfig.pattern} autoComplete={localConfig.autoComplete} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
diff --git a/frontend/components/screen/config-panels/TextareaConfigPanel.tsx b/frontend/components/screen/config-panels/TextareaConfigPanel.tsx index 5cd0c825..f700e61d 100644 --- a/frontend/components/screen/config-panels/TextareaConfigPanel.tsx +++ b/frontend/components/screen/config-panels/TextareaConfigPanel.tsx @@ -68,7 +68,7 @@ export const TextareaConfigPanel: React.FC = ({ return ( - + 텍스트영역 설정 @@ -88,7 +88,7 @@ export const TextareaConfigPanel: React.FC = ({ value={localConfig.placeholder || ""} onChange={(e) => updateConfig("placeholder", e.target.value)} placeholder="내용을 입력하세요" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -101,7 +101,7 @@ export const TextareaConfigPanel: React.FC = ({ value={localConfig.defaultValue || ""} onChange={(e) => updateConfig("defaultValue", e.target.value)} placeholder="기본 텍스트 내용" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} rows={3} /> {localConfig.showCharCount && ( @@ -151,7 +151,7 @@ export const TextareaConfigPanel: React.FC = ({ placeholder="자동 (CSS로 제어)" min={10} max={200} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />

비워두면 CSS width로 제어됩니다.

@@ -203,7 +203,7 @@ export const TextareaConfigPanel: React.FC = ({ }} placeholder="제한 없음" min={0} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -221,7 +221,7 @@ export const TextareaConfigPanel: React.FC = ({ }} placeholder="제한 없음" min={1} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -333,7 +333,7 @@ export const TextareaConfigPanel: React.FC = ({ resize: localConfig.resizable ? "both" : "none", minHeight: localConfig.autoHeight ? "auto" : undefined, }} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} wrap={localConfig.wrap} /> {localConfig.showCharCount && ( diff --git a/frontend/components/screen/dialogs/FlowButtonGroupDialog.tsx b/frontend/components/screen/dialogs/FlowButtonGroupDialog.tsx index 659c5fa1..606a3071 100644 --- a/frontend/components/screen/dialogs/FlowButtonGroupDialog.tsx +++ b/frontend/components/screen/dialogs/FlowButtonGroupDialog.tsx @@ -94,7 +94,7 @@ export const FlowButtonGroupDialog: React.FC = ({ max={100} value={gap} onChange={(e) => setGap(Number(e.target.value))} - className="h-9 text-sm sm:h-10" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} /> {gap}px @@ -109,7 +109,7 @@ export const FlowButtonGroupDialog: React.FC = ({ 정렬 방식 setSearchQuery(e.target.value)} - className="h-8 pl-8 text-xs" + onChange={(e) => { + const value = e.target.value; + setSearchQuery(value); + // 테이블 검색도 함께 업데이트 + if (onSearchChange) { + onSearchChange(value); + } + }} + className="h-8 pl-8 text-xs" style={{ fontSize: "12px" }} />
{/* 카테고리 탭 */} - - - + + + - 테이블 + 테이블 - + - 입력 + 입력 - + - 액션 + 액션 - + - 표시 + 표시 - + - 레이아웃 + 레이아웃 diff --git a/frontend/components/screen/panels/DataTableConfigPanel.tsx b/frontend/components/screen/panels/DataTableConfigPanel.tsx index 163a446d..8cf02cbc 100644 --- a/frontend/components/screen/panels/DataTableConfigPanel.tsx +++ b/frontend/components/screen/panels/DataTableConfigPanel.tsx @@ -458,7 +458,7 @@ const DataTableConfigPanelComponent: React.FC = ({ updateSettings({ options: newOptions }); }} placeholder="옵션명" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -558,7 +558,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={localSettings.max || ""} onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })} placeholder="최대값" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -571,7 +571,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={localSettings.step || "0.01"} onChange={(e) => updateSettings({ step: e.target.value })} placeholder="0.01" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
)} @@ -589,7 +589,7 @@ const DataTableConfigPanelComponent: React.FC = ({ type="date" value={localSettings.minDate || ""} onChange={(e) => updateSettings({ minDate: e.target.value })} - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -598,7 +598,7 @@ const DataTableConfigPanelComponent: React.FC = ({ type="date" value={localSettings.maxDate || ""} onChange={(e) => updateSettings({ maxDate: e.target.value })} - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -626,7 +626,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={localSettings.maxLength || ""} onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })} placeholder="최대 문자 수" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -635,7 +635,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={localSettings.placeholder || ""} onChange={(e) => updateSettings({ placeholder: e.target.value })} placeholder="입력 안내 텍스트" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -652,7 +652,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={localSettings.rows || "3"} onChange={(e) => updateSettings({ rows: Number(e.target.value) })} placeholder="3" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -662,7 +662,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={localSettings.maxLength || ""} onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })} placeholder="최대 문자 수" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -678,7 +678,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={localSettings.accept || ""} onChange={(e) => updateSettings({ accept: e.target.value })} placeholder=".jpg,.png,.pdf" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -688,7 +688,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"} onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })} placeholder="10" - className="h-7 text-xs" + className="h-7 text-xs" style={{ fontSize: "12px" }} />
@@ -1132,7 +1132,7 @@ const DataTableConfigPanelComponent: React.FC = ({ {/* 기본 설정 */} - + 기본 설정 @@ -1184,7 +1184,7 @@ const DataTableConfigPanelComponent: React.FC = ({ onUpdateComponent({ enableAdd: checked as boolean }); }} /> -
@@ -1198,7 +1198,7 @@ const DataTableConfigPanelComponent: React.FC = ({ onUpdateComponent({ enableEdit: checked as boolean }); }} /> -
@@ -1212,7 +1212,7 @@ const DataTableConfigPanelComponent: React.FC = ({ onUpdateComponent({ enableDelete: checked as boolean }); }} /> -
@@ -1220,7 +1220,7 @@ const DataTableConfigPanelComponent: React.FC = ({
-
-
-
@@ -1284,7 +1284,7 @@ const DataTableConfigPanelComponent: React.FC = ({
-
-
-
@@ -1521,7 +1521,7 @@ const DataTableConfigPanelComponent: React.FC = ({ - + 컬럼 설정 @@ -1535,7 +1535,7 @@ const DataTableConfigPanelComponent: React.FC = ({
{/* 파일 컬럼 추가 버튼 */} - @@ -1654,7 +1654,7 @@ const DataTableConfigPanelComponent: React.FC = ({ } }} placeholder="표시명을 입력하세요" - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />
@@ -1673,7 +1673,7 @@ const DataTableConfigPanelComponent: React.FC = ({ updateColumn(column.id, { gridColumns: newGridColumns }); }} > - + @@ -1861,7 +1861,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} > - + @@ -1902,7 +1902,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} > - + @@ -1947,7 +1947,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} placeholder="고정값 입력..." - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />
)} @@ -1967,7 +1967,7 @@ const DataTableConfigPanelComponent: React.FC = ({ - + 필터 설정 @@ -1995,7 +1995,7 @@ const DataTableConfigPanelComponent: React.FC = ({ {component.filters.length === 0 ? (
-

필터가 없습니다

+

필터가 없습니다

컬럼을 추가하면 자동으로 필터가 생성됩니다

) : ( @@ -2073,7 +2073,7 @@ const DataTableConfigPanelComponent: React.FC = ({ updateFilter(index, { label: newValue }); }} placeholder="필터 이름 입력..." - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />

@@ -2112,7 +2112,7 @@ const DataTableConfigPanelComponent: React.FC = ({ } }} > - + @@ -2144,7 +2144,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={filter.gridColumns.toString()} onValueChange={(value) => updateFilter(index, { gridColumns: parseInt(value) })} > - + @@ -2192,7 +2192,7 @@ const DataTableConfigPanelComponent: React.FC = ({ - + 모달 및 페이징 설정 @@ -2258,7 +2258,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} /> -

@@ -185,7 +186,8 @@ export const DetailSettingsPanel: React.FC = ({ onUpdateProperty(layoutComponent.id, "zones", newZones); } }} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} />
@@ -199,7 +201,8 @@ export const DetailSettingsPanel: React.FC = ({ onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value)) } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} /> @@ -243,7 +246,8 @@ export const DetailSettingsPanel: React.FC = ({ onUpdateProperty(layoutComponent.id, "zones", updatedZones); } }} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} > @@ -302,7 +306,8 @@ export const DetailSettingsPanel: React.FC = ({ onUpdateProperty(layoutComponent.id, "zones", newZones); } }} - className="w-20 rounded border border-gray-300 px-2 py-1 text-sm" + className="w-20 rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} /> @@ -317,7 +322,8 @@ export const DetailSettingsPanel: React.FC = ({ onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value)) } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} /> @@ -332,7 +338,8 @@ export const DetailSettingsPanel: React.FC = ({ - + diff --git a/frontend/components/screen/panels/FlowButtonGroupPanel.tsx b/frontend/components/screen/panels/FlowButtonGroupPanel.tsx index 83720945..5a28aa70 100644 --- a/frontend/components/screen/panels/FlowButtonGroupPanel.tsx +++ b/frontend/components/screen/panels/FlowButtonGroupPanel.tsx @@ -98,7 +98,7 @@ export const FlowButtonGroupPanel: React.FC = ({ size="sm" variant="ghost" onClick={() => onSelectGroup(groupInfo.buttons.map((b) => b.id))} - className="h-7 px-2 text-xs" + className="h-7 px-2 text-xs" style={{ fontSize: "12px" }} > 선택 @@ -152,7 +152,7 @@ export const FlowButtonGroupPanel: React.FC = ({ {groupInfo.buttons.map((button) => (
diff --git a/frontend/components/screen/panels/GridPanel.tsx b/frontend/components/screen/panels/GridPanel.tsx index 4df77897..16178e57 100644 --- a/frontend/components/screen/panels/GridPanel.tsx +++ b/frontend/components/screen/panels/GridPanel.tsx @@ -68,7 +68,7 @@ export const GridPanel: React.FC = ({ size="sm" variant="outline" onClick={onForceGridUpdate} - className="h-7 px-2 text-xs" + className="h-7 px-2 text-xs" style={{ fontSize: "12px" }} title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다" > @@ -266,7 +266,7 @@ export const GridPanel: React.FC = ({

격자 정보

-
+
해상도: diff --git a/frontend/components/screen/panels/LayoutsPanel.tsx b/frontend/components/screen/panels/LayoutsPanel.tsx index c38082b9..760a8229 100644 --- a/frontend/components/screen/panels/LayoutsPanel.tsx +++ b/frontend/components/screen/panels/LayoutsPanel.tsx @@ -214,7 +214,7 @@ export default function LayoutsPanel({
- {layout.name} + {layout.name} {layout.description && ( diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx index 248e2e8a..b45bc517 100644 --- a/frontend/components/screen/panels/PropertiesPanel.tsx +++ b/frontend/components/screen/panels/PropertiesPanel.tsx @@ -551,11 +551,6 @@ const PropertiesPanelComponent: React.FC = ({ {/* 액션 버튼들 */}
- - {canGroup && ( )} - -
@@ -655,7 +645,7 @@ const PropertiesPanelComponent: React.FC = ({ }} className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" /> -
@@ -671,7 +661,7 @@ const PropertiesPanelComponent: React.FC = ({ }} className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" /> -
@@ -952,7 +942,7 @@ const PropertiesPanelComponent: React.FC = ({ ) : (
-

카드 레이아웃은 자동으로 크기가 계산됩니다

+

카드 레이아웃은 자동으로 크기가 계산됩니다

카드 개수와 간격 설정은 상세설정에서 조정하세요

)} diff --git a/frontend/components/screen/panels/ResolutionPanel.tsx b/frontend/components/screen/panels/ResolutionPanel.tsx index 82072987..90680f01 100644 --- a/frontend/components/screen/panels/ResolutionPanel.tsx +++ b/frontend/components/screen/panels/ResolutionPanel.tsx @@ -82,9 +82,9 @@ const ResolutionPanel: React.FC = ({ currentResolution, on
{/* 프리셋 선택 */}
- + setCustomWidth(e.target.value)} placeholder="1920" min="1" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
- + setCustomHeight(e.target.value)} placeholder="1080" min="1" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
-
)} - - {/* 해상도 정보 */} -
-
- 화면 비율: - {(currentResolution.width / currentResolution.height).toFixed(2)}:1 -
-
- 총 픽셀: - {(currentResolution.width * currentResolution.height).toLocaleString()} -
-
); }; diff --git a/frontend/components/screen/panels/RowSettingsPanel.tsx b/frontend/components/screen/panels/RowSettingsPanel.tsx index 4bb535aa..2bd48a12 100644 --- a/frontend/components/screen/panels/RowSettingsPanel.tsx +++ b/frontend/components/screen/panels/RowSettingsPanel.tsx @@ -106,7 +106,7 @@ export const RowSettingsPanel: React.FC = ({ row, onUpdat variant={row.gap === preset ? "default" : "outline"} size="sm" onClick={() => onUpdateRow({ gap: preset })} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} > {GAP_PRESETS[preset].label} @@ -127,7 +127,7 @@ export const RowSettingsPanel: React.FC = ({ row, onUpdat variant={row.padding === preset ? "default" : "outline"} size="sm" onClick={() => onUpdateRow({ padding: preset })} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} > {GAP_PRESETS[preset].label} diff --git a/frontend/components/screen/panels/TablesPanel.tsx b/frontend/components/screen/panels/TablesPanel.tsx index f75b699e..abeff8d6 100644 --- a/frontend/components/screen/panels/TablesPanel.tsx +++ b/frontend/components/screen/panels/TablesPanel.tsx @@ -1,23 +1,8 @@ "use client"; -import React, { useState } from "react"; -import { Button } from "@/components/ui/button"; +import React from "react"; import { Badge } from "@/components/ui/badge"; -import { - Database, - ChevronDown, - ChevronRight, - Type, - Hash, - Calendar, - CheckSquare, - List, - AlignLeft, - Code, - Building, - File, - Search, -} from "lucide-react"; +import { Database, Type, Hash, Calendar, CheckSquare, List, AlignLeft, Code, Building, File } from "lucide-react"; import { TableInfo, WebType } from "@/types/screen"; interface TablesPanelProps { @@ -65,23 +50,9 @@ const getWidgetIcon = (widgetType: WebType) => { export const TablesPanel: React.FC = ({ tables, searchTerm, - onSearchChange, onDragStart, - selectedTableName, placedColumns = new Set(), }) => { - const [expandedTables, setExpandedTables] = useState>(new Set()); - - const toggleTable = (tableName: string) => { - const newExpanded = new Set(expandedTables); - if (newExpanded.has(tableName)) { - newExpanded.delete(tableName); - } else { - newExpanded.add(tableName); - } - setExpandedTables(newExpanded); - }; - // 이미 배치된 컬럼을 제외한 테이블 정보 생성 const tablesWithAvailableColumns = tables.map((table) => ({ ...table, @@ -91,137 +62,89 @@ export const TablesPanel: React.FC = ({ }), })); + // 검색어가 있으면 컬럼 필터링 const filteredTables = tablesWithAvailableColumns - .filter((table) => table.columns.length > 0) // 사용 가능한 컬럼이 있는 테이블만 표시 - .filter( - (table) => - table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) || - table.columns.some((col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase())), - ); + .map((table) => { + if (!searchTerm) { + return table; + } + + const searchLower = searchTerm.toLowerCase(); + + // 테이블명이 검색어와 일치하면 모든 컬럼 표시 + if ( + table.tableName.toLowerCase().includes(searchLower) || + (table.tableLabel && table.tableLabel.toLowerCase().includes(searchLower)) + ) { + return table; + } + + // 그렇지 않으면 컬럼명/라벨이 검색어와 일치하는 컬럼만 필터링 + const filteredColumns = table.columns.filter( + (col) => + col.columnName.toLowerCase().includes(searchLower) || + (col.columnLabel && col.columnLabel.toLowerCase().includes(searchLower)), + ); + + return { + ...table, + columns: filteredColumns, + }; + }) + .filter((table) => table.columns.length > 0); // 컬럼이 있는 테이블만 표시 return (
- {/* 헤더 */} -
- {selectedTableName && ( -
-
선택된 테이블
-
- - {selectedTableName} -
-
- )} - - {/* 검색 */} -
- - onSearchChange(e.target.value)} - className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 pl-8 text-xs focus-visible:ring-1 focus-visible:outline-none" - /> -
- -
총 {filteredTables.length}개
-
- - {/* 테이블 목록 */} -
-
- {filteredTables.map((table) => { - const isExpanded = expandedTables.has(table.tableName); - - return ( -
- {/* 테이블 헤더 */} -
toggleTable(table.tableName)} - > -
- {isExpanded ? ( - - ) : ( - - )} - -
-
{table.tableLabel || table.tableName}
-
{table.columns.length}개
-
-
- - + {/* 테이블과 컬럼 평면 목록 */} +
+
+ {filteredTables.map((table) => ( +
+ {/* 테이블 헤더 */} +
+
+ + {table.tableLabel || table.tableName} + + {table.columns.length}개 +
+
- {/* 컬럼 목록 */} - {isExpanded && ( -
-
8 ? "max-h-64 overflow-y-auto" : ""}`}> - {table.columns.map((column, index) => ( -
onDragStart(e, table, column)} - > -
- {getWidgetIcon(column.widgetType)} -
-
- {column.columnLabel || column.columnName} -
-
{column.dataType}
-
-
+ {/* 컬럼 목록 (항상 표시) */} +
+ {table.columns.map((column) => ( +
onDragStart(e, table, column)} + > +
+ {getWidgetIcon(column.widgetType)} +
+
{column.columnLabel || column.columnName}
+
{column.dataType}
+
+
-
- - {column.widgetType} - - {column.required && ( - - 필수 - - )} -
-
- ))} - - {/* 컬럼 수가 많을 때 안내 메시지 */} - {table.columns.length > 8 && ( -
-
- 📜 총 {table.columns.length}개 컬럼 (스크롤하여 더 보기) -
-
+
+ + {column.widgetType} + + {column.required && ( + + 필수 + )}
- )} + ))}
- ); - })} +
+ ))}
- - {/* 푸터 */} -
-
💡 테이블이나 컬럼을 캔버스로 드래그하세요
-
); }; diff --git a/frontend/components/screen/panels/TemplatesPanel.tsx b/frontend/components/screen/panels/TemplatesPanel.tsx index 76e78337..d4d4bae9 100644 --- a/frontend/components/screen/panels/TemplatesPanel.tsx +++ b/frontend/components/screen/panels/TemplatesPanel.tsx @@ -528,7 +528,7 @@ export const TemplatesPanel: React.FC = ({ onDragStart }) =
- 템플릿 로딩 실패, 기본 템플릿 사용 중 + 템플릿 로딩 실패, 기본 템플릿 사용 중
- )} - {onDeleteComponent && ( - - )} -
); }; @@ -513,7 +497,7 @@ export const UnifiedPropertiesPanel: React.FC = ({
handleUpdate("webType", value)}> - + diff --git a/frontend/components/screen/panels/WebTypeConfigPanel.tsx b/frontend/components/screen/panels/WebTypeConfigPanel.tsx index 9227c269..8c44fb48 100644 --- a/frontend/components/screen/panels/WebTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/WebTypeConfigPanel.tsx @@ -109,7 +109,7 @@ export const WebTypeConfigPanel: React.FC = ({ webType, <>
-
-