diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index 746bf931..da0ea772 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -1097,7 +1097,11 @@ export async function saveMenu( let requestCompanyCode = menuData.companyCode || menuData.company_code; // "none"이나 빈 값은 undefined로 처리하여 사용자 회사 코드 사용 - if (requestCompanyCode === "none" || requestCompanyCode === "" || !requestCompanyCode) { + if ( + requestCompanyCode === "none" || + requestCompanyCode === "" || + !requestCompanyCode + ) { requestCompanyCode = undefined; } @@ -1252,7 +1256,8 @@ export async function updateMenu( } } - const requestCompanyCode = menuData.companyCode || menuData.company_code || currentMenu.company_code; + const requestCompanyCode = + menuData.companyCode || menuData.company_code || currentMenu.company_code; // company_code 변경 시도하는 경우 권한 체크 if (requestCompanyCode !== currentMenu.company_code) { @@ -1268,7 +1273,10 @@ export async function updateMenu( } } // 회사 관리자는 자기 회사로만 변경 가능 - else if (userCompanyCode !== "*" && requestCompanyCode !== userCompanyCode) { + else if ( + userCompanyCode !== "*" && + requestCompanyCode !== userCompanyCode + ) { res.status(403).json({ success: false, message: "해당 회사로 변경할 권한이 없습니다.", @@ -1324,7 +1332,7 @@ export async function updateMenu( if (!menuUrl) { await query( `UPDATE screen_menu_assignments - SET is_active = 'N', updated_date = NOW() + SET is_active = 'N' WHERE menu_objid = $1 AND company_code = $2`, [Number(menuId), companyCode] ); @@ -1493,8 +1501,13 @@ export async function deleteMenusBatch( ); // 권한 체크: 공통 메뉴 포함 여부 확인 - const hasCommonMenu = menusToDelete.some((menu: any) => menu.company_code === "*"); - if (hasCommonMenu && (userCompanyCode !== "*" || userType !== "SUPER_ADMIN")) { + const hasCommonMenu = menusToDelete.some( + (menu: any) => menu.company_code === "*" + ); + if ( + hasCommonMenu && + (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") + ) { res.status(403).json({ success: false, message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.", @@ -1506,7 +1519,8 @@ export async function deleteMenusBatch( // 회사 관리자는 자기 회사 메뉴만 삭제 가능 if (userCompanyCode !== "*") { const unauthorizedMenus = menusToDelete.filter( - (menu: any) => menu.company_code !== userCompanyCode && menu.company_code !== "*" + (menu: any) => + menu.company_code !== userCompanyCode && menu.company_code !== "*" ); if (unauthorizedMenus.length > 0) { res.status(403).json({ @@ -2674,7 +2688,10 @@ export const getCompanyByCode = async ( res.status(200).json(response); } catch (error) { - logger.error("회사 정보 조회 실패", { error, companyCode: req.params.companyCode }); + logger.error("회사 정보 조회 실패", { + error, + companyCode: req.params.companyCode, + }); res.status(500).json({ success: false, message: "회사 정보 조회 중 오류가 발생했습니다.", @@ -2740,7 +2757,9 @@ export const updateCompany = async ( // 사업자등록번호 중복 체크 및 유효성 검증 (자기 자신 제외) if (business_registration_number && business_registration_number.trim()) { // 유효성 검증 - const businessNumberValidation = validateBusinessNumber(business_registration_number.trim()); + const businessNumberValidation = validateBusinessNumber( + business_registration_number.trim() + ); if (!businessNumberValidation.isValid) { res.status(400).json({ success: false, @@ -3283,7 +3302,9 @@ export async function copyMenu( // 권한 체크: 최고 관리자만 가능 if (!isSuperAdmin && userType !== "SUPER_ADMIN") { - logger.warn(`권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})`); + logger.warn( + `권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})` + ); res.status(403).json({ success: false, message: "메뉴 복사는 최고 관리자(SUPER_ADMIN)만 가능합니다", diff --git a/backend-node/src/controllers/screenManagementController.ts b/backend-node/src/controllers/screenManagementController.ts index be3a16a3..0ff80988 100644 --- a/backend-node/src/controllers/screenManagementController.ts +++ b/backend-node/src/controllers/screenManagementController.ts @@ -148,11 +148,11 @@ export const updateScreenInfo = async ( try { const { id } = req.params; const { companyCode } = req.user as any; - const { screenName, description, isActive } = req.body; + const { screenName, tableName, description, isActive } = req.body; await screenManagementService.updateScreenInfo( parseInt(id), - { screenName, description, isActive }, + { screenName, tableName, description, isActive }, companyCode ); res.json({ success: true, message: "화면 정보가 수정되었습니다." }); diff --git a/backend-node/src/routes/dataflow/node-flows.ts b/backend-node/src/routes/dataflow/node-flows.ts index 7ede970a..f13d65cf 100644 --- a/backend-node/src/routes/dataflow/node-flows.ts +++ b/backend-node/src/routes/dataflow/node-flows.ts @@ -7,6 +7,7 @@ import { query, queryOne } from "../../database/db"; import { logger } from "../../utils/logger"; import { NodeFlowExecutionService } from "../../services/nodeFlowExecutionService"; import { AuthenticatedRequest } from "../../types/auth"; +import { authenticateToken } from "../../middleware/authMiddleware"; const router = Router(); @@ -217,19 +218,29 @@ router.delete("/:flowId", async (req: Request, res: Response) => { * 플로우 실행 * POST /api/dataflow/node-flows/:flowId/execute */ -router.post("/:flowId/execute", async (req: Request, res: Response) => { +router.post("/:flowId/execute", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { try { const { flowId } = req.params; const contextData = req.body; logger.info(`🚀 플로우 실행 요청: flowId=${flowId}`, { contextDataKeys: Object.keys(contextData), + userId: req.user?.userId, + companyCode: req.user?.companyCode, }); + // 사용자 정보를 contextData에 추가 + const enrichedContextData = { + ...contextData, + userId: req.user?.userId, + userName: req.user?.userName, + companyCode: req.user?.companyCode, + }; + // 플로우 실행 const result = await NodeFlowExecutionService.executeFlow( parseInt(flowId, 10), - contextData + enrichedContextData ); return res.json({ diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index 546b215a..9cdd85f3 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -938,6 +938,30 @@ export class NodeFlowExecutionService { insertedData[mapping.targetField] = value; }); + // 🆕 writer와 company_code 자동 추가 (필드 매핑에 없는 경우) + const hasWriterMapping = fieldMappings.some((m: any) => m.targetField === "writer"); + const hasCompanyCodeMapping = fieldMappings.some((m: any) => m.targetField === "company_code"); + + // 컨텍스트에서 사용자 정보 추출 + const userId = context.buttonContext?.userId; + const companyCode = context.buttonContext?.companyCode; + + // writer 자동 추가 (매핑에 없고, 컨텍스트에 userId가 있는 경우) + if (!hasWriterMapping && userId) { + fields.push("writer"); + values.push(userId); + insertedData.writer = userId; + console.log(` 🔧 자동 추가: writer = ${userId}`); + } + + // company_code 자동 추가 (매핑에 없고, 컨텍스트에 companyCode가 있는 경우) + if (!hasCompanyCodeMapping && companyCode && companyCode !== "*") { + fields.push("company_code"); + values.push(companyCode); + insertedData.company_code = companyCode; + console.log(` 🔧 자동 추가: company_code = ${companyCode}`); + } + const sql = ` INSERT INTO ${targetTable} (${fields.join(", ")}) VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")}) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 6c3a3430..9dbe0270 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -321,7 +321,7 @@ export class ScreenManagementService { */ async updateScreenInfo( screenId: number, - updateData: { screenName: string; description?: string; isActive: string }, + updateData: { screenName: string; tableName?: string; description?: string; isActive: string }, userCompanyCode: string ): Promise { // 권한 확인 @@ -343,16 +343,18 @@ export class ScreenManagementService { throw new Error("이 화면을 수정할 권한이 없습니다."); } - // 화면 정보 업데이트 + // 화면 정보 업데이트 (tableName 포함) await query( `UPDATE screen_definitions SET screen_name = $1, - description = $2, - is_active = $3, - updated_date = $4 - WHERE screen_id = $5`, + table_name = $2, + description = $3, + is_active = $4, + updated_date = $5 + WHERE screen_id = $6`, [ updateData.screenName, + updateData.tableName || null, updateData.description || null, updateData.isActive, new Date(), diff --git a/frontend/app/(main)/main/page.tsx b/frontend/app/(main)/main/page.tsx index 45f75f67..00ef509b 100644 --- a/frontend/app/(main)/main/page.tsx +++ b/frontend/app/(main)/main/page.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge"; */ export default function MainPage() { return ( -
+
{/* 메인 컨텐츠 */} {/* Welcome Message */} diff --git a/frontend/app/(main)/page.tsx b/frontend/app/(main)/page.tsx index 53c6dfb1..f5d7a153 100644 --- a/frontend/app/(main)/page.tsx +++ b/frontend/app/(main)/page.tsx @@ -1,6 +1,6 @@ export default function MainHomePage() { return ( -
+
{/* 대시보드 컨텐츠 */}

WACE 솔루션에 오신 것을 환영합니다!

diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 3b75f262..ce99a685 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -26,7 +26,7 @@ function ScreenViewPage() { const searchParams = useSearchParams(); const router = useRouter(); const screenId = parseInt(params.screenId as string); - + // URL 쿼리에서 menuObjid 가져오기 (메뉴 스코프) const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined; @@ -178,31 +178,26 @@ function ScreenViewPage() { for (const comp of layout.components) { // type: "component" 또는 type: "widget" 모두 처리 - if (comp.type === 'widget' || comp.type === 'component') { + if (comp.type === "widget" || comp.type === "component") { const widget = comp as any; const fieldName = widget.columnName || widget.id; - + // autoFill 처리 if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) { const autoFillConfig = widget.autoFill || (comp as any).autoFill; const currentValue = formData[fieldName]; - - if (currentValue === undefined || currentValue === '') { + + if (currentValue === undefined || currentValue === "") { const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig; - + // 사용자 정보에서 필터 값 가져오기 const userValue = user?.[userField as keyof typeof user]; - + if (userValue && sourceTable && filterColumn && displayColumn) { try { const { tableTypeApi } = await import("@/lib/api/screen"); - const result = await tableTypeApi.getTableRecord( - sourceTable, - filterColumn, - userValue, - displayColumn - ); - + const result = await tableTypeApi.getTableRecord(sourceTable, filterColumn, userValue, displayColumn); + setFormData((prev) => ({ ...prev, [fieldName]: result.value, @@ -233,24 +228,27 @@ function ScreenViewPage() { const designWidth = layout?.screenResolution?.width || 1200; const designHeight = layout?.screenResolution?.height || 800; - // containerRef는 이미 패딩이 적용된 영역 내부이므로 offsetWidth는 패딩을 제외한 크기입니다 + // 컨테이너의 실제 크기 const containerWidth = containerRef.current.offsetWidth; const containerHeight = containerRef.current.offsetHeight; - // 화면이 잘리지 않도록 가로/세로 중 작은 쪽 기준으로 스케일 조정 - const scaleX = containerWidth / designWidth; - const scaleY = containerHeight / designHeight; - // 전체 화면이 보이도록 작은 쪽 기준으로 스케일 설정 - const newScale = Math.min(scaleX, scaleY); - + // 여백 설정: 좌우 16px씩 (총 32px), 상단 패딩 32px (pt-8) + const MARGIN_X = 32; + const availableWidth = containerWidth - MARGIN_X; + + // 가로 기준 스케일 계산 (좌우 여백 16px씩 고정) + const newScale = availableWidth / designWidth; + console.log("📐 스케일 계산:", { containerWidth, containerHeight, + MARGIN_X, + availableWidth, designWidth, designHeight, - scaleX, - scaleY, finalScale: newScale, + "스케일된 화면 크기": `${designWidth * newScale}px × ${designHeight * newScale}px`, + "실제 좌우 여백": `${(containerWidth - designWidth * newScale) / 2}px씩`, }); setScale(newScale); @@ -307,503 +305,531 @@ function ScreenViewPage() { return ( -
- {/* 레이아웃 준비 중 로딩 표시 */} - {!layoutReady && ( -
-
- -

화면 준비 중...

-
+
+ {/* 레이아웃 준비 중 로딩 표시 */} + {!layoutReady && ( +
+
+ +

화면 준비 중...

- )} +
+ )} - {/* 절대 위치 기반 렌더링 (화면관리와 동일한 방식) */} - {layoutReady && layout && layout.components.length > 0 ? ( -
- {/* 최상위 컴포넌트들 렌더링 */} - {(() => { - // 🆕 플로우 버튼 그룹 감지 및 처리 - const topLevelComponents = layout.components.filter((component) => !component.parentId); + {/* 절대 위치 기반 렌더링 (화면관리와 동일한 방식) */} + {layoutReady && layout && layout.components.length > 0 ? ( +
+ {/* 최상위 컴포넌트들 렌더링 */} + {(() => { + // 🆕 플로우 버튼 그룹 감지 및 처리 + const topLevelComponents = layout.components.filter((component) => !component.parentId); - // 화면 관리에서 설정한 해상도를 사용하므로 widthOffset 계산 불필요 - // 모든 컴포넌트는 원본 위치 그대로 사용 - const widthOffset = 0; + // 화면 관리에서 설정한 해상도를 사용하므로 widthOffset 계산 불필요 + // 모든 컴포넌트는 원본 위치 그대로 사용 + const widthOffset = 0; - const buttonGroups: Record = {}; - const processedButtonIds = new Set(); - // 🔍 전체 버튼 목록 확인 - const allButtons = topLevelComponents.filter((component) => { - const isButton = - (component.type === "component" && - ["button-primary", "button-secondary"].includes((component as any).componentType)) || - (component.type === "widget" && (component as any).widgetType === "button"); - return isButton; - }); + const buttonGroups: Record = {}; + const processedButtonIds = new Set(); + // 🔍 전체 버튼 목록 확인 + const allButtons = topLevelComponents.filter((component) => { + const isButton = + (component.type === "component" && + ["button-primary", "button-secondary"].includes((component as any).componentType)) || + (component.type === "widget" && (component as any).widgetType === "button"); + return isButton; + }); - console.log( - "🔍 메뉴에서 발견된 전체 버튼:", - allButtons.map((b) => ({ - id: b.id, - label: b.label, - positionX: b.position.x, - positionY: b.position.y, - width: b.size?.width, - height: b.size?.height, - })), - ); + console.log( + "🔍 메뉴에서 발견된 전체 버튼:", + allButtons.map((b) => ({ + id: b.id, + label: b.label, + positionX: b.position.x, + positionY: b.position.y, + width: b.size?.width, + height: b.size?.height, + })), + ); - topLevelComponents.forEach((component) => { - const isButton = - (component.type === "component" && - ["button-primary", "button-secondary"].includes((component as any).componentType)) || - (component.type === "widget" && (component as any).widgetType === "button"); + topLevelComponents.forEach((component) => { + const isButton = + (component.type === "component" && + ["button-primary", "button-secondary"].includes((component as any).componentType)) || + (component.type === "widget" && (component as any).widgetType === "button"); - if (isButton) { - const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as - | FlowVisibilityConfig - | undefined; + if (isButton) { + const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as + | FlowVisibilityConfig + | undefined; - // 🔧 임시: 버튼 그룹 기능 완전 비활성화 - // TODO: 사용자가 명시적으로 그룹을 원하는 경우에만 활성화하도록 UI 개선 필요 - const DISABLE_BUTTON_GROUPS = false; + // 🔧 임시: 버튼 그룹 기능 완전 비활성화 + // TODO: 사용자가 명시적으로 그룹을 원하는 경우에만 활성화하도록 UI 개선 필요 + const DISABLE_BUTTON_GROUPS = false; - if ( - !DISABLE_BUTTON_GROUPS && - flowConfig?.enabled && - flowConfig.layoutBehavior === "auto-compact" && - flowConfig.groupId - ) { - if (!buttonGroups[flowConfig.groupId]) { - buttonGroups[flowConfig.groupId] = []; + if ( + !DISABLE_BUTTON_GROUPS && + 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); + // else: 모든 버튼을 개별 렌더링 } - // else: 모든 버튼을 개별 렌더링 - } - }); + }); - const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); - // TableSearchWidget들을 먼저 찾기 - const tableSearchWidgets = regularComponents.filter( - (c) => (c as any).componentId === "table-search-widget" - ); + // TableSearchWidget들을 먼저 찾기 + const tableSearchWidgets = regularComponents.filter( + (c) => (c as any).componentId === "table-search-widget", + ); - // 디버그: 모든 컴포넌트 타입 확인 - console.log("🔍 전체 컴포넌트 타입:", regularComponents.map(c => ({ - id: c.id, - type: c.type, - componentType: (c as any).componentType, - componentId: (c as any).componentId, - }))); - - // 🆕 조건부 컨테이너들을 찾기 - const conditionalContainers = regularComponents.filter( - (c) => (c as any).componentId === "conditional-container" || (c as any).componentType === "conditional-container" - ); - - console.log("🔍 조건부 컨테이너 발견:", conditionalContainers.map(c => ({ - id: c.id, - y: c.position.y, - size: c.size, - }))); + // 디버그: 모든 컴포넌트 타입 확인 + console.log( + "🔍 전체 컴포넌트 타입:", + regularComponents.map((c) => ({ + id: c.id, + type: c.type, + componentType: (c as any).componentType, + componentId: (c as any).componentId, + })), + ); - // TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정 - const adjustedComponents = regularComponents.map((component) => { - const isTableSearchWidget = (component as any).componentId === "table-search-widget"; - const isConditionalContainer = (component as any).componentId === "conditional-container"; - - if (isTableSearchWidget || isConditionalContainer) { - // 자기 자신은 조정하지 않음 - return component; - } - - let totalHeightAdjustment = 0; - - // TableSearchWidget 높이 조정 - for (const widget of tableSearchWidgets) { - const isBelow = component.position.y > widget.position.y; - const heightDiff = getHeightDiff(screenId, widget.id); - - if (isBelow && heightDiff > 0) { - totalHeightAdjustment += heightDiff; + // 🆕 조건부 컨테이너들을 찾기 + const conditionalContainers = regularComponents.filter( + (c) => + (c as any).componentId === "conditional-container" || + (c as any).componentType === "conditional-container", + ); + + console.log( + "🔍 조건부 컨테이너 발견:", + conditionalContainers.map((c) => ({ + id: c.id, + y: c.position.y, + size: c.size, + })), + ); + + // TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정 + const adjustedComponents = regularComponents.map((component) => { + const isTableSearchWidget = (component as any).componentId === "table-search-widget"; + const isConditionalContainer = (component as any).componentId === "conditional-container"; + + if (isTableSearchWidget || isConditionalContainer) { + // 자기 자신은 조정하지 않음 + return component; } - } - - // 🆕 조건부 컨테이너 높이 조정 - for (const container of conditionalContainers) { - const isBelow = component.position.y > container.position.y; - const actualHeight = conditionalContainerHeights[container.id]; - const originalHeight = container.size?.height || 200; - const heightDiff = actualHeight ? (actualHeight - originalHeight) : 0; - - console.log(`🔍 높이 조정 체크:`, { - componentId: component.id, - componentY: component.position.y, - containerY: container.position.y, - isBelow, - actualHeight, - originalHeight, - heightDiff, - containerId: container.id, - containerSize: container.size, - }); - - if (isBelow && heightDiff > 0) { - totalHeightAdjustment += heightDiff; - console.log(`📐 컴포넌트 ${component.id} 위치 조정: ${heightDiff}px (조건부 컨테이너 ${container.id})`); + + let totalHeightAdjustment = 0; + + // TableSearchWidget 높이 조정 + for (const widget of tableSearchWidgets) { + const isBelow = component.position.y > widget.position.y; + const heightDiff = getHeightDiff(screenId, widget.id); + + if (isBelow && heightDiff > 0) { + totalHeightAdjustment += heightDiff; + } } - } - - if (totalHeightAdjustment > 0) { - return { - ...component, - position: { - ...component.position, - y: component.position.y + totalHeightAdjustment, - }, - }; - } - - return component; - }); - return ( - <> - {/* 일반 컴포넌트들 */} - {adjustedComponents.map((component) => { - // 화면 관리 해상도를 사용하므로 위치 조정 불필요 - return ( - {}} - menuObjid={menuObjid} - screenId={screenId} - tableName={screen?.tableName} - userId={user?.userId} - userName={userName} - companyCode={companyCode} - menuObjid={menuObjid} - selectedRowsData={selectedRowsData} - sortBy={tableSortBy} - sortOrder={tableSortOrder} - columnOrder={tableColumnOrder} - tableDisplayData={tableDisplayData} - onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { - console.log("🔍 화면에서 선택된 행 데이터:", selectedData); - console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder }); - console.log("📊 화면 표시 데이터:", { count: tableDisplayData?.length, firstRow: tableDisplayData?.[0] }); - setSelectedRowsData(selectedData); - setTableSortBy(sortBy); - setTableSortOrder(sortOrder || "asc"); - setTableColumnOrder(columnOrder); - setTableDisplayData(tableDisplayData || []); - }} - 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); - }} - formData={formData} - onFormDataChange={(fieldName, value) => { - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - onHeightChange={(componentId, newHeight) => { - setConditionalContainerHeights((prev) => ({ - ...prev, - [componentId]: newHeight, - })); - }} - > - {/* 자식 컴포넌트들 */} - {(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, - }, - }; + // 🆕 조건부 컨테이너 높이 조정 + for (const container of conditionalContainers) { + const isBelow = component.position.y > container.position.y; + const actualHeight = conditionalContainerHeights[container.id]; + const originalHeight = container.size?.height || 200; + const heightDiff = actualHeight ? actualHeight - originalHeight : 0; - return ( - {}} - menuObjid={menuObjid} - screenId={screenId} - tableName={screen?.tableName} - userId={user?.userId} - userName={userName} - companyCode={companyCode} - menuObjid={menuObjid} - selectedRowsData={selectedRowsData} - sortBy={tableSortBy} - sortOrder={tableSortOrder} - columnOrder={tableColumnOrder} - tableDisplayData={tableDisplayData} - onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { - console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); - console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder }); - console.log("📊 화면 표시 데이터 (자식):", { count: tableDisplayData?.length, firstRow: tableDisplayData?.[0] }); - setSelectedRowsData(selectedData); - setTableSortBy(sortBy); - setTableSortOrder(sortOrder || "asc"); - setTableColumnOrder(columnOrder); - setTableDisplayData(tableDisplayData || []); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - console.log("🔄 테이블 새로고침 요청됨 (자식)"); - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); // 선택 해제 - }} - formData={formData} - onFormDataChange={(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; - - // 🔍 버튼 그룹 설정 확인 - console.log("🔍 버튼 그룹 설정:", { - groupId, - buttonCount: buttons.length, - buttons: buttons.map((b) => ({ - id: b.id, - label: b.label, - x: b.position.x, - y: b.position.y, - })), - groupConfig: { - layoutBehavior: groupConfig.layoutBehavior, - groupDirection: groupConfig.groupDirection, - groupAlign: groupConfig.groupAlign, - groupGap: groupConfig.groupGap, - }, + console.log(`🔍 높이 조정 체크:`, { + componentId: component.id, + componentY: component.position.y, + containerY: container.position.y, + isBelow, + actualHeight, + originalHeight, + heightDiff, + containerId: container.id, + containerSize: container.size, }); - // 🔧 수정: 그룹 컨테이너는 첫 번째 버튼 위치를 기준으로 하되, - // 각 버튼의 상대 위치는 원래 위치를 유지 - const firstButtonPosition = { - x: buttons[0].position.x, - y: buttons[0].position.y, - z: buttons[0].position.z || 2, - }; - - // 버튼 그룹 위치에도 widthOffset 적용 - const adjustedGroupPosition = { - ...firstButtonPosition, - x: firstButtonPosition.x + widthOffset, - }; - - // 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산 - 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); + if (isBelow && heightDiff > 0) { + totalHeightAdjustment += heightDiff; + console.log( + `📐 컴포넌트 ${component.id} 위치 조정: ${heightDiff}px (조건부 컨테이너 ${container.id})`, + ); } + } - return ( -
- 0) { + return { + ...component, + position: { + ...component.position, + y: component.position.y + totalHeightAdjustment, + }, + }; + } + + return component; + }); + + return ( + <> + {/* 일반 컴포넌트들 */} + {adjustedComponents.map((component) => { + // 화면 관리 해상도를 사용하므로 위치 조정 불필요 + return ( + { - // 🔧 각 버튼의 상대 위치 = 버튼의 원래 위치 - 첫 번째 버튼 위치 - const relativeButton = { - ...button, - position: { - x: button.position.x - firstButtonPosition.x, - y: button.position.y - firstButtonPosition.y, - z: button.position.z || 1, - }, - }; + onClick={() => {}} + menuObjid={menuObjid} + screenId={screenId} + tableName={screen?.tableName} + userId={user?.userId} + userName={userName} + companyCode={companyCode} + menuObjid={menuObjid} + selectedRowsData={selectedRowsData} + sortBy={tableSortBy} + sortOrder={tableSortOrder} + columnOrder={tableColumnOrder} + tableDisplayData={tableDisplayData} + onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { + console.log("🔍 화면에서 선택된 행 데이터:", selectedData); + console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder }); + console.log("📊 화면 표시 데이터:", { + count: tableDisplayData?.length, + firstRow: tableDisplayData?.[0], + }); + setSelectedRowsData(selectedData); + setTableSortBy(sortBy); + setTableSortOrder(sortOrder || "asc"); + setTableColumnOrder(columnOrder); + setTableDisplayData(tableDisplayData || []); + }} + 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); + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} + onHeightChange={(componentId, newHeight) => { + setConditionalContainerHeights((prev) => ({ + ...prev, + [componentId]: newHeight, + })); + }} + > + {/* 자식 컴포넌트들 */} + {(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 ( -
-
- {}} + onClick={() => {}} + menuObjid={menuObjid} screenId={screenId} tableName={screen?.tableName} userId={user?.userId} userName={userName} companyCode={companyCode} - tableDisplayData={tableDisplayData} + menuObjid={menuObjid} selectedRowsData={selectedRowsData} sortBy={tableSortBy} sortOrder={tableSortOrder} columnOrder={tableColumnOrder} - onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => { + tableDisplayData={tableDisplayData} + onSelectedRowsChange={( + _, + selectedData, + sortBy, + sortOrder, + columnOrder, + tableDisplayData, + ) => { + console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); + console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder }); + console.log("📊 화면 표시 데이터 (자식):", { + count: tableDisplayData?.length, + firstRow: tableDisplayData?.[0], + }); setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); setTableColumnOrder(columnOrder); - }} - flowSelectedData={flowSelectedData} - flowSelectedStepId={flowSelectedStepId} - onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { - setFlowSelectedData(selectedData); - setFlowSelectedStepId(stepId); + setTableDisplayData(tableDisplayData || []); }} refreshKey={tableRefreshKey} onRefresh={() => { + console.log("🔄 테이블 새로고침 요청됨 (자식)"); setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); - }} - flowRefreshKey={flowRefreshKey} - onFlowRefresh={() => { - setFlowRefreshKey((prev) => prev + 1); - setFlowSelectedData([]); - setFlowSelectedStepId(null); + setSelectedRowsData([]); // 선택 해제 }} + formData={formData} 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; - }); - }} - /> + {/* 🆕 플로우 버튼 그룹들 */} + {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; + + // 🔍 버튼 그룹 설정 확인 + console.log("🔍 버튼 그룹 설정:", { + groupId, + buttonCount: buttons.length, + buttons: buttons.map((b) => ({ + id: b.id, + label: b.label, + x: b.position.x, + y: b.position.y, + })), + groupConfig: { + layoutBehavior: groupConfig.layoutBehavior, + groupDirection: groupConfig.groupDirection, + groupAlign: groupConfig.groupAlign, + groupGap: groupConfig.groupGap, + }, + }); + + // 🔧 수정: 그룹 컨테이너는 첫 번째 버튼 위치를 기준으로 하되, + // 각 버튼의 상대 위치는 원래 위치를 유지 + const firstButtonPosition = { + x: buttons[0].position.x, + y: buttons[0].position.y, + z: buttons[0].position.z || 2, + }; + + // 버튼 그룹 위치에도 widthOffset 적용 + const adjustedGroupPosition = { + ...firstButtonPosition, + x: firstButtonPosition.x + widthOffset, + }; + + // 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산 + 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: button.position.x - firstButtonPosition.x, + y: button.position.y - firstButtonPosition.y, + z: button.position.z || 1, + }, + }; + + return ( +
+
+ {}} + screenId={screenId} + tableName={screen?.tableName} + userId={user?.userId} + userName={userName} + companyCode={companyCode} + tableDisplayData={tableDisplayData} + selectedRowsData={selectedRowsData} + sortBy={tableSortBy} + sortOrder={tableSortOrder} + columnOrder={tableColumnOrder} + onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => { + setSelectedRowsData(selectedData); + setTableSortBy(sortBy); + setTableSortOrder(sortOrder || "asc"); + setTableColumnOrder(columnOrder); + }} + 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/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index e87dc73d..8394cd6d 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -470,7 +470,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { {/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
-
{children}
+ {children}
diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 8c93380e..4c3e6506 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -346,6 +346,14 @@ export const InteractiveScreenViewer: React.FC = ( // 실제 사용 가능한 위젯 렌더링 const renderInteractiveWidget = (comp: ComponentData) => { + console.log("🎯 renderInteractiveWidget 호출:", { + type: comp.type, + id: comp.id, + componentId: (comp as any).componentId, + hasComponentConfig: !!(comp as any).componentConfig, + componentConfig: (comp as any).componentConfig, + }); + // 데이터 테이블 컴포넌트 처리 if (comp.type === "datatable") { return ( @@ -397,6 +405,39 @@ export const InteractiveScreenViewer: React.FC = ( ); } + // 탭 컴포넌트 처리 + const componentType = (comp as any).componentType || (comp as any).componentId; + if (comp.type === "tabs" || (comp.type === "component" && componentType === "tabs-widget")) { + const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget; + + // componentConfig에서 탭 정보 추출 + const tabsConfig = comp.componentConfig || {}; + const tabsComponent = { + ...comp, + type: "tabs" as const, + tabs: tabsConfig.tabs || [], + defaultTab: tabsConfig.defaultTab, + orientation: tabsConfig.orientation || "horizontal", + variant: tabsConfig.variant || "default", + allowCloseable: tabsConfig.allowCloseable || false, + persistSelection: tabsConfig.persistSelection || false, + }; + + console.log("🔍 탭 컴포넌트 렌더링:", { + originalType: comp.type, + componentType, + componentId: (comp as any).componentId, + tabs: tabsComponent.tabs, + tabsConfig, + }); + + return ( +
+ +
+ ); + } + const { widgetType, label, placeholder, required, readonly, columnName } = comp; const fieldName = columnName || comp.id; const currentValue = formData[fieldName] || ""; diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 81f8dc21..0270ffa8 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -554,6 +554,73 @@ export const RealtimePreviewDynamic: React.FC = ({ ); })()} + {/* 탭 컴포넌트 타입 */} + {(type === "tabs" || (type === "component" && ((component as any).componentType === "tabs-widget" || (component as any).componentId === "tabs-widget"))) && + (() => { + console.log("🎯 탭 컴포넌트 조건 충족:", { + type, + componentType: (component as any).componentType, + componentId: (component as any).componentId, + isDesignMode, + }); + + if (isDesignMode) { + // 디자인 모드: 미리보기 표시 + const tabsComponent = component as any; + const tabs = tabsComponent.componentConfig?.tabs || tabsComponent.tabs || []; + + return ( +
+
+
+ +
+

탭 컴포넌트

+

+ {tabs.length > 0 + ? `${tabs.length}개의 탭 (실제 화면에서 표시됩니다)` + : "탭이 없습니다. 설정 패널에서 탭을 추가하세요"} +

+ {tabs.length > 0 && ( +
+ {tabs.map((tab: any, index: number) => ( + + {tab.label || `탭 ${index + 1}`} + {tab.screenName && ( + + ({tab.screenName}) + + )} + + ))} +
+ )} +
+
+ ); + } else { + // 실제 화면: TabsWidget 렌더링 + const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget; + const tabsConfig = (component as any).componentConfig || {}; + const tabsComponent = { + ...component, + type: "tabs" as const, + tabs: tabsConfig.tabs || [], + defaultTab: tabsConfig.defaultTab, + orientation: tabsConfig.orientation || "horizontal", + variant: tabsConfig.variant || "default", + allowCloseable: tabsConfig.allowCloseable || false, + persistSelection: tabsConfig.persistSelection || false, + }; + + return ( +
+ +
+ ); + } + })()} + {/* 그룹 타입 */} {type === "group" && (
diff --git a/frontend/components/screen/ResponsiveDesignerContainer.tsx b/frontend/components/screen/ResponsiveDesignerContainer.tsx index 57f55b14..392e59ce 100644 --- a/frontend/components/screen/ResponsiveDesignerContainer.tsx +++ b/frontend/components/screen/ResponsiveDesignerContainer.tsx @@ -35,9 +35,9 @@ export const ResponsiveDesignerContainer: React.FC { const handleKeyDown = (e: KeyboardEvent) => { - // 입력 필드에서는 스페이스바 무시 + // 입력 필드에서는 스페이스바 무시 (activeElement로 정확하게 체크) + const activeElement = document.activeElement; + if ( + activeElement instanceof HTMLInputElement || + activeElement instanceof HTMLTextAreaElement || + activeElement?.getAttribute('contenteditable') === 'true' || + activeElement?.getAttribute('role') === 'textbox' + ) { + return; + } + + // e.target도 함께 체크 (이중 방어) if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return; } @@ -997,6 +1008,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; const handleKeyUp = (e: KeyboardEvent) => { + // 입력 필드에서는 스페이스바 무시 + const activeElement = document.activeElement; + if ( + activeElement instanceof HTMLInputElement || + activeElement instanceof HTMLTextAreaElement || + activeElement?.getAttribute('contenteditable') === 'true' || + activeElement?.getAttribute('role') === 'textbox' + ) { + return; + } + if (e.code === "Space") { e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단 setIsPanMode(false); diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index 51a09164..d2d3e367 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -35,7 +35,10 @@ import { DialogDescription, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { cn } from "@/lib/utils"; +import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash, Check, ChevronsUpDown } from "lucide-react"; import { ScreenDefinition } from "@/types/screen"; import { screenApi } from "@/lib/api/screen"; import CreateScreenModal from "./CreateScreenModal"; @@ -127,8 +130,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr isActive: "Y", tableName: "", }); - const [tables, setTables] = useState([]); + const [tables, setTables] = useState>([]); const [loadingTables, setLoadingTables] = useState(false); + const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // 미리보기 관련 상태 const [previewDialogOpen, setPreviewDialogOpen] = useState(false); @@ -279,9 +283,12 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr const { tableManagementApi } = await import("@/lib/api/tableManagement"); const response = await tableManagementApi.getTableList(); if (response.success && response.data) { - // tableName만 추출 (camelCase) - const tableNames = response.data.map((table: any) => table.tableName); - setTables(tableNames); + // tableName과 displayName 매핑 (백엔드에서 displayName으로 라벨을 반환함) + const tableList = response.data.map((table: any) => ({ + tableName: table.tableName, + tableLabel: table.displayName || table.tableName, + })); + setTables(tableList); } } catch (error) { console.error("테이블 목록 조회 실패:", error); @@ -297,6 +304,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr // 화면 정보 업데이트 API 호출 await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData); + // 선택된 테이블의 라벨 찾기 + const selectedTable = tables.find((t) => t.tableName === editFormData.tableName); + const tableLabel = selectedTable?.tableLabel || editFormData.tableName; + // 목록에서 해당 화면 정보 업데이트 setScreens((prev) => prev.map((s) => @@ -304,6 +315,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr ? { ...s, screenName: editFormData.screenName, + tableName: editFormData.tableName, + tableLabel: tableLabel, description: editFormData.description, isActive: editFormData.isActive, } @@ -1202,22 +1215,62 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
- + + + + + + + + + + 테이블을 찾을 수 없습니다. + + + {tables.map((table) => ( + { + setEditFormData({ ...editFormData, tableName: table.tableName }); + setTableComboboxOpen(false); + }} + className="text-xs sm:text-sm" + > + +
+ {table.tableLabel} + {table.tableName} +
+
+ ))} +
+
+
+
+
diff --git a/frontend/components/screen/StyleEditor.tsx b/frontend/components/screen/StyleEditor.tsx index 054875f7..2e5dec7e 100644 --- a/frontend/components/screen/StyleEditor.tsx +++ b/frontend/components/screen/StyleEditor.tsx @@ -49,7 +49,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.borderWidth || ""} onChange={(e) => handleStyleChange("borderWidth", e.target.value)} className="h-6 w-full px-2 py-0 text-xs" - style={{ fontSize: "12px" }} />
@@ -60,20 +59,20 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.borderStyle || "solid"} onValueChange={(value) => handleStyleChange("borderStyle", value)} > - + - + 실선 - + 파선 - + 점선 - + 없음 @@ -93,7 +92,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.borderColor || "#000000"} onChange={(e) => handleStyleChange("borderColor", e.target.value)} className="h-6 w-12 p-1" - style={{ fontSize: "12px" }} + className="text-xs" /> handleStyleChange("borderColor", e.target.value)} placeholder="#000000" className="h-6 flex-1 text-xs" - style={{ fontSize: "12px" }} />
@@ -116,7 +114,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.borderRadius || ""} onChange={(e) => handleStyleChange("borderRadius", e.target.value)} className="h-6 w-full px-2 py-0 text-xs" - style={{ fontSize: "12px" }} />
@@ -142,7 +139,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.backgroundColor || "#ffffff"} onChange={(e) => handleStyleChange("backgroundColor", e.target.value)} className="h-6 w-12 p-1" - style={{ fontSize: "12px" }} + className="text-xs" /> handleStyleChange("backgroundColor", e.target.value)} placeholder="#ffffff" className="h-6 flex-1 text-xs" - style={{ fontSize: "12px" }} />
@@ -166,7 +162,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.backgroundImage || ""} onChange={(e) => handleStyleChange("backgroundImage", e.target.value)} className="h-6 w-full px-2 py-0 text-xs" - style={{ fontSize: "12px" }} />

