From d609cc89b95a99fb56a6a5c620797d51ad3aa051 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 15 Sep 2025 15:38:48 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=85=80=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=EB=B0=95=EC=8A=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/tableManagementService.ts | 8 + frontend/app/(main)/admin/tableMng/page.tsx | 82 ++- .../screen/InteractiveDataTable.tsx | 83 +++ frontend/components/screen/ScreenDesigner.tsx | 34 +- .../select-basic/SelectBasicComponent.tsx | 677 ++++++++++++++---- .../registry/components/select-basic/types.ts | 16 +- frontend/lib/utils/webTypeMapping.ts | 4 +- frontend/types/screen.ts | 4 + 8 files changed, 758 insertions(+), 150 deletions(-) diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 494c8f63..c5f4d9ff 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -306,6 +306,10 @@ export class TableManagementService { }, }); + // 캐시 무효화 - 해당 테이블의 컬럼 캐시 삭제 + cache.deleteByPattern(`table_columns:${tableName}:`); + cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName)); + logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`); } catch (error) { logger.error( @@ -354,6 +358,10 @@ export class TableManagementService { } }); + // 캐시 무효화 - 해당 테이블의 컬럼 캐시 삭제 + cache.deleteByPattern(`table_columns:${tableName}:`); + cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName)); + logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`); } catch (error) { logger.error( diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 8321b5ed..afdedf09 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -13,6 +13,7 @@ import { toast } from "sonner"; import { useMultiLang } from "@/hooks/useMultiLang"; import { TABLE_MANAGEMENT_KEYS, WEB_TYPE_OPTIONS_WITH_KEYS } from "@/constants/tableManagement"; import { apiClient } from "@/lib/api/client"; +import { commonCodeApi } from "@/lib/api/commonCode"; // 가상화 스크롤링을 위한 간단한 구현 interface TableInfo { @@ -112,14 +113,41 @@ export default function TableManagementPage() { ...tables.map((table) => ({ value: table.tableName, label: table.displayName || table.tableName })), ]; - // 공통 코드 옵션 (예시 - 실제로는 API에서 가져와야 함) + // 공통 코드 카테고리 목록 상태 + const [commonCodeCategories, setCommonCodeCategories] = useState>([]); + + // 공통 코드 옵션 const commonCodeOptions = [ { value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_CODE_PLACEHOLDER, "코드 선택") }, - { value: "USER_STATUS", label: "사용자 상태" }, - { value: "DEPT_TYPE", label: "부서 유형" }, - { value: "PRODUCT_CATEGORY", label: "제품 카테고리" }, + ...commonCodeCategories, ]; + // 공통코드 카테고리 목록 로드 + const loadCommonCodeCategories = async () => { + try { + const response = await commonCodeApi.categories.getList({ isActive: true }); + console.log("🔍 공통코드 카테고리 API 응답:", response); + + if (response.success && response.data) { + console.log("📋 공통코드 카테고리 데이터:", response.data); + + const categories = response.data.map((category) => { + console.log("🏷️ 카테고리 항목:", category); + return { + value: category.category_code, + label: category.category_name || category.category_code, + }; + }); + + console.log("✅ 매핑된 카테고리 옵션:", categories); + setCommonCodeCategories(categories); + } + } catch (error) { + console.error("공통코드 카테고리 로드 실패:", error); + // 에러는 로그만 남기고 사용자에게는 알리지 않음 (선택적 기능) + } + }; + // 테이블 목록 로드 const loadTables = async () => { setLoading(true); @@ -408,6 +436,7 @@ export default function TableManagementPage() { useEffect(() => { loadTables(); + loadCommonCodeCategories(); }, []); // 더 많은 데이터 로드 @@ -568,6 +597,7 @@ export default function TableManagementPage() {
라벨
DB 타입
웹 타입
+
상세 설정
설명
@@ -620,6 +650,50 @@ export default function TableManagementPage() { +
+ {/* 웹 타입이 'code'인 경우 공통코드 선택 */} + {column.webType === "code" && ( + + )} + {/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */} + {column.webType === "entity" && ( + + )} + {/* 다른 웹 타입인 경우 빈 공간 */} + {column.webType !== "code" && column.webType !== "entity" && ( +
-
+ )} +
= ({ const [selectedColumnForFiles, setSelectedColumnForFiles] = useState(null); // 선택된 컬럼 정보 const [linkedFiles, setLinkedFiles] = useState([]); + // 공통코드 관리 상태 + const [codeOptions, setCodeOptions] = useState>>({}); + + // 공통코드 옵션 가져오기 + const loadCodeOptions = useCallback( + async (categoryCode: string) => { + if (codeOptions[categoryCode]) { + return codeOptions[categoryCode]; // 이미 로드된 경우 캐시된 데이터 사용 + } + + try { + const response = await commonCodeApi.options.getOptions(categoryCode); + if (response.success && response.data) { + const options = response.data.map((code) => ({ + value: code.value, + label: code.label, + })); + + setCodeOptions((prev) => ({ + ...prev, + [categoryCode]: options, + })); + + return options; + } + } catch (error) { + console.error(`공통코드 옵션 로드 실패: ${categoryCode}`, error); + } + + return []; + }, + [codeOptions], + ); + // 파일 상태 확인 함수 const checkFileStatus = useCallback( async (rowData: Record) => { @@ -336,6 +371,17 @@ export const InteractiveDataTable: React.FC = ({ [component.columns, tableColumns], ); + // 컬럼의 코드 카테고리 가져오기 + const getColumnCodeCategory = useCallback( + (columnName: string) => { + const column = component.columns.find((col) => col.columnName === columnName); + // webTypeConfig가 CodeTypeConfig인 경우 codeCategory 반환 + const webTypeConfig = column?.webTypeConfig as any; + return webTypeConfig?.codeCategory || column?.codeCategory; + }, + [component.columns], + ); + // 그리드 컬럼 계산 const totalGridColumns = visibleColumns.reduce((sum, col) => sum + (col.gridColumns || 2), 0); @@ -1351,6 +1397,43 @@ export const InteractiveDataTable: React.FC = ({
); + case "code": + // 코드 카테고리에서 코드 옵션 가져오기 + const codeCategory = getColumnCodeCategory(column.columnName); + if (codeCategory) { + const codeOptionsForCategory = codeOptions[codeCategory] || []; + + // 코드 옵션이 없으면 로드 + if (codeOptionsForCategory.length === 0) { + loadCodeOptions(codeCategory); + } + + return ( +
+ + {advancedConfig?.helpText &&

{advancedConfig.helpText}

} +
+ ); + } else { + return ( +
+ + {advancedConfig?.helpText &&

{advancedConfig.helpText}

} +
+ ); + } + case "file": return (
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 2bb8a595..ecffbad6 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -669,6 +669,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", columnDefault: col.columnDefault || col.column_default, characterMaximumLength: col.characterMaximumLength || col.character_maximum_length, + // 코드 카테고리 정보 추가 + codeCategory: col.codeCategory || col.code_category, + codeValue: col.codeValue || col.code_value, })); const tableInfo: TableInfo = { @@ -1753,14 +1756,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; case "code": return { - language: "javascript", - theme: "light", - fontSize: 14, - lineNumbers: true, - wordWrap: false, - readOnly: false, - autoFormat: true, - placeholder: "코드를 입력하세요...", + codeCategory: "", // 기본값, 실제로는 컬럼 정보에서 가져옴 + placeholder: "선택하세요", + options: [], // 기본 빈 배열, 실제로는 API에서 로드 }; case "entity": return { @@ -1808,6 +1806,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD position: { x: relativeX, y: relativeY, z: 1 } as Position, size: { width: defaultWidth, height: 40 }, gridColumns: 1, + // 코드 타입인 경우 코드 카테고리 정보 추가 + ...(column.widgetType === "code" && + column.codeCategory && { + codeCategory: column.codeCategory, + }), style: { labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelFontSize: "12px", @@ -1819,6 +1822,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD type: componentId, // text-input, number-input 등 webType: column.widgetType, // 원본 웹타입 보존 ...getDefaultWebTypeConfig(column.widgetType), + // 코드 타입인 경우 코드 카테고리 정보 추가 + ...(column.widgetType === "code" && + column.codeCategory && { + codeCategory: column.codeCategory, + }), }, }; } else { @@ -1841,6 +1849,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD position: { x, y, z: 1 } as Position, size: { width: defaultWidth, height: 40 }, gridColumns: 1, + // 코드 타입인 경우 코드 카테고리 정보 추가 + ...(column.widgetType === "code" && + column.codeCategory && { + codeCategory: column.codeCategory, + }), style: { labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelFontSize: "12px", @@ -1852,6 +1865,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD type: componentId, // text-input, number-input 등 webType: column.widgetType, // 원본 웹타입 보존 ...getDefaultWebTypeConfig(column.widgetType), + // 코드 타입인 경우 코드 카테고리 정보 추가 + ...(column.widgetType === "code" && + column.codeCategory && { + codeCategory: column.codeCategory, + }), }, }; } diff --git a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx index 3eb63208..fb41d3bc 100644 --- a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx +++ b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx @@ -1,154 +1,571 @@ -"use client"; +import React, { useState, useEffect, useRef } from "react"; +import { commonCodeApi } from "../../../api/commonCode"; +import { tableTypeApi } from "../../../api/screen"; -import React from "react"; -import { ComponentRendererProps } from "@/types/component"; -import { SelectBasicConfig } from "./types"; - -export interface SelectBasicComponentProps extends ComponentRendererProps { - config?: SelectBasicConfig; +interface Option { + value: string; + label: string; } -/** - * SelectBasic 컴포넌트 - * select-basic 컴포넌트입니다 - */ -export const SelectBasicComponent: React.FC = ({ +export interface SelectBasicComponentProps { + component: any; + componentConfig: any; + screenId?: number; + onUpdate?: (field: string, value: any) => void; + isSelected?: boolean; + isDesignMode?: boolean; + className?: string; + style?: React.CSSProperties; + onClick?: () => void; + onDragStart?: () => void; + onDragEnd?: () => void; + [key: string]: any; +} + +// 🚀 전역 상태 관리: 모든 컴포넌트가 공유하는 상태 +interface GlobalState { + tableCategories: Map; // tableName.columnName -> codeCategory + codeOptions: Map; // codeCategory -> options + activeRequests: Map>; // 진행 중인 요청들 + subscribers: Set<() => void>; // 상태 변경 구독자들 +} + +const globalState: GlobalState = { + tableCategories: new Map(), + codeOptions: new Map(), + activeRequests: new Map(), + subscribers: new Set(), +}; + +// 전역 상태 변경 알림 +const notifyStateChange = () => { + globalState.subscribers.forEach((callback) => callback()); +}; + +// 캐시 유효 시간 (5분) +const CACHE_DURATION = 5 * 60 * 1000; + +// 🔧 전역 테이블 코드 카테고리 로딩 (중복 방지) +const loadGlobalTableCodeCategory = async (tableName: string, columnName: string): Promise => { + const key = `${tableName}.${columnName}`; + + // 이미 진행 중인 요청이 있으면 대기 + if (globalState.activeRequests.has(`table_${key}`)) { + try { + await globalState.activeRequests.get(`table_${key}`); + } catch (error) { + console.error(`❌ 테이블 설정 로딩 대기 중 오류:`, error); + } + } + + // 캐시된 값이 있으면 반환 + if (globalState.tableCategories.has(key)) { + const cachedCategory = globalState.tableCategories.get(key); + console.log(`✅ 캐시된 테이블 설정 사용: ${key} -> ${cachedCategory}`); + return cachedCategory || null; + } + + // 새로운 요청 생성 + const request = (async () => { + try { + console.log(`🔍 테이블 코드 카테고리 조회: ${key}`); + const columns = await tableTypeApi.getColumns(tableName); + const targetColumn = columns.find((col) => col.columnName === columnName); + + const codeCategory = + targetColumn?.codeCategory && targetColumn.codeCategory !== "none" ? targetColumn.codeCategory : null; + + // 전역 상태에 저장 + globalState.tableCategories.set(key, codeCategory || ""); + + console.log(`✅ 테이블 설정 조회 완료: ${key} -> ${codeCategory}`); + + // 상태 변경 알림 + notifyStateChange(); + + return codeCategory; + } catch (error) { + console.error(`❌ 테이블 코드 카테고리 조회 실패: ${key}`, error); + return null; + } finally { + globalState.activeRequests.delete(`table_${key}`); + } + })(); + + globalState.activeRequests.set(`table_${key}`, request); + return request; +}; + +// 🔧 전역 코드 옵션 로딩 (중복 방지) +const loadGlobalCodeOptions = async (codeCategory: string): Promise => { + if (!codeCategory || codeCategory === "none") { + return []; + } + + // 이미 진행 중인 요청이 있으면 대기 + if (globalState.activeRequests.has(`code_${codeCategory}`)) { + try { + await globalState.activeRequests.get(`code_${codeCategory}`); + } catch (error) { + console.error(`❌ 코드 옵션 로딩 대기 중 오류:`, error); + } + } + + // 캐시된 값이 유효하면 반환 + const cached = globalState.codeOptions.get(codeCategory); + if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { + console.log(`✅ 캐시된 코드 옵션 사용: ${codeCategory} (${cached.options.length}개)`); + return cached.options; + } + + // 새로운 요청 생성 + const request = (async () => { + try { + console.log(`🔄 코드 옵션 로딩: ${codeCategory}`); + const response = await commonCodeApi.codes.getList(codeCategory, { isActive: true }); + + console.log(`🔍 [API 응답 원본] ${codeCategory}:`, { + response, + success: response.success, + data: response.data, + dataType: typeof response.data, + isArray: Array.isArray(response.data), + dataLength: response.data?.length, + firstItem: response.data?.[0], + }); + + if (response.success && response.data) { + const options = response.data.map((code: any, index: number) => { + console.log(`🔍 [코드 매핑] ${index}:`, { + originalCode: code, + codeKeys: Object.keys(code), + values: Object.values(code), + // 가능한 모든 필드 확인 + code: code.code, + codeName: code.codeName, + name: code.name, + label: code.label, + // 대문자 버전 + CODE: code.CODE, + CODE_NAME: code.CODE_NAME, + NAME: code.NAME, + LABEL: code.LABEL, + // 스네이크 케이스 + code_name: code.code_name, + code_value: code.code_value, + // 기타 가능한 필드들 + value: code.value, + text: code.text, + title: code.title, + description: code.description, + }); + + // 실제 값 찾기 시도 (우선순위 순) + const actualValue = code.code || code.CODE || code.value || code.code_value || `code_${index}`; + const actualLabel = + code.codeName || + code.name || + code.CODE_NAME || + code.NAME || + code.label || + code.LABEL || + code.text || + code.title || + code.description || + actualValue; + + console.log(`✨ [최종 매핑] ${index}:`, { + actualValue, + actualLabel, + hasValue: !!actualValue, + hasLabel: !!actualLabel, + }); + + return { + value: actualValue, + label: actualLabel, + }; + }); + + console.log(`🔍 [최종 옵션 배열] ${codeCategory}:`, { + optionsLength: options.length, + options: options.map((opt, idx) => ({ + index: idx, + value: opt.value, + label: opt.label, + hasLabel: !!opt.label, + hasValue: !!opt.value, + })), + }); + + // 전역 상태에 저장 + globalState.codeOptions.set(codeCategory, { + options, + timestamp: Date.now(), + }); + + console.log(`✅ 코드 옵션 로딩 완료: ${codeCategory} (${options.length}개)`); + + // 상태 변경 알림 + notifyStateChange(); + + return options; + } else { + console.log(`⚠️ 빈 응답: ${codeCategory}`); + return []; + } + } catch (error) { + console.error(`❌ 코드 옵션 로딩 실패: ${codeCategory}`, error); + return []; + } finally { + globalState.activeRequests.delete(`code_${codeCategory}`); + } + })(); + + globalState.activeRequests.set(`code_${codeCategory}`, request); + return request; +}; + +const SelectBasicComponent: React.FC = ({ component, - isDesignMode = false, + componentConfig, + screenId, + onUpdate, isSelected = false, - isInteractive = false, + isDesignMode = false, + className, + style, onClick, onDragStart, onDragEnd, - config, - className, - style, - formData, - onFormDataChange, ...props }) => { - // 컴포넌트 설정 - const componentConfig = { - ...config, - ...component.config, - } as SelectBasicConfig; + const [isOpen, setIsOpen] = useState(false); + const [selectedValue, setSelectedValue] = useState(componentConfig?.value || ""); + const [selectedLabel, setSelectedLabel] = useState(""); + const [codeOptions, setCodeOptions] = useState([]); + const [isLoadingCodes, setIsLoadingCodes] = useState(false); + const [dynamicCodeCategory, setDynamicCodeCategory] = useState(null); + const [globalStateVersion, setGlobalStateVersion] = useState(0); // 전역 상태 변경 감지용 + const selectRef = useRef(null); - // 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외) - const componentStyle: React.CSSProperties = { - width: "100%", - height: "100%", - ...component.style, - ...style, + // 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리 + const codeCategory = dynamicCodeCategory || componentConfig?.codeCategory; + + // 🚀 전역 상태 구독 및 동기화 + useEffect(() => { + const updateFromGlobalState = () => { + setGlobalStateVersion((prev) => prev + 1); + }; + + // 전역 상태 변경 구독 + globalState.subscribers.add(updateFromGlobalState); + + return () => { + globalState.subscribers.delete(updateFromGlobalState); + }; + }, []); + + // 🔧 테이블 코드 카테고리 로드 (전역 상태 사용) + const loadTableCodeCategory = async () => { + if (!component.tableName || !component.columnName) return; + + try { + console.log(`🔍 [${component.id}] 전역 테이블 코드 카테고리 조회`); + const category = await loadGlobalTableCodeCategory(component.tableName, component.columnName); + + if (category !== dynamicCodeCategory) { + console.log(`🔄 [${component.id}] 코드 카테고리 변경: ${dynamicCodeCategory} → ${category}`); + setDynamicCodeCategory(category); + } + } catch (error) { + console.error(`❌ [${component.id}] 테이블 코드 카테고리 조회 실패:`, error); + } }; - // 디자인 모드 스타일 - if (isDesignMode) { - componentStyle.border = "1px dashed #cbd5e1"; - componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; - } + // 🔧 코드 옵션 로드 (전역 상태 사용) + const loadCodeOptions = async (category: string) => { + if (!category || category === "none") { + setCodeOptions([]); + setIsLoadingCodes(false); + return; + } - // 이벤트 핸들러 - const handleClick = (e: React.MouseEvent) => { - e.stopPropagation(); - onClick?.(); + try { + setIsLoadingCodes(true); + console.log(`🔄 [${component.id}] 전역 코드 옵션 로딩: ${category}`); + + const options = await loadGlobalCodeOptions(category); + setCodeOptions(options); + + console.log(`✅ [${component.id}] 코드 옵션 업데이트 완료: ${category} (${options.length}개)`); + } catch (error) { + console.error(`❌ [${component.id}] 코드 옵션 로딩 실패:`, error); + setCodeOptions([]); + } finally { + setIsLoadingCodes(false); + } }; - // DOM에 전달하면 안 되는 React-specific props 필터링 - const { - selectedScreen, - onZoneComponentDrop, - onZoneClick, - componentConfig: _componentConfig, - component: _component, - isSelected: _isSelected, - onClick: _onClick, - onDragStart: _onDragStart, - onDragEnd: _onDragEnd, - size: _size, - position: _position, - style: _style, - screenId: _screenId, - tableName: _tableName, - onRefresh: _onRefresh, - onClose: _onClose, - ...domProps - } = props; + // 초기 테이블 코드 카테고리 로드 + useEffect(() => { + loadTableCodeCategory(); + }, [component.tableName, component.columnName]); + + // 전역 상태 변경 시 동기화 + useEffect(() => { + if (component.tableName && component.columnName) { + const key = `${component.tableName}.${component.columnName}`; + const cachedCategory = globalState.tableCategories.get(key); + + if (cachedCategory && cachedCategory !== dynamicCodeCategory) { + console.log(`🔄 [${component.id}] 전역 상태 동기화: ${dynamicCodeCategory} → ${cachedCategory}`); + setDynamicCodeCategory(cachedCategory || null); + } + } + }, [globalStateVersion, component.tableName, component.columnName]); + + // 코드 카테고리 변경 시 옵션 로드 + useEffect(() => { + if (codeCategory && codeCategory !== "none") { + // 전역 캐시된 옵션부터 확인 + const cached = globalState.codeOptions.get(codeCategory); + if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { + console.log(`🚀 [${component.id}] 전역 캐시 즉시 적용: ${codeCategory} (${cached.options.length}개)`); + setCodeOptions(cached.options); + setIsLoadingCodes(false); + } else { + loadCodeOptions(codeCategory); + } + } else { + setCodeOptions([]); + setIsLoadingCodes(false); + } + }, [codeCategory]); + + // 전역 상태에서 코드 옵션 변경 감지 + useEffect(() => { + if (codeCategory) { + const cached = globalState.codeOptions.get(codeCategory); + if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { + if (JSON.stringify(cached.options) !== JSON.stringify(codeOptions)) { + console.log(`🔄 [${component.id}] 전역 옵션 변경 감지: ${codeCategory}`); + setCodeOptions(cached.options); + } + } + } + }, [globalStateVersion, codeCategory]); + + // 선택된 값에 따른 라벨 업데이트 + useEffect(() => { + const getAllOptions = () => { + const configOptions = componentConfig.options || []; + return [...codeOptions, ...configOptions]; + }; + + const options = getAllOptions(); + const selectedOption = options.find((option) => option.value === selectedValue); + const newLabel = selectedOption?.label || ""; + + if (newLabel !== selectedLabel) { + setSelectedLabel(newLabel); + } + }, [selectedValue, codeOptions, componentConfig.options]); + + // 클릭 이벤트 핸들러 (전역 상태 새로고침) + const handleToggle = () => { + if (isDesignMode) return; + + console.log(`🖱️ [${component.id}] 드롭다운 토글: ${isOpen} → ${!isOpen}`); + console.log(`📊 [${component.id}] 현재 상태:`, { + isDesignMode, + isLoadingCodes, + allOptionsLength: allOptions.length, + allOptions: allOptions.map((o) => ({ value: o.value, label: o.label })), + }); + + // 드롭다운을 열 때 전역 상태 새로고침 + if (!isOpen) { + console.log(`🖱️ [${component.id}] 셀렉트박스 클릭 - 전역 상태 새로고침`); + + // 테이블 설정 캐시 무효화 후 재로드 + if (component.tableName && component.columnName) { + const key = `${component.tableName}.${component.columnName}`; + globalState.tableCategories.delete(key); + + // 현재 코드 카테고리의 캐시도 무효화 + if (dynamicCodeCategory) { + globalState.codeOptions.delete(dynamicCodeCategory); + console.log(`🗑️ [${component.id}] 코드 옵션 캐시 무효화: ${dynamicCodeCategory}`); + + // 강제로 새로운 API 호출 수행 + console.log(`🔄 [${component.id}] 강제 코드 옵션 재로드 시작: ${dynamicCodeCategory}`); + loadCodeOptions(dynamicCodeCategory); + } + + loadTableCodeCategory(); + } + } + + setIsOpen(!isOpen); + }; + + // 옵션 선택 핸들러 + const handleOptionSelect = (value: string, label: string) => { + setSelectedValue(value); + setSelectedLabel(label); + setIsOpen(false); + + if (onUpdate) { + onUpdate("value", value); + } + + console.log(`✅ [${component.id}] 옵션 선택:`, { value, label }); + }; + + // 외부 클릭 시 드롭다운 닫기 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (selectRef.current && !selectRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen]); + + // 🚀 실시간 업데이트를 위한 이벤트 리스너 + useEffect(() => { + const handleFocus = () => { + console.log(`👁️ [${component.id}] 윈도우 포커스 - 전역 상태 새로고침`); + if (component.tableName && component.columnName) { + const key = `${component.tableName}.${component.columnName}`; + globalState.tableCategories.delete(key); // 캐시 무효화 + loadTableCodeCategory(); + } + }; + + const handleVisibilityChange = () => { + if (!document.hidden) { + console.log(`👁️ [${component.id}] 페이지 가시성 변경 - 전역 상태 새로고침`); + if (component.tableName && component.columnName) { + const key = `${component.tableName}.${component.columnName}`; + globalState.tableCategories.delete(key); // 캐시 무효화 + loadTableCodeCategory(); + } + } + }; + + window.addEventListener("focus", handleFocus); + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + window.removeEventListener("focus", handleFocus); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [component.tableName, component.columnName]); + + // 모든 옵션 가져오기 + const getAllOptions = () => { + const configOptions = componentConfig.options || []; + console.log(`🔧 [${component.id}] 옵션 병합:`, { + codeOptionsLength: codeOptions.length, + codeOptions: codeOptions.map((o) => ({ value: o.value, label: o.label })), + configOptionsLength: configOptions.length, + configOptions: configOptions.map((o) => ({ value: o.value, label: o.label })), + }); + return [...codeOptions, ...configOptions]; + }; + + const allOptions = getAllOptions(); + const placeholder = componentConfig.placeholder || "선택하세요"; return ( -
- {/* 라벨 렌더링 */} - {component.label && ( - - )} - - + {selectedLabel || placeholder} + + {/* 드롭다운 아이콘 */} + + + +
+ + {/* 드롭다운 옵션 */} + {isOpen && !isDesignMode && ( +
+ {(() => { + console.log(`🎨 [${component.id}] 드롭다운 렌더링:`, { + isOpen, + isDesignMode, + isLoadingCodes, + allOptionsLength: allOptions.length, + allOptions: allOptions.map((o) => ({ value: o.value, label: o.label })), + }); + return null; + })()} + {isLoadingCodes ? ( +
로딩 중...
+ ) : allOptions.length > 0 ? ( + allOptions.map((option, index) => ( +
handleOptionSelect(option.value, option.label)} + > + {option.label || option.value || `옵션 ${index + 1}`} +
+ )) + ) : ( +
옵션이 없습니다
+ )} +
+ )}
); }; -/** - * SelectBasic 래퍼 컴포넌트 - * 추가적인 로직이나 상태 관리가 필요한 경우 사용 - */ -export const SelectBasicWrapper: React.FC = (props) => { - return ; -}; +// Wrapper 컴포넌트 (기존 호환성을 위해) +export const SelectBasicWrapper = SelectBasicComponent; + +// 기본 export +export { SelectBasicComponent }; diff --git a/frontend/lib/registry/components/select-basic/types.ts b/frontend/lib/registry/components/select-basic/types.ts index 18bef2bf..2398a665 100644 --- a/frontend/lib/registry/components/select-basic/types.ts +++ b/frontend/lib/registry/components/select-basic/types.ts @@ -6,20 +6,24 @@ import { ComponentConfig } from "@/types/component"; * SelectBasic 컴포넌트 설정 타입 */ export interface SelectBasicConfig extends ComponentConfig { - // select 관련 설정 + // select 관련 설정 placeholder?: string; - + options?: Array<{ value: string; label: string }>; + multiple?: boolean; + + // 코드 관련 설정 + codeCategory?: string; + // 공통 설정 disabled?: boolean; required?: boolean; readonly?: boolean; - placeholder?: string; helperText?: string; - + // 스타일 관련 variant?: "default" | "outlined" | "filled"; size?: "sm" | "md" | "lg"; - + // 이벤트 관련 onChange?: (value: any) => void; onFocus?: () => void; @@ -37,7 +41,7 @@ export interface SelectBasicProps { config?: SelectBasicConfig; className?: string; style?: React.CSSProperties; - + // 이벤트 핸들러 onChange?: (value: any) => void; onFocus?: () => void; diff --git a/frontend/lib/utils/webTypeMapping.ts b/frontend/lib/utils/webTypeMapping.ts index 1117056f..f27fa26e 100644 --- a/frontend/lib/utils/webTypeMapping.ts +++ b/frontend/lib/utils/webTypeMapping.ts @@ -47,8 +47,8 @@ export const WEB_TYPE_COMPONENT_MAPPING: Record = { // 기타 label: "text-display", - code: "text-input", // 임시로 텍스트 입력 사용 - entity: "select-basic", // 임시로 선택상자 사용 + code: "select-basic", // 코드 타입은 선택상자 사용 + entity: "select-basic", // 엔티티 타입은 선택상자 사용 }; /** diff --git a/frontend/types/screen.ts b/frontend/types/screen.ts index 2b5a8bee..f7bde4a0 100644 --- a/frontend/types/screen.ts +++ b/frontend/types/screen.ts @@ -388,6 +388,10 @@ export interface DataTableColumn { searchable: boolean; // 검색 대상 여부 webTypeConfig?: WebTypeConfig; // 컬럼별 상세 설정 + // 레거시 지원용 (테이블 타입 관리에서 설정된 값) + codeCategory?: string; // 코드 카테고리 (코드 타입용) + referenceTable?: string; // 참조 테이블 (엔티티 타입용) + // 가상 파일 컬럼 관련 속성 isVirtualFileColumn?: boolean; // 가상 파일 컬럼인지 여부 fileColumnConfig?: {