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 ab72faa3..63ec2210 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -1253,21 +1253,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/components/screen/config-panels/ButtonConfigPanel-fixed.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel-fixed.tsx index a70a0633..dc991519 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel-fixed.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel-fixed.tsx @@ -242,6 +242,7 @@ export const ButtonConfigPanel: React.FC = ({ component, 취소 삭제 수정 + 복사 (품목코드 초기화) 추가 검색 초기화 @@ -386,6 +387,71 @@ export const ButtonConfigPanel: React.FC = ({ component, + +
+
+ + setModalSearchTerm(e.target.value)} + className="border-0 p-0 focus-visible:ring-0" + /> +
+
+ {(() => { + const filteredScreens = filterScreens(modalSearchTerm); + if (screensLoading) { + return
화면 목록을 불러오는 중...
; + } + if (filteredScreens.length === 0) { + return
검색 결과가 없습니다.
; + } + return filteredScreens.map((screen, index) => ( +
{ + onUpdateProperty("componentConfig.action.targetScreenId", screen.id); + setModalScreenOpen(false); + setModalSearchTerm(""); + }} + > + {screen.name} +
+ )); + })()} +
+
+
+ + + + )} + + {/* 복사 액션 설정 */} + {localSelects.actionType === "copy" && ( +
+

복사 설정 (품목코드 자동 초기화)

+ +
+ + + + +
@@ -434,12 +500,12 @@ export const ButtonConfigPanel: React.FC = ({ component,

- 선택된 데이터가 이 폼 화면에 자동으로 로드되어 수정할 수 있습니다 + 선택된 데이터가 복사되며, 품목코드는 자동으로 초기화됩니다

- + setModalSearchTerm(e.target.value)} + className="border-0 p-0 focus-visible:ring-0" + /> +
+
+ {(() => { + const filteredScreens = filterScreens(modalSearchTerm); + if (screensLoading) { + return
화면 목록을 불러오는 중...
; + } + if (filteredScreens.length === 0) { + return
검색 결과가 없습니다.
; + } + return filteredScreens.map((screen, index) => ( +
{ + onUpdateProperty("componentConfig.action.targetScreenId", screen.id); + setModalScreenOpen(false); + setModalSearchTerm(""); + }} + > + +
+ {screen.name} + {screen.description && {screen.description}} +
+
+ )); + })()} +
+
+
+
+

+ 선택된 데이터가 복사되며, 품목코드는 자동으로 초기화됩니다 +

+
+ +
+ + +
+ + {(component.componentConfig?.action?.editMode || "modal") === "modal" && ( + <> +
+ + { + const newValue = e.target.value; + setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue })); + onUpdateProperty("componentConfig.action.editModalTitle", newValue); + onUpdateProperty("webTypeConfig.editModalTitle", newValue); + }} + /> +

비워두면 기본 제목이 표시됩니다

+
+ +
+ + { + const newValue = e.target.value; + setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue })); + onUpdateProperty("componentConfig.action.editModalDescription", newValue); + onUpdateProperty("webTypeConfig.editModalDescription", newValue); + }} + /> +

비워두면 설명이 표시되지 않습니다