위젯 배경 꾸미기용 (고급 사용자 전용) @@ -195,7 +190,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.color || "#000000"} onChange={(e) => handleStyleChange("color", e.target.value)} className="h-6 w-12 p-1" - style={{ fontSize: "12px" }} + className="text-xs" /> handleStyleChange("color", e.target.value)} placeholder="#000000" className="h-6 flex-1 text-xs" - style={{ fontSize: "12px" }} />

@@ -218,7 +212,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.fontSize || ""} onChange={(e) => handleStyleChange("fontSize", e.target.value)} className="h-6 w-full px-2 py-0 text-xs" - style={{ fontSize: "12px" }} /> @@ -232,29 +225,29 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.fontWeight || "normal"} onValueChange={(value) => handleStyleChange("fontWeight", value)} > - + - + 보통 - + 굵게 - + 100 - + 400 - + 500 - + 600 - + 700 @@ -268,20 +261,20 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.textAlign || "left"} onValueChange={(value) => handleStyleChange("textAlign", value)} > - + - + 왼쪽 - + 가운데 - + 오른쪽 - + 양쪽 diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 1ef8fee4..7af50458 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -509,7 +509,7 @@ export const ButtonConfigPanel: React.FC = ({ role="combobox" aria-expanded={modalScreenOpen} className="h-6 w-full justify-between px-2 py-0" - style={{ fontSize: "12px" }} + className="text-xs" disabled={screensLoading} > {config.action?.targetScreenId @@ -900,7 +900,7 @@ export const ButtonConfigPanel: React.FC = ({ role="combobox" aria-expanded={modalScreenOpen} className="h-6 w-full justify-between px-2 py-0" - style={{ fontSize: "12px" }} + className="text-xs" disabled={screensLoading} > {config.action?.targetScreenId @@ -978,7 +978,7 @@ export const ButtonConfigPanel: React.FC = ({ role="combobox" aria-expanded={modalScreenOpen} className="h-6 w-full justify-between px-2 py-0" - style={{ fontSize: "12px" }} + className="text-xs" disabled={screensLoading} > {config.action?.targetScreenId @@ -1132,7 +1132,7 @@ export const ButtonConfigPanel: React.FC = ({ role="combobox" aria-expanded={modalScreenOpen} className="h-6 w-full justify-between px-2 py-0" - style={{ fontSize: "12px" }} + className="text-xs" disabled={screensLoading} > {config.action?.targetScreenId @@ -1286,7 +1286,6 @@ export const ButtonConfigPanel: React.FC = ({ role="combobox" aria-expanded={displayColumnOpen} className="mt-2 h-8 w-full justify-between text-xs" - style={{ fontSize: "12px" }} disabled={columnsLoading || tableColumns.length === 0} > {columnsLoading @@ -1301,9 +1300,9 @@ export const ButtonConfigPanel: React.FC = ({ - + - + 컬럼을 찾을 수 없습니다. @@ -1316,7 +1315,6 @@ export const ButtonConfigPanel: React.FC = ({ setDisplayColumnOpen(false); }} className="text-xs" - style={{ fontSize: "12px" }} > = ({ role="combobox" aria-expanded={navScreenOpen} className="h-6 w-full justify-between px-2 py-0" - style={{ fontSize: "12px" }} + className="text-xs" disabled={screensLoading} > {config.action?.targetScreenId @@ -1424,7 +1422,6 @@ export const ButtonConfigPanel: React.FC = ({ onUpdateProperty("componentConfig.action.targetUrl", newValue); }} className="h-6 w-full px-2 py-0 text-xs" - style={{ fontSize: "12px" }} />

URL을 입력하면 화면 선택보다 우선 적용됩니다

diff --git a/frontend/components/screen/config-panels/CheckboxConfigPanel.tsx b/frontend/components/screen/config-panels/CheckboxConfigPanel.tsx index 85a78b7a..bff983dc 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" style={{ fontSize: "12px" }} + className="text-xs" /> @@ -187,7 +187,7 @@ export const CheckboxConfigPanel: React.FC = ({ value={localConfig.checkedValue || ""} onChange={(e) => updateConfig("checkedValue", e.target.value)} placeholder="Y" - className="text-xs" style={{ fontSize: "12px" }} + className="text-xs" />
@@ -199,7 +199,7 @@ export const CheckboxConfigPanel: React.FC = ({ value={localConfig.uncheckedValue || ""} onChange={(e) => updateConfig("uncheckedValue", e.target.value)} placeholder="N" - className="text-xs" style={{ fontSize: "12px" }} + className="text-xs" />
@@ -232,7 +232,7 @@ export const CheckboxConfigPanel: React.FC = ({ value={localConfig.groupLabel || ""} onChange={(e) => updateConfig("groupLabel", e.target.value)} placeholder="체크박스 그룹 제목" - className="text-xs" style={{ fontSize: "12px" }} + className="text-xs" /> @@ -244,19 +244,19 @@ export const CheckboxConfigPanel: React.FC = ({ value={newOptionLabel} onChange={(e) => setNewOptionLabel(e.target.value)} placeholder="라벨" - className="flex-1 text-xs" style={{ fontSize: "12px" }} + className="flex-1 text-xs" /> setNewOptionValue(e.target.value)} placeholder="값" - className="flex-1 text-xs" style={{ fontSize: "12px" }} + className="flex-1 text-xs" /> @@ -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" style={{ fontSize: "12px" }} + className="flex-1 text-xs" /> updateOption(index, "value", e.target.value)} placeholder="값" - className="flex-1 text-xs" style={{ fontSize: "12px" }} + className="flex-1 text-xs" /> = ({ disabled={localConfig.readonly} required={localConfig.required} defaultChecked={localConfig.defaultChecked} - className="text-xs" style={{ fontSize: "12px" }} + className="text-xs" />