From 13fe9c97feb172283fcb541ef702b1cd568fe14d Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 26 Nov 2025 14:44:49 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/ScreenList.tsx | 21 +- .../config-panels/CheckboxConfigPanel.tsx | 71 ++- .../config-panels/EntityConfigPanel.tsx | 73 ++- .../screen/config-panels/RadioConfigPanel.tsx | 55 +- .../config-panels/SelectConfigPanel.tsx | 49 +- .../screen/templates/NumberingRuleTemplate.ts | 1 + .../webtypes/config/RepeaterConfigPanel.tsx | 71 ++- frontend/lib/api/tableCategoryValue.ts | 19 + .../SelectedItemsDetailInputConfigPanel.tsx | 594 +++++++++++++----- 동적_테이블_접근_시스템_개선_완료.md | 1 + 10 files changed, 712 insertions(+), 243 deletions(-) diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index d2d3e367..eff55312 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -47,6 +47,7 @@ import dynamic from "next/dynamic"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicWebTypeRenderer } from "@/lib/registry"; import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils"; +import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // InteractiveScreenViewer를 동적으로 import (SSR 비활성화) const InteractiveScreenViewer = dynamic( @@ -1315,15 +1316,16 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr 화면 미리보기 - {screenToPreview?.screenName} -
- {isLoadingPreview ? ( -
-
-
레이아웃 로딩 중...
-
화면 정보를 불러오고 있습니다.
+ +
+ {isLoadingPreview ? ( +
+
+
레이아웃 로딩 중...
+
화면 정보를 불러오고 있습니다.
+
-
- ) : previewLayout && previewLayout.components ? ( + ) : previewLayout && previewLayout.components ? ( (() => { const screenWidth = previewLayout.screenResolution?.width || 1200; const screenHeight = previewLayout.screenResolution?.height || 800; @@ -1536,7 +1538,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
)} -
+ + @@ -1702,30 +1978,40 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {/* 2단계: 카테고리 선택 */} - {(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType && ( -
- - -
- )} + {(() => { + const hasSelectedMenu = !!(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType; + const columns = categoryColumns.discountType || []; + console.log("🎨 [렌더링] 2단계 카테고리 선택", { + hasSelectedMenu, + columns, + columnsCount: columns.length, + categoryColumnsState: categoryColumns + }); + return hasSelectedMenu ? ( +
+ + +
+ ) : null; + })()} {/* 3단계: 값 매핑 */} {(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType && ( @@ -1780,14 +2066,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {/* 반올림 방식 매핑 */} - + setExpandedCategoryMappings(prev => ({ ...prev, roundingType: open }))} + > @@ -1890,14 +2183,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {/* 반올림 단위 매핑 */} - + setExpandedCategoryMappings(prev => ({ ...prev, roundingUnit: open }))} + > @@ -2235,10 +2535,10 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {mapping.targetField - ? targetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel || + ? loadedTargetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel || mapping.targetField : "저장 테이블 컬럼 선택"} @@ -2248,13 +2548,15 @@ export const SelectedItemsDetailInputConfigPanel: React.FC - {targetTableColumns.length === 0 ? ( - 저장 테이블을 먼저 선택하세요 + {!config.targetTable ? ( + 저장 대상 테이블을 먼저 선택하세요 + ) : loadedTargetTableColumns.length === 0 ? ( + 컬럼 로딩 중... ) : ( <> 컬럼을 찾을 수 없습니다. - {targetTableColumns.map((col) => { + {loadedTargetTableColumns.map((col) => { const searchValue = `${col.columnLabel || col.columnName} ${col.columnName} ${col.dataType || ""}`.toLowerCase(); return ( {col.columnLabel || col.columnName} {col.dataType && ( - {col.dataType} + + {col.dataType} + )} @@ -2289,17 +2593,27 @@ export const SelectedItemsDetailInputConfigPanel: React.FC +

+ 현재 화면의 저장 대상 테이블 ({config.targetTable || "미선택"})의 컬럼 +

{/* 기본값 (선택사항) */}
{ - const updated = [...(config.parentDataMapping || [])]; - updated[index] = { ...updated[index], defaultValue: e.target.value }; - handleChange("parentDataMapping", updated); + const newValue = e.target.value; + setLocalMappingInputs(prev => ({ ...prev, [index]: newValue })); + }} + onBlur={() => { + const currentValue = localMappingInputs[index]; + if (currentValue !== undefined) { + const updated = [...(config.parentDataMapping || [])]; + updated[index] = { ...updated[index], defaultValue: currentValue || undefined }; + handleChange("parentDataMapping", updated); + } }} placeholder="값이 없을 때 사용할 기본값" className="h-7 text-xs" @@ -2307,46 +2621,24 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {/* 삭제 버튼 */} - +
+ +
))} - - {(config.parentDataMapping || []).length === 0 && ( -

- 매핑 설정이 없습니다. "추가" 버튼을 클릭하여 설정하세요. -

- )} - - {/* 예시 */} -
-

💡 예시

-
-

매핑 1: 거래처 ID

-

• 소스 테이블: customer_mng

-

• 원본 필드: id → 저장 필드: customer_id

- -

매핑 2: 품목 ID

-

• 소스 테이블: item_info

-

• 원본 필드: id → 저장 필드: item_id

- -

매핑 3: 품목 기준단가

-

• 소스 테이블: item_info

-

• 원본 필드: standard_price → 저장 필드: base_price

-
-
{/* 사용 예시 */} @@ -2363,3 +2655,5 @@ export const SelectedItemsDetailInputConfigPanel: React.FC Date: Wed, 26 Nov 2025 14:58:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/ScreenList.tsx | 192 +++++++----------- .../table-search-widget/TableSearchWidget.tsx | 16 +- 2 files changed, 87 insertions(+), 121 deletions(-) diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index eff55312..8e5ba1d2 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -48,6 +48,8 @@ import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRendere import { DynamicWebTypeRenderer } from "@/lib/registry"; import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; +import { RealtimePreview } from "./RealtimePreviewDynamic"; +import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; // InteractiveScreenViewer를 동적으로 import (SSR 비활성화) const InteractiveScreenViewer = dynamic( @@ -1316,8 +1318,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr 화면 미리보기 - {screenToPreview?.screenName} - -
+ + +
{isLoadingPreview ? (
@@ -1331,10 +1334,24 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr const screenHeight = previewLayout.screenResolution?.height || 800; // 모달 내부 가용 공간 계산 (헤더, 푸터, 패딩 제외) - const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - 100 : 1800; // 95vw - 패딩 + const modalPadding = 100; // 헤더 + 푸터 + 패딩 + const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - modalPadding : 1700; + const availableHeight = typeof window !== "undefined" ? window.innerHeight * 0.95 - modalPadding : 900; - // 가로폭 기준으로 스케일 계산 (가로폭에 맞춤) - const scale = availableWidth / screenWidth; + // 가로/세로 비율을 모두 고려하여 작은 쪽에 맞춤 (화면이 잘리지 않도록) + const scaleX = availableWidth / screenWidth; + const scaleY = availableHeight / screenHeight; + const scale = Math.min(scaleX, scaleY, 1); // 최대 1배율 (확대 방지) + + console.log("📐 미리보기 스케일 계산:", { + screenWidth, + screenHeight, + availableWidth, + availableHeight, + scaleX, + scaleY, + finalScale: scale, + }); return (
- {/* 라벨을 외부에 별도로 렌더링 */} - {shouldShowLabel && ( -
- {labelText} - {component.required && *} -
- )} + {}} + screenId={screenToPreview!.screenId} + tableName={screenToPreview?.tableName} + formData={previewFormData} + onFormDataChange={(fieldName, value) => { + setPreviewFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); + }} + > + {/* 자식 컴포넌트들 */} + {(component.type === "group" || + component.type === "container" || + component.type === "area") && + previewLayout.components + .filter((child: any) => child.parentId === component.id) + .map((child: any) => { + // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 + const relativeChildComponent = { + ...child, + position: { + x: child.position.x - component.position.x, + y: child.position.y - component.position.y, + z: child.position.z || 1, + }, + }; - {/* 실제 컴포넌트 */} -
{ - const style = { - position: "absolute" as const, - left: `${component.position.x}px`, - top: `${component.position.y}px`, - width: component.style?.width || `${component.size.width}px`, - height: component.style?.height || `${component.size.height}px`, - zIndex: component.position.z || 1, - }; - - return style; - })()} - > - {/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */} - {component.type !== "widget" ? ( - { - setPreviewFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); - }} - screenId={screenToPreview!.screenId} - tableName={screenToPreview?.tableName} - /> - ) : ( - { - // 유틸리티 함수로 파일 컴포넌트 감지 - if (isFileComponent(component)) { - return "file"; - } - // 다른 컴포넌트는 유틸리티 함수로 webType 결정 - return getComponentWebType(component) || "text"; - })()} - config={component.webTypeConfig} - props={{ - component: component, - value: previewFormData[component.columnName || component.id] || "", - onChange: (value: any) => { - const fieldName = component.columnName || component.id; - setPreviewFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); - }, - onFormDataChange: (fieldName, value) => { - setPreviewFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); - }, - isInteractive: true, - formData: previewFormData, - readonly: component.readonly, - required: component.required, - placeholder: component.placeholder, - className: "w-full h-full", - }} - /> - )} -
-
+ return ( + {}} + screenId={screenToPreview!.screenId} + tableName={screenToPreview?.tableName} + formData={previewFormData} + onFormDataChange={(fieldName, value) => { + setPreviewFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); + }} + /> + ); + })} + ); })}
@@ -1538,8 +1501,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
)} -
-
+ + +