+
+ +
+ + +
+ + )} +
+ )} + {/* 테이블 이력 보기 액션 설정 */} {(component.componentConfig?.action?.type || "save") === "view_table_history" && (
diff --git a/frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx index afa181f8..e3226a10 100644 --- a/frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx @@ -188,6 +188,7 @@ export const ButtonDataflowConfigPanel: React.FC save: "저장", delete: "삭제", edit: "수정", + copy: "복사", add: "추가", search: "검색", reset: "초기화", 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 db08b470..30756d09 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -35,6 +35,11 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Label } from "@/components/ui/label"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; import { SingleTableWithSticky } from "./SingleTableWithSticky"; @@ -274,7 +279,6 @@ export const TableListComponent: React.FC = ({ const [visibleFilterColumns, setVisibleFilterColumns] = useState>(new Set()); // 그룹 설정 관련 상태 - const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false); const [groupByColumns, setGroupByColumns] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); @@ -1106,16 +1110,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); @@ -1284,17 +1288,14 @@ export const TableListComponent: React.FC = ({ })); }, [visibleColumns, visibleFilterColumns, columnLabels]); - // 그룹 설정 저장 - const saveGroupSettings = useCallback(() => { + // 그룹 설정 자동 저장 (localStorage) + useEffect(() => { if (!groupSettingKey) return; try { localStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns)); - setIsGroupSettingOpen(false); - toast.success("그룹 설정이 저장되었습니다"); } catch (error) { console.error("그룹 설정 저장 실패:", error); - toast.error("설정 저장에 실패했습니다"); } }, [groupSettingKey, groupByColumns]); @@ -1545,10 +1546,6 @@ export const TableListComponent: React.FC = ({ > - - - 전체 {totalItems.toLocaleString()}개 -
{/* 우측 새로고침 버튼 */} @@ -1610,7 +1607,12 @@ export const TableListComponent: React.FC = ({ onClearFilters={handleClearAdvancedFilters} /> -
+
+ {/* 전체 개수 */} +
+ 전체 {totalItems.toLocaleString()}개 +
+ - + + + + + +
+
+

그룹 설정

+

+ 데이터를 그룹화할 컬럼을 선택하세요 +

+
+ + {/* 컬럼 목록 */} +
+ {visibleColumns + .filter((col) => col.columnName !== "__checkbox__") + .map((col) => ( +
+ toggleGroupColumn(col.columnName)} + /> + +
+ ))} +
+ + {/* 선택된 그룹 안내 */} + {groupByColumns.length > 0 && ( +
+ + {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")} + +
+ )} + + {/* 초기화 버튼 */} + {groupByColumns.length > 0 && ( + + )} +
+
+
@@ -1717,7 +1788,12 @@ export const TableListComponent: React.FC = ({ onClearFilters={handleClearAdvancedFilters} /> -
+
+ {/* 전체 개수 */} +
+ 전체 {totalItems.toLocaleString()}개 +
+ - + + + + + +
+
+

그룹 설정

+

+ 데이터를 그룹화할 컬럼을 선택하세요 +

+
+ + {/* 컬럼 목록 */} +
+ {visibleColumns + .filter((col) => col.columnName !== "__checkbox__") + .map((col) => ( +
+ toggleGroupColumn(col.columnName)} + /> + +
+ ))} +
+ + {/* 선택된 그룹 안내 */} + {groupByColumns.length > 0 && ( +
+ + {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")} + +
+ )} + + {/* 초기화 버튼 */} + {groupByColumns.length > 0 && ( + + )} +
+
+
@@ -2209,68 +2354,6 @@ export const TableListComponent: React.FC = ({ - {/* 그룹 설정 다이얼로그 */} - - - - 그룹 설정 - - 데이터를 그룹화할 컬럼을 선택하세요. 여러 컬럼을 선택하면 계층적으로 그룹화됩니다. - - - -
- {/* 컬럼 목록 */} -
- {visibleColumns - .filter((col) => col.columnName !== "__checkbox__") - .map((col) => ( -
- toggleGroupColumn(col.columnName)} - /> - -
- ))} -
- - {/* 선택된 그룹 안내 */} -
- {groupByColumns.length === 0 ? ( - 그룹화할 컬럼을 선택하세요 - ) : ( - - 선택된 그룹:{" "} - - {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")} - - - )} -
-
- - - - - -
-
- {/* 테이블 옵션 모달 */} = {}; @@ -269,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}`); @@ -299,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, @@ -341,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) { @@ -886,6 +891,197 @@ export class ButtonActionExecutor { window.location.href = editUrl; } + /** + * 복사 액션 처리 (품목코드 초기화) + */ + private static async handleCopy(config: ButtonActionConfig, context: ButtonActionContext): Promise { + try { + const { selectedRowsData, flowSelectedData } = context; + + // 플로우 선택 데이터 우선 사용 + let dataToCopy = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData; + + console.log("📋 handleCopy - 데이터 소스 확인:", { + hasFlowSelectedData: !!(flowSelectedData && flowSelectedData.length > 0), + flowSelectedDataLength: flowSelectedData?.length || 0, + hasSelectedRowsData: !!(selectedRowsData && selectedRowsData.length > 0), + selectedRowsDataLength: selectedRowsData?.length || 0, + dataToCopyLength: dataToCopy?.length || 0, + }); + + // 선택된 데이터가 없는 경우 + if (!dataToCopy || dataToCopy.length === 0) { + toast.error("복사할 항목을 선택해주세요."); + return false; + } + + // 복사 화면이 설정되지 않은 경우 + if (!config.targetScreenId) { + toast.error("복사 폼 화면이 설정되지 않았습니다. 버튼 설정에서 복사 폼 화면을 선택해주세요."); + return false; + } + + console.log(`📋 복사 액션 실행: ${dataToCopy.length}개 항목`, { + dataToCopy, + targetScreenId: config.targetScreenId, + editMode: config.editMode, + }); + + if (dataToCopy.length === 1) { + // 단일 항목 복사 + const rowData = dataToCopy[0]; + 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) { + const originalValue = copiedData[field]; + const ruleIdKey = `${field}_numberingRuleId`; + 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; + 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(`복사본이 생성되었습니다. 품목코드는 저장 시 자동으로 생성됩니다.`); + } else { + console.warn("⚠️ 품목코드 필드를 찾을 수 없습니다. 전체 데이터를 복사합니다."); + console.warn("⚠️ 사용 가능한 필드:", Object.keys(copiedData)); + toast.info("복사본이 생성됩니다."); + } + + console.log("📋 복사된 데이터:", copiedData); + await this.openCopyForm(config, copiedData, context); + } else { + // 다중 항목 복사 - 현재는 단일 복사만 지원 + toast.error("현재 단일 항목 복사만 지원됩니다. 하나의 항목만 선택해주세요."); + return false; + } + + return true; + } catch (error: any) { + console.error("❌ 복사 액션 실행 중 오류:", error); + toast.error(`복사 중 오류가 발생했습니다: ${error.message || "알 수 없는 오류"}`); + return false; + } + } + + /** + * 복사 폼 열기 (단일 항목) + */ + private static async openCopyForm( + config: ButtonActionConfig, + rowData: any, + context: ButtonActionContext, + ): Promise { + try { + const editMode = config.editMode || "modal"; + console.log("📋 openCopyForm 실행:", { editMode, targetScreenId: config.targetScreenId }); + + switch (editMode) { + case "modal": + // 모달로 복사 폼 열기 (편집 모달 재사용) + console.log("📋 모달로 복사 폼 열기"); + await this.openEditModal(config, rowData, context); + break; + + case "navigate": + // 새 페이지로 이동 + console.log("📋 새 페이지로 복사 화면 이동"); + this.navigateToCopyScreen(config, rowData, context); + break; + + default: + // 기본값: 모달 + console.log("📋 기본 모달로 복사 폼 열기"); + this.openEditModal(config, rowData, context); + } + } catch (error: any) { + console.error("❌ openCopyForm 실행 중 오류:", error); + throw error; + } + } + + /** + * 복사 화면으로 네비게이션 + */ + private static navigateToCopyScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void { + const copyUrl = `/screens/${config.targetScreenId}?mode=copy`; + console.log("🔄 복사 화면으로 이동:", copyUrl); + + // 복사할 데이터를 sessionStorage에 저장 + sessionStorage.setItem("copyData", JSON.stringify(rowData)); + + window.location.href = copyUrl; + } + /** * 닫기 액션 처리 */ diff --git a/frontend/types/unified-core.ts b/frontend/types/unified-core.ts index 6f7c2b40..cba1c3f7 100644 --- a/frontend/types/unified-core.ts +++ b/frontend/types/unified-core.ts @@ -55,6 +55,7 @@ export type ButtonActionType = | "cancel" | "delete" | "edit" + | "copy" // 복사 (품목코드 초기화) | "add" // 검색 및 초기화 | "search"