diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index e7b6e806..c2036cbd 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -1068,43 +1068,131 @@ export class ScreenManagementService { [tableName] ); - // column_labels 테이블에서 입력타입 정보 조회 (있는 경우) - const webTypeInfo = await query<{ + // 🆕 table_type_columns에서 입력타입 정보 조회 (회사별만, fallback 없음) + // 멀티테넌시: 각 회사는 자신의 설정만 사용, 최고관리자 설정은 별도 관리 + console.log(`🔍 [getTableColumns] 시작: table=${tableName}, company=${companyCode}`); + + const typeInfo = await query<{ column_name: string; input_type: string | null; - column_label: string | null; detail_settings: any; }>( - `SELECT column_name, input_type, column_label, detail_settings + `SELECT column_name, input_type, detail_settings + FROM table_type_columns + WHERE table_name = $1 + AND company_code = $2 + ORDER BY id DESC`, // 최신 레코드 우선 (중복 방지) + [tableName, companyCode] + ); + + console.log(`📊 [getTableColumns] typeInfo 조회 완료: ${typeInfo.length}개`); + const currencyCodeType = typeInfo.find(t => t.column_name === 'currency_code'); + if (currencyCodeType) { + console.log(`💰 [getTableColumns] currency_code 발견:`, currencyCodeType); + } else { + console.log(`⚠️ [getTableColumns] currency_code 없음`); + } + + // column_labels 테이블에서 라벨 정보 조회 (우선순위 2) + const labelInfo = await query<{ + column_name: string; + column_label: string | null; + }>( + `SELECT column_name, column_label FROM column_labels WHERE table_name = $1`, [tableName] ); - // 컬럼 정보 매핑 - return columns.map((column: any) => { - const webTypeData = webTypeInfo.find( - (wt) => wt.column_name === column.column_name - ); + // 🆕 category_column_mapping에서 코드 카테고리 정보 조회 + const categoryInfo = await query<{ + physical_column_name: string; + logical_column_name: string; + }>( + `SELECT physical_column_name, logical_column_name + FROM category_column_mapping + WHERE table_name = $1 + AND company_code = $2`, + [tableName, companyCode] + ); - return { + // 컬럼 정보 매핑 + const columnMap = new Map(); + + // 먼저 information_schema에서 가져온 컬럼들로 기본 맵 생성 + columns.forEach((column: any) => { + columnMap.set(column.column_name, { tableName: tableName, columnName: column.column_name, - columnLabel: - webTypeData?.column_label || - this.getColumnLabel(column.column_name), dataType: column.data_type, - webType: - (webTypeData?.input_type as WebType) || - this.inferWebType(column.data_type), isNullable: column.is_nullable, columnDefault: column.column_default || undefined, characterMaximumLength: column.character_maximum_length || undefined, numericPrecision: column.numeric_precision || undefined, numericScale: column.numeric_scale || undefined, - detailSettings: webTypeData?.detail_settings || undefined, - }; + }); }); + + console.log(`🗺️ [getTableColumns] 기본 columnMap 생성: ${columnMap.size}개`); + + // table_type_columns에서 input_type 추가 (중복 시 최신 것만) + const addedTypes = new Set(); + typeInfo.forEach((type) => { + const colName = type.column_name; + if (!addedTypes.has(colName) && columnMap.has(colName)) { + const col = columnMap.get(colName); + col.inputType = type.input_type; + col.webType = type.input_type; // webType도 동일하게 설정 + col.detailSettings = type.detail_settings; + addedTypes.add(colName); + + if (colName === 'currency_code') { + console.log(`✅ [getTableColumns] currency_code inputType 설정됨: ${type.input_type}`); + } + } + }); + + console.log(`🏷️ [getTableColumns] inputType 추가 완료: ${addedTypes.size}개`); + + // column_labels에서 라벨 추가 + labelInfo.forEach((label) => { + const col = columnMap.get(label.column_name); + if (col) { + col.columnLabel = label.column_label || this.getColumnLabel(label.column_name); + } + }); + + // category_column_mapping에서 코드 카테고리 추가 + categoryInfo.forEach((cat) => { + const col = columnMap.get(cat.physical_column_name); + if (col) { + col.codeCategory = cat.logical_column_name; + } + }); + + // 최종 결과 생성 + const result = Array.from(columnMap.values()).map((col) => ({ + ...col, + // 기본값 설정 + columnLabel: col.columnLabel || this.getColumnLabel(col.columnName), + inputType: col.inputType || this.inferWebType(col.dataType), + webType: col.webType || this.inferWebType(col.dataType), + detailSettings: col.detailSettings || undefined, + codeCategory: col.codeCategory || undefined, + })); + + // 디버깅: currency_code의 최종 inputType 확인 + const currencyCodeResult = result.find(r => r.columnName === 'currency_code'); + if (currencyCodeResult) { + console.log(`🎯 [getTableColumns] 최종 currency_code:`, { + inputType: currencyCodeResult.inputType, + webType: currencyCodeResult.webType, + dataType: currencyCodeResult.dataType + }); + } + + console.log(`✅ [getTableColumns] 반환: ${result.length}개 컬럼`); + return result; } catch (error) { console.error("테이블 컬럼 조회 실패:", error); throw new Error("테이블 컬럼 정보를 조회할 수 없습니다."); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 8ce3c9d4..e9104bd6 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -165,6 +165,10 @@ export class TableManagementService { const offset = (page - 1) * size; // 🔥 company_code가 있으면 table_type_columns 조인하여 회사별 inputType 가져오기 + console.log( + `🔍 [getColumnList] 시작: table=${tableName}, company=${companyCode}` + ); + const rawColumns = companyCode ? await query( `SELECT @@ -174,6 +178,8 @@ export class TableManagementService { c.data_type as "dbType", COALESCE(cl.input_type, 'text') as "webType", COALESCE(ttc.input_type, cl.input_type, 'direct') as "inputType", + ttc.input_type as "ttc_input_type", + cl.input_type as "cl_input_type", COALESCE(ttc.detail_settings::text, cl.detail_settings, '') as "detailSettings", COALESCE(cl.description, '') as "description", c.is_nullable as "isNullable", @@ -250,6 +256,22 @@ export class TableManagementService { [tableName, size, offset] ); + // 디버깅: currency_code 확인 + const currencyCol = rawColumns.find( + (col: any) => col.columnName === "currency_code" + ); + if (currencyCol) { + console.log(`🎯 [getColumnList] currency_code 원본 쿼리 결과:`, { + columnName: currencyCol.columnName, + inputType: currencyCol.inputType, + ttc_input_type: currencyCol.ttc_input_type, + cl_input_type: currencyCol.cl_input_type, + webType: currencyCol.webType, + }); + } else { + console.log(`⚠️ [getColumnList] currency_code가 rawColumns에 없음`); + } + // 🆕 category_column_mapping 조회 const tableExistsResult = await query( `SELECT EXISTS ( diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 4bbb913e..3cb55fc1 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -119,7 +119,19 @@ export const ScreenModal: React.FC = ({ className }) => { // 전역 모달 이벤트 리스너 useEffect(() => { const handleOpenModal = (event: CustomEvent) => { - const { screenId, title, description, size } = event.detail; + const { screenId, title, description, size, urlParams } = event.detail; + + // 🆕 URL 파라미터가 있으면 현재 URL에 추가 + if (urlParams && typeof window !== "undefined") { + const currentUrl = new URL(window.location.href); + Object.entries(urlParams).forEach(([key, value]) => { + currentUrl.searchParams.set(key, String(value)); + }); + // pushState로 URL 변경 (페이지 새로고침 없이) + window.history.pushState({}, "", currentUrl.toString()); + console.log("✅ URL 파라미터 추가:", urlParams); + } + setModalState({ isOpen: true, screenId, @@ -130,6 +142,15 @@ export const ScreenModal: React.FC = ({ className }) => { }; const handleCloseModal = () => { + // 🆕 URL 파라미터 제거 + if (typeof window !== "undefined") { + const currentUrl = new URL(window.location.href); + // dataSourceId 파라미터 제거 + currentUrl.searchParams.delete("dataSourceId"); + window.history.pushState({}, "", currentUrl.toString()); + console.log("🧹 URL 파라미터 제거"); + } + setModalState({ isOpen: false, screenId: null, diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index a2ab1522..ba27c94e 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -316,6 +316,7 @@ export const InteractiveScreenViewerDynamic: React.FC { console.log("🔍 테이블에서 선택된 행 데이터:", selectedData); diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 7bbc4dbe..5288108f 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -419,17 +419,22 @@ export const ButtonConfigPanel: React.FC = ({

