diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 6958199b..fa763690 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -303,8 +303,10 @@ export default function ScreenViewPage() { style={{ transform: `scale(${scale})`, transformOrigin: "top left", - width: containerWidth > 0 ? `${containerWidth / scale}px` : "100%", - minWidth: containerWidth > 0 ? `${containerWidth / scale}px` : "100%", + width: `${screenWidth}px`, + height: `${screenHeight}px`, + minWidth: `${screenWidth}px`, + minHeight: `${screenHeight}px`, }} > {/* 최상위 컴포넌트들 렌더링 */} @@ -312,26 +314,9 @@ export default function ScreenViewPage() { // 🆕 플로우 버튼 그룹 감지 및 처리 const topLevelComponents = layout.components.filter((component) => !component.parentId); - // 버튼은 scale에 맞춰 위치만 조정하면 됨 (scale = 1.0이면 그대로, scale < 1.0이면 왼쪽으로) - // 하지만 x=0 컴포넌트는 width: 100%로 확장되므로, 그만큼 버튼을 오른쪽으로 이동 - const leftmostComponent = topLevelComponents.find((c) => c.position.x === 0); - let widthOffset = 0; - - if (leftmostComponent && containerWidth > 0) { - const originalWidth = leftmostComponent.size?.width || screenWidth; - const actualWidth = containerWidth / scale; - widthOffset = Math.max(0, actualWidth - originalWidth); - - console.log("📊 widthOffset 계산:", { - containerWidth, - scale, - screenWidth, - originalWidth, - actualWidth, - widthOffset, - leftmostType: leftmostComponent.type, - }); - } + // 화면 관리에서 설정한 해상도를 사용하므로 widthOffset 계산 불필요 + // 모든 컴포넌트는 원본 위치 그대로 사용 + const widthOffset = 0; const buttonGroups: Record = {}; const processedButtonIds = new Set(); @@ -393,37 +378,11 @@ export default function ScreenViewPage() { <> {/* 일반 컴포넌트들 */} {regularComponents.map((component) => { - // 버튼인 경우 위치 조정 (테이블이 늘어난 만큼 오른쪽으로 이동) - const isButton = - (component.type === "component" && - ["button-primary", "button-secondary"].includes((component as any).componentType)) || - (component.type === "widget" && (component as any).widgetType === "button"); - - const adjustedComponent = - isButton && widthOffset > 0 - ? { - ...component, - position: { - ...component.position, - x: component.position.x + widthOffset, - }, - } - : component; - - // 버튼일 경우 로그 출력 - if (isButton) { - console.log("🔘 버튼 위치 조정:", { - label: component.label, - originalX: component.position.x, - adjustedX: component.position.x + widthOffset, - widthOffset, - }); - } - + // 화면 관리 해상도를 사용하므로 위치 조정 불필요 return ( {}} diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 5703753a..50423460 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { ResizableDialog, ResizableDialogContent, @@ -8,6 +8,8 @@ import { ResizableDialogTitle, ResizableDialogDescription, } from "@/components/ui/resizable-dialog"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; import { screenApi } from "@/lib/api/screen"; import { ComponentData } from "@/types/screen"; @@ -52,6 +54,19 @@ export const ScreenModal: React.FC = ({ className }) => { // 폼 데이터 상태 추가 const [formData, setFormData] = useState>({}); + + // 연속 등록 모드 상태 (localStorage에 저장하여 리렌더링에 영향받지 않도록) + const continuousModeRef = useRef(false); + const [, setForceUpdate] = useState(0); // 강제 리렌더링용 (값은 사용하지 않음) + + // localStorage에서 연속 모드 상태 복원 + useEffect(() => { + const savedMode = localStorage.getItem("screenModal_continuousMode"); + if (savedMode === "true") { + continuousModeRef.current = true; + // console.log("🔄 연속 모드 복원: true"); + } + }, []); // 화면의 실제 크기 계산 함수 const calculateScreenDimensions = (components: ComponentData[]) => { @@ -124,16 +139,43 @@ export const ScreenModal: React.FC = ({ className }) => { }); setScreenData(null); setFormData({}); + continuousModeRef.current = false; + localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 저장 + // console.log("🔄 연속 모드 초기화: false"); + }; + + // 저장 성공 이벤트 처리 (연속 등록 모드 지원) + const handleSaveSuccess = () => { + const isContinuousMode = continuousModeRef.current; + // console.log("💾 저장 성공 이벤트 수신"); + // console.log("📌 현재 연속 모드 상태 (ref):", isContinuousMode); + // console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode")); + + if (isContinuousMode) { + // 연속 모드: 폼만 초기화하고 모달은 유지 + // console.log("✅ 연속 모드 활성화 - 폼만 초기화"); + + // 폼만 초기화 (연속 모드 상태는 localStorage에 저장되어 있으므로 유지됨) + setFormData({}); + + toast.success("저장되었습니다. 계속 입력하세요."); + } else { + // 일반 모드: 모달 닫기 + // console.log("❌ 일반 모드 - 모달 닫기"); + handleCloseModal(); + } }; window.addEventListener("openScreenModal", handleOpenModal as EventListener); window.addEventListener("closeSaveModal", handleCloseModal); + window.addEventListener("saveSuccessInModal", handleSaveSuccess); return () => { window.removeEventListener("openScreenModal", handleOpenModal as EventListener); window.removeEventListener("closeSaveModal", handleCloseModal); + window.removeEventListener("saveSuccessInModal", handleSaveSuccess); }; - }, []); + }, []); // 의존성 제거 (ref 사용으로 최신 상태 참조) // 화면 데이터 로딩 useEffect(() => { @@ -160,8 +202,25 @@ export const ScreenModal: React.FC = ({ className }) => { if (screenInfo && layoutData) { const components = layoutData.components || []; - // 화면의 실제 크기 계산 - const dimensions = calculateScreenDimensions(components); + // 화면 관리에서 설정한 해상도 사용 (우선순위) + const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution; + + let dimensions; + if (screenResolution && screenResolution.width && screenResolution.height) { + // 화면 관리에서 설정한 해상도 사용 + dimensions = { + width: screenResolution.width, + height: screenResolution.height, + offsetX: 0, + offsetY: 0, + }; + console.log("✅ 화면 관리 해상도 사용:", dimensions); + } else { + // 해상도 정보가 없으면 자동 계산 + dimensions = calculateScreenDimensions(components); + console.log("⚠️ 자동 계산된 크기 사용:", dimensions); + } + setScreenDimensions(dimensions); setScreenData({ @@ -235,39 +294,39 @@ export const ScreenModal: React.FC = ({ className }) => { // 1순위: screenId (가장 안정적) if (modalState.screenId) { newModalId = `screen-modal-${modalState.screenId}`; - console.log("🔑 ScreenModal modalId 생성:", { - method: "screenId", - screenId: modalState.screenId, - result: newModalId, - }); + // console.log("🔑 ScreenModal modalId 생성:", { + // method: "screenId", + // screenId: modalState.screenId, + // result: newModalId, + // }); } // 2순위: 테이블명 else if (screenData?.screenInfo?.tableName) { newModalId = `screen-modal-table-${screenData.screenInfo.tableName}`; - console.log("🔑 ScreenModal modalId 생성:", { - method: "tableName", - tableName: screenData.screenInfo.tableName, - result: newModalId, - }); + // console.log("🔑 ScreenModal modalId 생성:", { + // method: "tableName", + // tableName: screenData.screenInfo.tableName, + // result: newModalId, + // }); } // 3순위: 화면명 else if (screenData?.screenInfo?.screenName) { newModalId = `screen-modal-name-${screenData.screenInfo.screenName}`; - console.log("🔑 ScreenModal modalId 생성:", { - method: "screenName", - screenName: screenData.screenInfo.screenName, - result: newModalId, - }); + // console.log("🔑 ScreenModal modalId 생성:", { + // method: "screenName", + // screenName: screenData.screenInfo.screenName, + // result: newModalId, + // }); } // 4순위: 제목 else if (modalState.title) { - const titleId = modalState.title.replace(/\s+/g, '-'); + const titleId = modalState.title.replace(/\s+/g, "-"); newModalId = `screen-modal-title-${titleId}`; - console.log("🔑 ScreenModal modalId 생성:", { - method: "title", - title: modalState.title, - result: newModalId, - }); + // console.log("🔑 ScreenModal modalId 생성:", { + // method: "title", + // title: modalState.title, + // result: newModalId, + // }); } if (newModalId) { @@ -325,11 +384,12 @@ export const ScreenModal: React.FC = ({ className }) => { }} > {screenData.components.map((component) => { - // 컴포넌트 위치를 offset만큼 조정 (왼쪽 상단으로 정렬) + // 화면 관리 해상도를 사용하는 경우 offset 조정 불필요 const offsetX = screenDimensions?.offsetX || 0; const offsetY = screenDimensions?.offsetY || 0; - const adjustedComponent = { + // offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시) + const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : { ...component, position: { ...component.position, @@ -345,14 +405,14 @@ export const ScreenModal: React.FC = ({ className }) => { allComponents={screenData.components} formData={formData} onFormDataChange={(fieldName, value) => { - console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`); - console.log("📋 현재 formData:", formData); + // console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`); + // console.log("📋 현재 formData:", formData); setFormData((prev) => { const newFormData = { ...prev, [fieldName]: value, }; - console.log("📝 ScreenModal 업데이트된 formData:", newFormData); + // console.log("📝 ScreenModal 업데이트된 formData:", newFormData); return newFormData; }); }} @@ -370,6 +430,29 @@ export const ScreenModal: React.FC = ({ className }) => { )} + + {/* 연속 등록 모드 체크박스 */} +
+
+ { + const isChecked = checked === true; + continuousModeRef.current = isChecked; + localStorage.setItem("screenModal_continuousMode", String(isChecked)); + setForceUpdate((prev) => prev + 1); // 체크박스 UI 업데이트를 위한 강제 리렌더링 + // console.log("🔄 연속 모드 변경:", isChecked); + }} + /> + +
+
); diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index c8e53cdd..7ad86f9c 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -127,6 +127,11 @@ export const InteractiveScreenViewerDynamic: React.FC {label || "버튼"} diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 48761e42..bf76ba1d 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -241,7 +241,17 @@ export const RealtimePreviewDynamic: React.FC = ({ return "100%"; } - // 3순위: size.width (픽셀) + // 3순위: size.width (픽셀) - 버튼의 경우 항상 픽셀 사용 + if (isButtonComponent && size?.width) { + const width = `${size.width}px`; + console.log("🔘 [getWidth] 버튼 픽셀 사용:", { + componentId: id, + label: component.label, + width, + }); + return width; + } + if (component.componentConfig?.type === "table-list") { const width = `${Math.max(size?.width || 120, 120)}px`; console.log("📏 [getWidth] 픽셀 사용 (table-list):", { diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index 98fb5b0f..650e3e9b 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -1267,21 +1267,6 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr zIndex: component.position.z || 1, }; - // 버튼 타입일 때 디버깅 (widget 타입 또는 component 타입 모두 체크) - if ( - (component.type === "widget" && (component as any).widgetType === "button") || - (component.type === "component" && (component as any).componentType?.includes("button")) - ) { - console.log("🔘 ScreenList 버튼 외부 div 스타일:", { - id: component.id, - label: component.label, - position: component.position, - size: component.size, - componentStyle: component.style, - appliedStyle: style, - }); - } - return style; })()} > diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index fc86ceb7..43b08177 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -344,8 +344,8 @@ export const ButtonPrimaryComponent: React.FC = ({ window.dispatchEvent(new CustomEvent("closeEditModal")); } - // ScreenModal은 항상 닫기 - window.dispatchEvent(new CustomEvent("closeSaveModal")); + // ScreenModal은 연속 등록 모드를 지원하므로 saveSuccessInModal 이벤트 발생 + window.dispatchEvent(new CustomEvent("saveSuccessInModal")); }, 100); } } diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index d53798d1..6647fe4f 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -1107,16 +1107,16 @@ export const TableListComponent: React.FC = ({ const mapping = categoryMappings[column.columnName]; const categoryData = mapping?.[String(value)]; - console.log(`🎨 [카테고리 배지] ${column.columnName}:`, { - value, - stringValue: String(value), - mapping, - categoryData, - hasMapping: !!mapping, - hasCategoryData: !!categoryData, - allCategoryMappings: categoryMappings, // 전체 매핑 확인 - categoryMappingsKeys: Object.keys(categoryMappings), - }); + // console.log(`🎨 [카테고리 배지] ${column.columnName}:`, { + // value, + // stringValue: String(value), + // mapping, + // categoryData, + // hasMapping: !!mapping, + // hasCategoryData: !!categoryData, + // allCategoryMappings: categoryMappings, // 전체 매핑 확인 + // categoryMappingsKeys: Object.keys(categoryMappings), + // }); // 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상 const displayLabel = categoryData?.label || String(value); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 546319c6..4ee47277 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -252,19 +252,19 @@ export class ButtonActionExecutor { const writerValue = context.userId; const companyCodeValue = context.companyCode || ""; - console.log("👤 [buttonActions] 사용자 정보:", { - userId: context.userId, - userName: context.userName, - companyCode: context.companyCode, // ✅ 회사 코드 - formDataWriter: formData.writer, // ✅ 폼에서 입력한 writer 값 - formDataCompanyCode: formData.company_code, // ✅ 폼에서 입력한 company_code 값 - defaultWriterValue: writerValue, - companyCodeValue, // ✅ 최종 회사 코드 값 - }); + // console.log("👤 [buttonActions] 사용자 정보:", { + // userId: context.userId, + // userName: context.userName, + // companyCode: context.companyCode, // ✅ 회사 코드 + // formDataWriter: formData.writer, // ✅ 폼에서 입력한 writer 값 + // formDataCompanyCode: formData.company_code, // ✅ 폼에서 입력한 company_code 값 + // defaultWriterValue: writerValue, + // companyCodeValue, // ✅ 최종 회사 코드 값 + // }); // 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가) - console.log("🔍 채번 규칙 할당 체크 시작"); - console.log("📦 현재 formData:", JSON.stringify(formData, null, 2)); + // console.log("🔍 채번 규칙 할당 체크 시작"); + // console.log("📦 현재 formData:", JSON.stringify(formData, null, 2)); const fieldsWithNumbering: Record = {}; @@ -273,26 +273,26 @@ export class ButtonActionExecutor { if (key.endsWith("_numberingRuleId") && value) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumbering[fieldName] = value as string; - console.log(`🎯 발견: ${fieldName} → 규칙 ${value}`); + // console.log(`🎯 발견: ${fieldName} → 규칙 ${value}`); } } - console.log("📋 채번 규칙이 설정된 필드:", fieldsWithNumbering); - console.log("📊 필드 개수:", Object.keys(fieldsWithNumbering).length); + // console.log("📋 채번 규칙이 설정된 필드:", fieldsWithNumbering); + // console.log("📊 필드 개수:", Object.keys(fieldsWithNumbering).length); // 각 필드에 대해 실제 코드 할당 for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { try { - console.log(`🎫 ${fieldName} 필드에 채번 규칙 ${ruleId} 할당 시작`); + // console.log(`🎫 ${fieldName} 필드에 채번 규칙 ${ruleId} 할당 시작`); const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); const response = await allocateNumberingCode(ruleId); - console.log(`📡 API 응답 (${fieldName}):`, response); + // console.log(`📡 API 응답 (${fieldName}):`, response); if (response.success && response.data) { const generatedCode = response.data.generatedCode; formData[fieldName] = generatedCode; - console.log(`✅ ${fieldName} = ${generatedCode} (할당 완료)`); + // console.log(`✅ ${fieldName} = ${generatedCode} (할당 완료)`); } else { console.error(`❌ 채번 규칙 할당 실패 (${fieldName}):`, response.error); toast.error(`${fieldName} 채번 규칙 할당 실패: ${response.error}`); @@ -303,8 +303,8 @@ export class ButtonActionExecutor { } } - console.log("✅ 채번 규칙 할당 완료"); - console.log("📦 최종 formData:", JSON.stringify(formData, null, 2)); + // console.log("✅ 채번 규칙 할당 완료"); + // console.log("📦 최종 formData:", JSON.stringify(formData, null, 2)); const dataWithUserInfo = { ...formData, @@ -345,8 +345,9 @@ export class ButtonActionExecutor { context.onRefresh?.(); context.onFlowRefresh?.(); - // 저장 성공 후 EditModal 닫기 이벤트 발생 - window.dispatchEvent(new CustomEvent("closeEditModal")); + // 저장 성공 후 이벤트 발생 + window.dispatchEvent(new CustomEvent("closeEditModal")); // EditModal 닫기 + window.dispatchEvent(new CustomEvent("saveSuccessInModal")); // ScreenModal 연속 등록 모드 처리 return true; } catch (error) { @@ -932,45 +933,88 @@ export class ButtonActionExecutor { console.log("📋 단일 항목 복사:", rowData); console.log("📋 원본 데이터 키 목록:", Object.keys(rowData)); - // 품목코드 필드 초기화 (여러 가능한 필드명 확인) + // 복사 시 제거할 필드들 const copiedData = { ...rowData }; + const fieldsToRemove = [ + // ID 필드 (새 레코드 생성) + "id", + "ID", + // 날짜 필드 (자동 생성) + "created_date", + "createdDate", + "updated_date", + "updatedDate", + "created_at", + "createdAt", + "updated_at", + "updatedAt", + "reg_date", + "regDate", + "mod_date", + "modDate", + ]; + + // 제거할 필드 삭제 + fieldsToRemove.forEach((field) => { + if (copiedData[field] !== undefined) { + delete copiedData[field]; + console.log(`🗑️ 필드 제거: ${field}`); + } + }); + + // 품목코드 필드 초기화 (여러 가능한 필드명 확인) const itemCodeFields = [ "item_code", "itemCode", "item_no", "itemNo", + "item_number", + "itemNumber", "품목코드", "품번", "code", ]; - // 품목코드 필드를 찾아서 초기화 + // 품목코드 필드를 찾아서 무조건 공백으로 초기화 let resetFieldName = ""; for (const field of itemCodeFields) { if (copiedData[field] !== undefined) { - // 품목코드 필드를 빈 문자열로 초기화 - // (저장 시점에 채번 규칙이 자동으로 적용됨) - copiedData[field] = ""; - - // 채번 규칙 ID도 함께 저장 (formData에 있을 경우) + const originalValue = copiedData[field]; const ruleIdKey = `${field}_numberingRuleId`; - if (rowData[ruleIdKey]) { + const hasNumberingRule = rowData[ruleIdKey] !== undefined && rowData[ruleIdKey] !== null && rowData[ruleIdKey] !== ""; + + // 품목코드를 무조건 공백으로 초기화 + copiedData[field] = ""; + + // 채번 규칙 ID가 있으면 복사 (저장 시 자동 생성) + if (hasNumberingRule) { copiedData[ruleIdKey] = rowData[ruleIdKey]; + console.log(`✅ 품목코드 초기화 (채번 규칙 있음): ${field} (기존값: ${originalValue})`); console.log(`📋 채번 규칙 ID 복사: ${ruleIdKey} = ${rowData[ruleIdKey]}`); + } else { + console.log(`✅ 품목코드 초기화 (수동 입력 필요): ${field} (기존값: ${originalValue})`); } - + resetFieldName = field; - console.log(`✅ 품목코드 필드 초기화: ${field} (기존값: ${rowData[field]})`); break; } } + // 작성자 정보를 현재 사용자로 변경 + const writerFields = ["writer", "creator", "reg_user", "regUser", "created_by", "createdBy"]; + writerFields.forEach((field) => { + if (copiedData[field] !== undefined && context.userId) { + copiedData[field] = context.userId; + console.log(`👤 작성자 변경: ${field} = ${context.userId}`); + } + }); + if (resetFieldName) { - toast.success(`품목코드(${resetFieldName})가 초기화되었습니다. 저장 시 자동으로 새 코드가 생성됩니다.`); + toast.success(`복사본이 생성되었습니다. 품목코드는 저장 시 자동으로 생성됩니다.`); } else { console.warn("⚠️ 품목코드 필드를 찾을 수 없습니다. 전체 데이터를 복사합니다."); console.warn("⚠️ 사용 가능한 필드:", Object.keys(copiedData)); - toast.info("복사본이 생성됩니다. (품목코드 필드를 찾을 수 없음)"); + toast.info("복사본이 생성됩니다."); } console.log("📋 복사된 데이터:", copiedData);