- + { onUpdateProperty("componentConfig.action.dataSourceId", e.target.value); }} /> +

+ ✨ 비워두면 같은 화면의 TableList를 자동으로 감지합니다 +

- TableList에서 데이터를 저장한 ID와 동일해야 합니다 (보통 테이블명) + 직접 지정하려면 테이블명을 입력하세요 (예: item_info)

diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 183581ca..1ae66e0b 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -52,6 +52,9 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps { // 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용) flowSelectedData?: any[]; flowSelectedStepId?: number | null; + + // 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용) + allComponents?: any[]; } /** @@ -88,6 +91,7 @@ export const ButtonPrimaryComponent: React.FC = ({ selectedRowsData, flowSelectedData, flowSelectedStepId, + allComponents, // 🆕 같은 화면의 모든 컴포넌트 ...props }) => { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 @@ -409,6 +413,8 @@ export const ButtonPrimaryComponent: React.FC = ({ sortOrder, // 🆕 정렬 방향 columnOrder, // 🆕 컬럼 순서 tableDisplayData, // 🆕 화면에 표시된 데이터 + // 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용) + allComponents, // 플로우 선택된 데이터 정보 추가 flowSelectedData, flowSelectedStepId, diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryConfigPanel.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryConfigPanel.tsx deleted file mode 100644 index bbe1faed..00000000 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryConfigPanel.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"use client"; - -import React from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { ButtonPrimaryConfig } from "./types"; - -export interface ButtonPrimaryConfigPanelProps { - config: ButtonPrimaryConfig; - onChange: (config: Partial) => void; -} - -/** - * ButtonPrimary 설정 패널 - * 컴포넌트의 설정값들을 편집할 수 있는 UI 제공 - */ -export const ButtonPrimaryConfigPanel: React.FC = ({ config, onChange }) => { - const handleChange = (key: keyof ButtonPrimaryConfig, value: any) => { - onChange({ [key]: value }); - }; - - return ( -
-
button-primary 설정
- - {/* 버튼 관련 설정 */} -
- - handleChange("text", e.target.value)} /> -
- -
- - -
- - {/* 공통 설정 */} -
- - handleChange("disabled", checked)} - /> -
- -
- - handleChange("required", checked)} - /> -
- -
- - handleChange("readonly", checked)} - /> -
-
- ); -}; diff --git a/frontend/lib/registry/components/button-primary/index.ts b/frontend/lib/registry/components/button-primary/index.ts index f9e19a14..7710c338 100644 --- a/frontend/lib/registry/components/button-primary/index.ts +++ b/frontend/lib/registry/components/button-primary/index.ts @@ -5,7 +5,6 @@ import { createComponentDefinition } from "../../utils/createComponentDefinition import { ComponentCategory } from "@/types/component"; import type { WebType } from "@/types/screen"; import { ButtonPrimaryWrapper } from "./ButtonPrimaryComponent"; -import { ButtonPrimaryConfigPanel } from "./ButtonPrimaryConfigPanel"; import { ButtonPrimaryConfig } from "./types"; /** @@ -31,7 +30,7 @@ export const ButtonPrimaryDefinition = createComponentDefinition({ }, }, defaultSize: { width: 120, height: 40 }, - configPanel: ButtonPrimaryConfigPanel, + configPanel: undefined, // 상세 설정 패널(ButtonConfigPanel)이 대신 사용됨 icon: "MousePointer", tags: ["버튼", "액션", "클릭"], version: "1.0.0", diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx index b4f2e38d..23238eb2 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState, useEffect, useMemo, useCallback } from "react"; +import { useSearchParams } from "next/navigation"; import { ComponentRendererProps } from "@/types/component"; import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition } from "./types"; import { useModalDataStore, ModalDataItem } from "@/stores/modalDataStore"; @@ -12,6 +13,7 @@ import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { X } from "lucide-react"; +import { commonCodeApi } from "@/lib/api/commonCode"; import { cn } from "@/lib/utils"; export interface SelectedItemsDetailInputComponentProps extends ComponentRendererProps { @@ -38,6 +40,10 @@ export const SelectedItemsDetailInputComponent: React.FC { + // 🆕 URL 파라미터에서 dataSourceId 읽기 + const searchParams = useSearchParams(); + const urlDataSourceId = searchParams?.get("dataSourceId") || undefined; + // 컴포넌트 설정 const componentConfig = useMemo(() => ({ dataSourceId: component.id || "default", @@ -52,13 +58,22 @@ export const SelectedItemsDetailInputComponent: React.FC 컴포넌트 설정 > component.id const dataSourceId = useMemo( - () => componentConfig.dataSourceId || component.id || "default", - [componentConfig.dataSourceId, component.id] + () => urlDataSourceId || componentConfig.dataSourceId || component.id || "default", + [urlDataSourceId, componentConfig.dataSourceId, component.id] ); + // 디버깅 로그 + useEffect(() => { + console.log("📍 [SelectedItemsDetailInput] dataSourceId 결정:", { + urlDataSourceId, + configDataSourceId: componentConfig.dataSourceId, + componentId: component.id, + finalDataSourceId: dataSourceId, + }); + }, [urlDataSourceId, componentConfig.dataSourceId, component.id, dataSourceId]); + // 전체 레지스트리를 가져와서 컴포넌트 내부에서 필터링 (캐싱 문제 회피) const dataRegistry = useModalDataStore((state) => state.dataRegistry); const modalData = useMemo( @@ -70,6 +85,79 @@ export const SelectedItemsDetailInputComponent: React.FC([]); + + // 🆕 코드 카테고리별 옵션 캐싱 + const [codeOptions, setCodeOptions] = useState>>({}); + + // 🆕 필드에 codeCategory가 있으면 자동으로 옵션 로드 + useEffect(() => { + const loadCodeOptions = async () => { + // 🆕 code/category 타입 필드 + codeCategory가 있는 필드 모두 처리 + const codeFields = componentConfig.additionalFields?.filter( + (field) => field.inputType === "code" || field.inputType === "category" + ); + + if (!codeFields || codeFields.length === 0) return; + + const newOptions: Record> = { ...codeOptions }; + + // 🆕 대상 테이블의 컬럼 메타데이터에서 codeCategory 가져오기 + const targetTable = componentConfig.targetTable; + let targetTableColumns: any[] = []; + + if (targetTable) { + try { + const { tableTypeApi } = await import("@/lib/api/screen"); + const columnsResponse = await tableTypeApi.getColumns(targetTable); + targetTableColumns = columnsResponse || []; + } catch (error) { + console.error("❌ 대상 테이블 컬럼 조회 실패:", error); + } + } + + for (const field of codeFields) { + // 이미 codeCategory가 있으면 사용 + let codeCategory = field.codeCategory; + + // 🆕 codeCategory가 없으면 대상 테이블 컬럼에서 찾기 + if (!codeCategory && targetTableColumns.length > 0) { + const columnMeta = targetTableColumns.find( + (col: any) => (col.columnName || col.column_name) === field.name + ); + if (columnMeta) { + codeCategory = columnMeta.codeCategory || columnMeta.code_category; + console.log(`🔍 필드 "${field.name}"의 codeCategory를 메타데이터에서 찾음:`, codeCategory); + } + } + + if (!codeCategory) { + console.warn(`⚠️ 필드 "${field.name}"의 codeCategory를 찾을 수 없습니다`); + continue; + } + + // 이미 로드된 옵션이면 스킵 + if (newOptions[codeCategory]) continue; + + try { + const response = await commonCodeApi.options.getOptions(codeCategory); + if (response.success && response.data) { + newOptions[codeCategory] = response.data.map((opt) => ({ + label: opt.label, + value: opt.value, + })); + console.log(`✅ 코드 옵션 로드 완료: ${codeCategory}`, newOptions[codeCategory]); + } + } catch (error) { + console.error(`❌ 코드 옵션 로드 실패: ${codeCategory}`, error); + } + } + + setCodeOptions(newOptions); + }; + + loadCodeOptions(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [componentConfig.additionalFields, componentConfig.targetTable]); // 모달 데이터가 변경되면 로컬 상태 업데이트 useEffect(() => { @@ -151,7 +239,130 @@ export const SelectedItemsDetailInputComponent: React.FC handleFieldChange(item.id, field.name, e.target.value)} + maxLength={field.validation?.maxLength} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> + ); + + case "number": + case "int": + case "integer": + case "bigint": + case "decimal": + case "numeric": + return ( + handleFieldChange(item.id, field.name, e.target.value)} + min={field.validation?.min} + max={field.validation?.max} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> + ); + + case "date": + case "timestamp": + case "datetime": + return ( + handleFieldChange(item.id, field.name, e.target.value)} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> + ); + + case "checkbox": + case "boolean": + case "bool": + return ( + handleFieldChange(item.id, field.name, checked)} + disabled={componentConfig.disabled || componentConfig.readonly} + /> + ); + + case "textarea": + return ( +