diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index 53c5de22..77fdb0dd 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -26,6 +26,7 @@ export class EntityJoinController { sortOrder = "asc", enableEntityJoin = true, additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열) + screenEntityConfigs, // 화면별 엔티티 설정 (JSON 문자열) userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함 ...otherParams } = req.query; @@ -65,6 +66,24 @@ export class EntityJoinController { } } + // 화면별 엔티티 설정 처리 + let parsedScreenEntityConfigs: Record = {}; + if (screenEntityConfigs) { + try { + parsedScreenEntityConfigs = + typeof screenEntityConfigs === "string" + ? JSON.parse(screenEntityConfigs) + : screenEntityConfigs; + logger.info( + "화면별 엔티티 설정 파싱 완료:", + parsedScreenEntityConfigs + ); + } catch (error) { + logger.warn("화면별 엔티티 설정 파싱 오류:", error); + parsedScreenEntityConfigs = {}; + } + } + const result = await tableManagementService.getTableDataWithEntityJoins( tableName, { @@ -79,6 +98,7 @@ export class EntityJoinController { enableEntityJoin: enableEntityJoin === "true" || enableEntityJoin === true, additionalJoinColumns: parsedAdditionalJoinColumns, + screenEntityConfigs: parsedScreenEntityConfigs, } ); @@ -348,14 +368,16 @@ export class EntityJoinController { ); // 현재 display_column으로 사용 중인 컬럼 제외 + const currentDisplayColumn = + config.displayColumn || config.displayColumns[0]; const availableColumns = columns.filter( - (col) => col.columnName !== config.displayColumn + (col) => col.columnName !== currentDisplayColumn ); return { joinConfig: config, tableName: config.referenceTable, - currentDisplayColumn: config.displayColumn, + currentDisplayColumn: currentDisplayColumn, availableColumns: availableColumns.map((col) => ({ columnName: col.columnName, columnLabel: col.displayName || col.columnName, @@ -373,7 +395,8 @@ export class EntityJoinController { return { joinConfig: config, tableName: config.referenceTable, - currentDisplayColumn: config.displayColumn, + currentDisplayColumn: + config.displayColumn || config.displayColumns[0], availableColumns: [], error: error instanceof Error ? error.message : "Unknown error", }; diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index f84cf167..b88c0c8b 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -16,8 +16,13 @@ const prisma = new PrismaClient(); export class EntityJoinService { /** * 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성 + * @param tableName 테이블명 + * @param screenEntityConfigs 화면별 엔티티 설정 (선택사항) */ - async detectEntityJoins(tableName: string): Promise { + async detectEntityJoins( + tableName: string, + screenEntityConfigs?: Record + ): Promise { try { logger.info(`Entity 컬럼 감지 시작: ${tableName}`); @@ -37,9 +42,23 @@ export class EntityJoinService { }, }); + logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`); + entityColumns.forEach((col, index) => { + logger.info( + ` ${index + 1}. ${col.column_name} -> ${col.reference_table}.${col.reference_column} (display: ${col.display_column})` + ); + }); + const joinConfigs: EntityJoinConfig[] = []; for (const column of entityColumns) { + logger.info(`🔍 Entity 컬럼 상세 정보:`, { + column_name: column.column_name, + reference_table: column.reference_table, + reference_column: column.reference_column, + display_column: column.display_column, + }); + if ( !column.column_name || !column.reference_table || @@ -48,8 +67,50 @@ export class EntityJoinService { continue; } - // display_column이 없으면 reference_column 사용 - const displayColumn = column.display_column || column.reference_column; + // 화면별 엔티티 설정이 있으면 우선 사용, 없으면 기본값 사용 + const screenConfig = screenEntityConfigs?.[column.column_name]; + let displayColumns: string[] = []; + let separator = " - "; + + logger.info(`🔍 조건 확인 - 컬럼: ${column.column_name}`, { + hasScreenConfig: !!screenConfig, + hasDisplayColumns: screenConfig?.displayColumns, + displayColumn: column.display_column, + }); + + if (screenConfig && screenConfig.displayColumns) { + // 화면에서 설정된 표시 컬럼들 사용 (기본 테이블 + 조인 테이블 조합 지원) + displayColumns = screenConfig.displayColumns; + separator = screenConfig.separator || " - "; + console.log(`🎯 화면별 엔티티 설정 적용: ${column.column_name}`, { + displayColumns, + separator, + screenConfig, + }); + } else if (column.display_column && column.display_column !== "none") { + // 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만) + displayColumns = [column.display_column]; + logger.info( + `🔧 기존 display_column 사용: ${column.column_name} → ${column.display_column}` + ); + } else { + // display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정 + // 🚨 display_column이 항상 "none"이므로 이 로직을 기본으로 사용 + let defaultDisplayColumn = column.reference_column; + if (column.reference_table === "dept_info") { + defaultDisplayColumn = "dept_name"; + } else if (column.reference_table === "company_info") { + defaultDisplayColumn = "company_name"; + } else if (column.reference_table === "user_info") { + defaultDisplayColumn = "user_name"; + } + + displayColumns = [defaultDisplayColumn]; + logger.info( + `🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})` + ); + logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns); + } // 별칭 컬럼명 생성 (writer -> writer_name) const aliasColumn = `${column.column_name}_name`; @@ -59,17 +120,38 @@ export class EntityJoinService { sourceColumn: column.column_name, referenceTable: column.reference_table, referenceColumn: column.reference_column, - displayColumn: displayColumn, + displayColumns: displayColumns, + displayColumn: displayColumns[0], // 하위 호환성 aliasColumn: aliasColumn, + separator: separator, }; + logger.info(`🔧 기본 조인 설정 생성:`, { + sourceTable: joinConfig.sourceTable, + sourceColumn: joinConfig.sourceColumn, + referenceTable: joinConfig.referenceTable, + aliasColumn: joinConfig.aliasColumn, + displayColumns: joinConfig.displayColumns, + }); + // 조인 설정 유효성 검증 + logger.info( + `🔍 조인 설정 검증 중: ${joinConfig.sourceColumn} -> ${joinConfig.referenceTable}` + ); if (await this.validateJoinConfig(joinConfig)) { joinConfigs.push(joinConfig); + logger.info(`✅ 조인 설정 추가됨: ${joinConfig.aliasColumn}`); + } else { + logger.warn(`❌ 조인 설정 검증 실패: ${joinConfig.sourceColumn}`); } } - logger.info(`Entity 조인 설정 생성 완료: ${joinConfigs.length}개`); + logger.info(`🎯 Entity 조인 설정 생성 완료: ${joinConfigs.length}개`); + joinConfigs.forEach((config, index) => { + logger.info( + ` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable}.${config.referenceColumn} AS ${config.aliasColumn}` + ); + }); return joinConfigs; } catch (error) { logger.error(`Entity 조인 감지 실패: ${tableName}`, error); @@ -90,8 +172,10 @@ export class EntityJoinService { offset?: number ): { query: string; aliasMap: Map } { try { - // 기본 SELECT 컬럼들 - const baseColumns = selectColumns.map((col) => `main.${col}`).join(", "); + // 기본 SELECT 컬럼들 (TEXT로 캐스팅하여 record 타입 오류 방지) + const baseColumns = selectColumns + .map((col) => `main.${col}::TEXT AS ${col}`) + .join(", "); // Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리) // 별칭 매핑 생성 (JOIN 절과 동일한 로직) @@ -130,10 +214,69 @@ export class EntityJoinService { }); const joinColumns = joinConfigs - .map( - (config) => - `COALESCE(${aliasMap.get(config.referenceTable)}.${config.displayColumn}, '') AS ${config.aliasColumn}` - ) + .map((config) => { + const alias = aliasMap.get(config.referenceTable); + const displayColumns = config.displayColumns || [ + config.displayColumn, + ]; + const separator = config.separator || " - "; + + if (displayColumns.length === 1) { + // 단일 컬럼인 경우 + const col = displayColumns[0]; + const isJoinTableColumn = [ + "dept_name", + "dept_code", + "master_user_id", + "location_name", + "parent_dept_code", + "master_sabun", + "location", + "data_type", + "company_name", + "sales_yn", + "status", + ].includes(col); + + if (isJoinTableColumn) { + return `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`; + } else { + return `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`; + } + } else { + // 여러 컬럼인 경우 CONCAT으로 연결 + // 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리 + const concatParts = displayColumns + .map((col) => { + // 조인 테이블의 컬럼인지 확인 (조인 테이블에 존재하는 컬럼만 조인 별칭 사용) + // 현재는 dept_info 테이블의 컬럼들을 확인 + const isJoinTableColumn = [ + "dept_name", + "dept_code", + "master_user_id", + "location_name", + "parent_dept_code", + "master_sabun", + "location", + "data_type", + "company_name", + "sales_yn", + "status", + ].includes(col); + + if (isJoinTableColumn) { + // 조인 테이블 컬럼은 조인 별칭 사용 + return `COALESCE(${alias}.${col}::TEXT, '')`; + } else { + // 기본 테이블 컬럼은 main 별칭 사용 + return `COALESCE(main.${col}::TEXT, '')`; + } + }) + .join(` || '${separator}' || `); + + return `(${concatParts}) AS ${config.aliasColumn}`; + } + }) .join(", "); // SELECT 절 구성 @@ -179,7 +322,7 @@ export class EntityJoinService { .filter(Boolean) .join("\n"); - logger.debug(`생성된 Entity 조인 쿼리:`, query); + logger.info(`🔍 생성된 Entity 조인 쿼리:`, query); return { query: query, aliasMap: aliasMap, @@ -199,11 +342,28 @@ export class EntityJoinService { try { const strategies = await Promise.all( joinConfigs.map(async (config) => { + // 여러 컬럼을 조합하는 경우 캐시 전략 사용 불가 + if (config.displayColumns && config.displayColumns.length > 1) { + console.log( + `🎯 여러 컬럼 조합으로 인해 조인 전략 사용: ${config.sourceColumn}`, + config.displayColumns + ); + return "join"; + } + // 참조 테이블의 캐시 가능성 확인 + const displayCol = + config.displayColumn || + config.displayColumns?.[0] || + config.referenceColumn; + logger.info( + `🔍 캐시 확인용 표시 컬럼: ${config.referenceTable} - ${displayCol}` + ); + const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + displayCol ); return cachedData ? "cache" : "join"; @@ -233,6 +393,14 @@ export class EntityJoinService { */ private async validateJoinConfig(config: EntityJoinConfig): Promise { try { + logger.info("🔍 조인 설정 검증 상세:", { + sourceColumn: config.sourceColumn, + referenceTable: config.referenceTable, + displayColumns: config.displayColumns, + displayColumn: config.displayColumn, + aliasColumn: config.aliasColumn, + }); + // 참조 테이블 존재 확인 const tableExists = await prisma.$queryRaw` SELECT 1 FROM information_schema.tables @@ -245,19 +413,34 @@ export class EntityJoinService { return false; } - // 참조 컬럼 존재 확인 - const columnExists = await prisma.$queryRaw` - SELECT 1 FROM information_schema.columns - WHERE table_name = ${config.referenceTable} - AND column_name = ${config.displayColumn} - LIMIT 1 - `; + // 참조 컬럼 존재 확인 (displayColumns[0] 사용) + const displayColumn = config.displayColumns?.[0] || config.displayColumn; + logger.info( + `🔍 표시 컬럼 확인: ${displayColumn} (from displayColumns: ${config.displayColumns}, displayColumn: ${config.displayColumn})` + ); - if (!Array.isArray(columnExists) || columnExists.length === 0) { - logger.warn( - `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${config.displayColumn}` + // 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용 + if (displayColumn && displayColumn !== "none") { + const columnExists = await prisma.$queryRaw` + SELECT 1 FROM information_schema.columns + WHERE table_name = ${config.referenceTable} + AND column_name = ${displayColumn} + LIMIT 1 + `; + + if (!Array.isArray(columnExists) || columnExists.length === 0) { + logger.warn( + `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` + ); + return false; + } + logger.info( + `✅ 표시 컬럼 확인 완료: ${config.referenceTable}.${displayColumn}` + ); + } else { + logger.info( + `🔧 표시 컬럼 검증 생략: display_column이 none이거나 설정되지 않음` ); - return false; } return true; diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 94f8aa30..4ca5369d 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2023,6 +2023,7 @@ export class TableManagementService { sourceColumn: string; joinAlias: string; }>; + screenEntityConfigs?: Record; // 화면별 엔티티 설정 } ): Promise { const startTime = Date.now(); @@ -2042,8 +2043,22 @@ export class TableManagementService { }; } - // Entity 조인 설정 감지 - let joinConfigs = await entityJoinService.detectEntityJoins(tableName); + // Entity 조인 설정 감지 (화면별 엔티티 설정 전달) + let joinConfigs = await entityJoinService.detectEntityJoins( + tableName, + options.screenEntityConfigs + ); + + logger.info( + `🔍 detectEntityJoins 결과: ${joinConfigs.length}개 조인 설정` + ); + if (joinConfigs.length > 0) { + joinConfigs.forEach((config, index) => { + logger.info( + ` 조인 ${index + 1}: ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}` + ); + }); + } // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 if ( @@ -2053,32 +2068,84 @@ export class TableManagementService { logger.info( `추가 조인 컬럼 처리: ${options.additionalJoinColumns.length}개` ); + logger.info( + "📋 전달받은 additionalJoinColumns:", + options.additionalJoinColumns + ); for (const additionalColumn of options.additionalJoinColumns) { - // 기존 조인 설정에서 같은 참조 테이블을 사용하는 설정 찾기 + // 🔍 sourceColumn을 기준으로 기존 조인 설정 찾기 (dept_code로 찾기) const baseJoinConfig = joinConfigs.find( - (config) => config.referenceTable === additionalColumn.sourceTable + (config) => config.sourceColumn === additionalColumn.sourceColumn ); if (baseJoinConfig) { + // joinAlias에서 실제 컬럼명 추출 (예: dept_code_location_name -> location_name) + // sourceColumn을 제거한 나머지 부분이 실제 컬럼명 + const sourceColumn = baseJoinConfig.sourceColumn; // dept_code + const joinAlias = additionalColumn.joinAlias; // dept_code_company_name + const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // company_name + + logger.info(`🔍 조인 컬럼 상세 분석:`, { + sourceColumn, + joinAlias, + actualColumnName, + referenceTable: additionalColumn.sourceTable, + }); + + // 🚨 기본 Entity 조인과 중복되지 않도록 체크 + const isBasicEntityJoin = + additionalColumn.joinAlias === + `${baseJoinConfig.sourceColumn}_name`; + + if (isBasicEntityJoin) { + logger.info( + `⚠️ 기본 Entity 조인과 중복: ${additionalColumn.joinAlias} - 건너뜀` + ); + continue; // 기본 Entity 조인과 중복되면 추가하지 않음 + } + // 추가 조인 컬럼 설정 생성 const additionalJoinConfig: EntityJoinConfig = { sourceTable: tableName, - sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (writer) - referenceTable: additionalColumn.sourceTable, // 참조 테이블 (user_info) - referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (user_id) - displayColumn: additionalColumn.sourceColumn, // 표시할 컬럼 (email) - aliasColumn: additionalColumn.joinAlias, // 별칭 (writer_email) + sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code) + referenceTable: + (additionalColumn as any).referenceTable || + baseJoinConfig.referenceTable, // 참조 테이블 (dept_info) + referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code) + displayColumns: [actualColumnName], // 표시할 컬럼들 (company_name) + displayColumn: actualColumnName, // 하위 호환성 + aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_company_name) + separator: " - ", // 기본 구분자 }; joinConfigs.push(additionalJoinConfig); logger.info( - `추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn}` + `✅ 추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}` ); + logger.info(`🔍 추가된 조인 설정 상세:`, { + sourceTable: additionalJoinConfig.sourceTable, + sourceColumn: additionalJoinConfig.sourceColumn, + referenceTable: additionalJoinConfig.referenceTable, + displayColumns: additionalJoinConfig.displayColumns, + aliasColumn: additionalJoinConfig.aliasColumn, + }); } } } + // 최종 조인 설정 배열 로깅 + logger.info(`🎯 최종 joinConfigs 배열 (${joinConfigs.length}개):`); + joinConfigs.forEach((config, index) => { + logger.info( + ` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`, + { + displayColumns: config.displayColumns, + displayColumn: config.displayColumn, + } + ); + }); + if (joinConfigs.length === 0) { logger.info(`Entity 조인 설정이 없음: ${tableName}`); const basicResult = await this.getTableData(tableName, options); @@ -2092,8 +2159,21 @@ export class TableManagementService { } // 조인 전략 결정 (테이블 크기 기반) - const strategy = - await entityJoinService.determineJoinStrategy(joinConfigs); + // 🚨 additionalJoinColumns가 있는 경우 강제로 full_join 사용 (캐시 안정성 보장) + let strategy: "full_join" | "cache_lookup" | "hybrid"; + + if ( + options.additionalJoinColumns && + options.additionalJoinColumns.length > 0 + ) { + strategy = "full_join"; + console.log( + `🔧 additionalJoinColumns 감지: 강제로 full_join 전략 사용 (${options.additionalJoinColumns.length}개 추가 조인)` + ); + } else { + strategy = await entityJoinService.determineJoinStrategy(joinConfigs); + } + console.log( `🎯 선택된 조인 전략: ${strategy} (${joinConfigs.length}개 Entity 조인)` ); @@ -2239,10 +2319,18 @@ export class TableManagementService { try { // 캐시 데이터 미리 로드 for (const config of joinConfigs) { + const displayCol = + config.displayColumn || + config.displayColumns?.[0] || + config.referenceColumn; + logger.info( + `🔍 캐시 로드 - ${config.referenceTable}: keyCol=${config.referenceColumn}, displayCol=${displayCol}` + ); + await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + displayCol ); } @@ -2429,7 +2517,7 @@ export class TableManagementService { const lookupValue = referenceCacheService.getLookupValue( config.referenceTable, config.referenceColumn, - config.displayColumn, + config.displayColumn || config.displayColumns[0], String(sourceValue) ); @@ -2723,7 +2811,7 @@ export class TableManagementService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); if (cachedData && cachedData.size > 0) { @@ -2807,7 +2895,7 @@ export class TableManagementService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); if (cachedData) { @@ -2846,7 +2934,7 @@ export class TableManagementService { const hitRate = referenceCacheService.getCacheHitRate( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); totalHitRate += hitRate; } diff --git a/backend-node/src/types/tableManagement.ts b/backend-node/src/types/tableManagement.ts index 52dca092..ee5e97b1 100644 --- a/backend-node/src/types/tableManagement.ts +++ b/backend-node/src/types/tableManagement.ts @@ -77,8 +77,10 @@ export interface EntityJoinConfig { sourceColumn: string; // writer referenceTable: string; // user_info referenceColumn: string; // user_id (조인 키) - displayColumn: string; // user_name (표시할 값) + displayColumns: string[]; // ['user_name', 'dept_name'] (표시할 값들) + displayColumn?: string; // user_name (하위 호환성용, deprecated) aliasColumn: string; // writer_name (결과 컬럼명) + separator?: string; // ' - ' (여러 컬럼 연결 시 구분자) } export interface EntityJoinResponse { diff --git a/frontend/app/(main)/admin/batch-management/page.tsx b/frontend/app/(main)/admin/batch-management/page.tsx index 9b23cf70..9e48a097 100644 --- a/frontend/app/(main)/admin/batch-management/page.tsx +++ b/frontend/app/(main)/admin/batch-management/page.tsx @@ -185,11 +185,12 @@ export default function BatchManagementPage() { }; return ( -
- {/* 헤더 */} -
-
-

배치 관리

+
+
+ {/* 헤더 */} +
+
+

배치 관리

스케줄된 배치 작업을 관리하고 실행 상태를 모니터링합니다.

@@ -428,6 +429,7 @@ export default function BatchManagementPage() { onSave={handleModalSave} job={selectedJob} /> +
); } diff --git a/frontend/app/(main)/admin/collection-management/page.tsx b/frontend/app/(main)/admin/collection-management/page.tsx index 4edbcaec..8320caac 100644 --- a/frontend/app/(main)/admin/collection-management/page.tsx +++ b/frontend/app/(main)/admin/collection-management/page.tsx @@ -162,11 +162,12 @@ export default function CollectionManagementPage() { }; return ( -
- {/* 헤더 */} -
-
-

수집 관리

+
+
+ {/* 헤더 */} +
+
+

수집 관리

외부 데이터베이스에서 데이터를 수집하는 설정을 관리합니다.

@@ -332,6 +333,7 @@ export default function CollectionManagementPage() { onSave={handleModalSave} config={selectedConfig} /> +
); } diff --git a/frontend/app/(main)/admin/commonCode/page.tsx b/frontend/app/(main)/admin/commonCode/page.tsx index be946e05..6d5eba31 100644 --- a/frontend/app/(main)/admin/commonCode/page.tsx +++ b/frontend/app/(main)/admin/commonCode/page.tsx @@ -11,22 +11,23 @@ export default function CommonCodeManagementPage() { const { selectedCategoryCode, selectCategory } = useSelectedCategory(); return ( -
- {/* 페이지 헤더 */} -
-
-

공통코드 관리

-

시스템에서 사용하는 공통코드를 관리합니다

+
+
+ {/* 페이지 제목 */} +
+
+

공통코드 관리

+

시스템에서 사용하는 공통코드를 관리합니다

+
-
{/* 메인 콘텐츠 */} {/* 반응형 레이아웃: PC는 가로, 모바일은 세로 */}
{/* 카테고리 패널 - PC에서 좌측 고정 너비, 모바일에서 전체 너비 */}
- - + + 📂 코드 카테고리 @@ -37,8 +38,8 @@ export default function CommonCodeManagementPage() { {/* 코드 상세 패널 - PC에서 나머지 공간, 모바일에서 전체 너비 */}
- - + + 📋 코드 상세 정보 {selectedCategoryCode && ( @@ -52,6 +53,7 @@ export default function CommonCodeManagementPage() {
+
); } diff --git a/frontend/app/(main)/admin/company/page.tsx b/frontend/app/(main)/admin/company/page.tsx index 79e92516..645470eb 100644 --- a/frontend/app/(main)/admin/company/page.tsx +++ b/frontend/app/(main)/admin/company/page.tsx @@ -4,5 +4,18 @@ import { CompanyManagement } from "@/components/admin/CompanyManagement"; * 회사 관리 페이지 */ export default function CompanyPage() { - return ; + return ( +
+
+ {/* 페이지 제목 */} +
+
+

회사 관리

+

시스템에서 사용하는 회사 정보를 관리합니다

+
+
+ +
+
+ ); } diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index 19914665..de70ff1a 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -76,48 +76,49 @@ export default function DataFlowPage() { }; return ( -
- {/* 헤더 */} -
-
-
- {currentStep !== "list" && ( - - )} -
-

- {stepConfig[currentStep].icon} - {stepConfig[currentStep].title} -

-

{stepConfig[currentStep].description}

-
+
+
+ {/* 페이지 제목 */} +
+
+

데이터 흐름 관리

+

테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다

+ {currentStep !== "list" && ( + + )}
-
- {/* 단계별 내용 */} -
- {/* 관계도 목록 단계 */} - {currentStep === "list" && ( -
- + {/* 단계별 내용 */} +
+ {/* 관계도 목록 단계 */} + {currentStep === "list" && ( +
+
+

{stepConfig.list.title}

+
+
)} - {/* 관계도 설계 단계 */} - {currentStep === "design" && ( -
- goToStep("list")} - /> -
- )} + {/* 관계도 설계 단계 */} + {currentStep === "design" && ( +
+
+

{stepConfig.design.title}

+
+ goToStep("list")} + /> +
+ )} +
); diff --git a/frontend/app/(main)/admin/external-call-configs/page.tsx b/frontend/app/(main)/admin/external-call-configs/page.tsx index dbdd4aeb..e3755083 100644 --- a/frontend/app/(main)/admin/external-call-configs/page.tsx +++ b/frontend/app/(main)/admin/external-call-configs/page.tsx @@ -161,7 +161,8 @@ export default function ExternalCallConfigsPage() { }; return ( -
+
+
{/* 페이지 헤더 */}
@@ -396,6 +397,7 @@ export default function ExternalCallConfigsPage() { +
); } diff --git a/frontend/app/(main)/admin/external-connections/page.tsx b/frontend/app/(main)/admin/external-connections/page.tsx index 85e7911f..96dd64c4 100644 --- a/frontend/app/(main)/admin/external-connections/page.tsx +++ b/frontend/app/(main)/admin/external-connections/page.tsx @@ -220,14 +220,18 @@ export default function ExternalConnectionsPage() { }; return ( -
-
-

외부 커넥션 관리

-

외부 데이터베이스 연결 정보를 관리합니다.

-
+
+
+ {/* 페이지 제목 */} +
+
+

외부 커넥션 관리

+

외부 데이터베이스 연결 정보를 관리합니다

+
+
{/* 검색 및 필터 */} - +
@@ -285,7 +289,7 @@ export default function ExternalConnectionsPage() {
로딩 중...
) : connections.length === 0 ? ( - +
@@ -298,7 +302,7 @@ export default function ExternalConnectionsPage() { ) : ( - + @@ -446,6 +450,7 @@ export default function ExternalConnectionsPage() { connectionName={selectedConnection.connection_name} /> )} + ); } diff --git a/frontend/app/(main)/admin/i18n/page.tsx b/frontend/app/(main)/admin/i18n/page.tsx index f1fa7ef4..bb7308e2 100644 --- a/frontend/app/(main)/admin/i18n/page.tsx +++ b/frontend/app/(main)/admin/i18n/page.tsx @@ -3,6 +3,12 @@ import MultiLang from "@/components/admin/MultiLang"; export default function I18nPage() { - return ; + return ( +
+
+ +
+
+ ); } diff --git a/frontend/app/(main)/admin/layouts/page.tsx b/frontend/app/(main)/admin/layouts/page.tsx index c5215057..eb5b2aff 100644 --- a/frontend/app/(main)/admin/layouts/page.tsx +++ b/frontend/app/(main)/admin/layouts/page.tsx @@ -220,19 +220,21 @@ export default function LayoutManagementPage() { }; return ( -
-
-
-

레이아웃 관리

-

화면 레이아웃을 생성하고 관리합니다.

+
+
+ {/* 페이지 제목 */} +
+
+

레이아웃 관리

+

화면 레이아웃을 생성하고 관리합니다

+
+
- -
{/* 검색 및 필터 */} - +
@@ -282,7 +284,7 @@ export default function LayoutManagementPage() { {layouts.map((layout) => { const CategoryIcon = CATEGORY_ICONS[layout.category as keyof typeof CATEGORY_ICONS]; return ( - +
@@ -411,6 +413,7 @@ export default function LayoutManagementPage() { loadCategoryCounts(); }} /> +
); } diff --git a/frontend/app/(main)/admin/menu/page.tsx b/frontend/app/(main)/admin/menu/page.tsx index 301e0321..3d5548cc 100644 --- a/frontend/app/(main)/admin/menu/page.tsx +++ b/frontend/app/(main)/admin/menu/page.tsx @@ -4,8 +4,17 @@ import { MenuManagement } from "@/components/admin/MenuManagement"; export default function MenuPage() { return ( -
- +
+
+ {/* 페이지 제목 */} +
+
+

메뉴 관리

+

시스템 메뉴를 관리하고 화면을 할당합니다

+
+
+ +
); } diff --git a/frontend/app/(main)/admin/monitoring/page.tsx b/frontend/app/(main)/admin/monitoring/page.tsx index 6161c387..2f028639 100644 --- a/frontend/app/(main)/admin/monitoring/page.tsx +++ b/frontend/app/(main)/admin/monitoring/page.tsx @@ -5,17 +5,19 @@ import MonitoringDashboard from "@/components/admin/MonitoringDashboard"; export default function MonitoringPage() { return ( -
- {/* 헤더 */} -
-

모니터링

-

- 배치 작업 실행 상태를 실시간으로 모니터링합니다. -

-
+
+
+ {/* 헤더 */} +
+

모니터링

+

+ 배치 작업 실행 상태를 실시간으로 모니터링합니다. +

+
- {/* 모니터링 대시보드 */} - + {/* 모니터링 대시보드 */} + +
); } diff --git a/frontend/app/(main)/admin/page.tsx b/frontend/app/(main)/admin/page.tsx index b320ab45..8735d7f6 100644 --- a/frontend/app/(main)/admin/page.tsx +++ b/frontend/app/(main)/admin/page.tsx @@ -5,7 +5,8 @@ import Link from "next/link"; */ export default function AdminPage() { return ( -
+
+
{/* 관리자 기능 카드들 */}
@@ -162,6 +163,7 @@ export default function AdminPage() {
+
); } diff --git a/frontend/app/(main)/admin/screenMng/page.tsx b/frontend/app/(main)/admin/screenMng/page.tsx index bf90f2d7..2002d364 100644 --- a/frontend/app/(main)/admin/screenMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/page.tsx @@ -66,18 +66,27 @@ export default function ScreenManagementPage() { const isLastStep = currentStep === "template"; return ( -
- {/* 단계별 내용 */} -
- {/* 화면 목록 단계 */} - {currentStep === "list" && ( -
-
-

{stepConfig.list.title}

- -
+
+
+ {/* 페이지 제목 */} +
+
+

화면 관리

+

화면을 설계하고 템플릿을 관리합니다

+
+
+ + {/* 단계별 내용 */} +
+ {/* 화면 목록 단계 */} + {currentStep === "list" && ( +
+
+

{stepConfig.list.title}

+ +
)} - {/* 화면 설계 단계 */} - {currentStep === "design" && ( -
- goToStep("list")} /> -
- )} - - {/* 템플릿 관리 단계 */} - {currentStep === "template" && ( -
-
-

{stepConfig.template.title}

-
- -
+ goToStep("list")} />
- goToStep("list")} /> -
- )} + )} + + {/* 템플릿 관리 단계 */} + {currentStep === "template" && ( +
+
+

{stepConfig.template.title}

+
+ + +
+
+ goToStep("list")} /> +
+ )} +
); diff --git a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx index be7dd3f5..ff24db7f 100644 --- a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx +++ b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx @@ -203,7 +203,8 @@ export default function EditWebTypePage() { } return ( -
+
+
{/* 헤더 */}
@@ -502,6 +503,7 @@ export default function EditWebTypePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/standards/[webType]/page.tsx b/frontend/app/(main)/admin/standards/[webType]/page.tsx index c11999ff..f44a8447 100644 --- a/frontend/app/(main)/admin/standards/[webType]/page.tsx +++ b/frontend/app/(main)/admin/standards/[webType]/page.tsx @@ -80,7 +80,8 @@ export default function WebTypeDetailPage() { } return ( -
+
+
{/* 헤더 */}
@@ -280,6 +281,7 @@ export default function WebTypeDetailPage() { +
); } diff --git a/frontend/app/(main)/admin/standards/new/page.tsx b/frontend/app/(main)/admin/standards/new/page.tsx index 77df8a74..aa60ed45 100644 --- a/frontend/app/(main)/admin/standards/new/page.tsx +++ b/frontend/app/(main)/admin/standards/new/page.tsx @@ -159,7 +159,8 @@ export default function NewWebTypePage() { }; return ( -
+
+
{/* 헤더 */}
@@ -453,6 +454,7 @@ export default function NewWebTypePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index c21266ab..ce1170e9 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -127,46 +127,47 @@ export default function WebTypesManagePage() { } return ( -
- {/* 헤더 */} -
-
-

웹타입 관리

-

화면관리에서 사용할 웹타입들을 관리합니다.

+
+
+ {/* 페이지 제목 */} +
+
+

웹타입 관리

+

화면관리에서 사용할 웹타입들을 관리합니다

+
+ + +
- - - -
- {/* 필터 및 검색 */} - - - - - 필터 및 검색 - - - -
- {/* 검색 */} -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
+ {/* 필터 및 검색 */} + + + + + 필터 및 검색 + + + +
+ {/* 검색 */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
- {/* 카테고리 필터 */} - + + + 전체 카테고리 {categories.map((category) => ( @@ -177,96 +178,96 @@ export default function WebTypesManagePage() { - {/* 활성화 상태 필터 */} - + {/* 활성화 상태 필터 */} + - {/* 초기화 버튼 */} - + {/* 초기화 버튼 */} +
- {/* 결과 통계 */} -
-

총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.

-
+ {/* 결과 통계 */} +
+

총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.

+
- {/* 웹타입 목록 테이블 */} - - -
- - - handleSort("sort_order")}> -
- 순서 - {sortField === "sort_order" && - (sortDirection === "asc" ? : )} -
-
- handleSort("web_type")}> -
- 웹타입 코드 - {sortField === "web_type" && - (sortDirection === "asc" ? : )} -
-
- handleSort("type_name")}> -
- 웹타입명 - {sortField === "type_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("category")}> -
- 카테고리 - {sortField === "category" && - (sortDirection === "asc" ? : )} -
-
- 설명 - handleSort("component_name")}> -
- 연결된 컴포넌트 - {sortField === "component_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("config_panel")}> -
- 설정 패널 - {sortField === "config_panel" && - (sortDirection === "asc" ? : )} -
-
- handleSort("is_active")}> -
- 상태 - {sortField === "is_active" && - (sortDirection === "asc" ? : )} -
-
- handleSort("updated_date")}> -
- 최종 수정일 - {sortField === "updated_date" && - (sortDirection === "asc" ? : )} -
-
- 작업 + {/* 웹타입 목록 테이블 */} + + +
+ + + handleSort("sort_order")}> +
+ 순서 + {sortField === "sort_order" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("web_type")}> +
+ 웹타입 코드 + {sortField === "web_type" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("type_name")}> +
+ 웹타입명 + {sortField === "type_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("category")}> +
+ 카테고리 + {sortField === "category" && + (sortDirection === "asc" ? : )} +
+
+ 설명 + handleSort("component_name")}> +
+ 연결된 컴포넌트 + {sortField === "component_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("config_panel")}> +
+ 설정 패널 + {sortField === "config_panel" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("is_active")}> +
+ 상태 + {sortField === "is_active" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("updated_date")}> +
+ 최종 수정일 + {sortField === "updated_date" && + (sortDirection === "asc" ? : )} +
+
+ 작업
@@ -309,24 +310,24 @@ export default function WebTypesManagePage() { {webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"} - -
- - - - - - - - - - + +
+ + + + + + + + + + 웹타입 삭제 @@ -364,6 +365,7 @@ export default function WebTypesManagePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index f09f6bc8..9fbaaed5 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -541,9 +541,9 @@ export default function TableManagementPage() { }, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]); return ( -
+
{/* 페이지 제목 */} -
+

{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")} @@ -593,10 +593,10 @@ export default function TableManagementPage() {
{/* 테이블 목록 */} - - + + - + {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")} @@ -663,10 +663,10 @@ export default function TableManagementPage() { {/* 컬럼 타입 관리 */} - - + + - + {selectedTable ? <>테이블 설정 - {selectedTable} : "테이블 타입 관리"} @@ -866,47 +866,6 @@ export default function TableManagementPage() {
)} - - {/* 표시 컬럼 */} - {column.referenceTable && column.referenceTable !== "none" && ( -
- - -
- )}

{/* 설정 완료 표시 - 간소화 */} diff --git a/frontend/app/(main)/admin/templates/page.tsx b/frontend/app/(main)/admin/templates/page.tsx index 800c84ac..c06fda1d 100644 --- a/frontend/app/(main)/admin/templates/page.tsx +++ b/frontend/app/(main)/admin/templates/page.tsx @@ -145,27 +145,28 @@ export default function TemplatesManagePage() { } return ( -
- {/* 헤더 */} -
-
-

템플릿 관리

-

화면 디자이너에서 사용할 템플릿을 관리합니다.

+
+
+ {/* 페이지 제목 */} +
+
+

템플릿 관리

+

화면 디자이너에서 사용할 템플릿을 관리합니다

+
+
+ +
-
- -
-
{/* 필터 및 검색 */} - - + + - + 필터 및 검색 @@ -230,8 +231,8 @@ export default function TemplatesManagePage() { {/* 템플릿 목록 테이블 */} - - + + 템플릿 목록 ({filteredAndSortedTemplates.length}개) @@ -390,6 +391,7 @@ export default function TemplatesManagePage() {
+
); } diff --git a/frontend/app/(main)/admin/userMng/page.tsx b/frontend/app/(main)/admin/userMng/page.tsx index 94f861cc..3348148a 100644 --- a/frontend/app/(main)/admin/userMng/page.tsx +++ b/frontend/app/(main)/admin/userMng/page.tsx @@ -8,8 +8,17 @@ import { UserManagement } from "@/components/admin/UserManagement"; */ export default function UserMngPage() { return ( -
- +
+
+ {/* 페이지 제목 */} +
+
+

사용자 관리

+

시스템 사용자 계정 및 권한을 관리합니다

+
+
+ +
); } diff --git a/frontend/app/(main)/admin/validation-demo/page.tsx b/frontend/app/(main)/admin/validation-demo/page.tsx index bb567d63..e903bb4e 100644 --- a/frontend/app/(main)/admin/validation-demo/page.tsx +++ b/frontend/app/(main)/admin/validation-demo/page.tsx @@ -54,7 +54,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: true, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -72,7 +72,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: true, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -94,7 +94,7 @@ const TEST_COMPONENTS: ComponentData[] = [ }, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -112,7 +112,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: false, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -130,7 +130,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: false, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -152,7 +152,7 @@ const TEST_COMPONENTS: ComponentData[] = [ }, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 90195801..e5b622a6 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -237,7 +237,7 @@ export default function ScreenViewPage() { const labelText = component.style?.labelText || component.label || ""; const labelStyle = { fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: component.style?.labelFontWeight || "500", backgroundColor: component.style?.labelBackgroundColor || "transparent", padding: component.style?.labelPadding || "0", diff --git a/frontend/components/admin/MenuManagement.tsx b/frontend/components/admin/MenuManagement.tsx index eb6d72de..c92f0a2d 100644 --- a/frontend/components/admin/MenuManagement.tsx +++ b/frontend/components/admin/MenuManagement.tsx @@ -821,8 +821,11 @@ export const MenuManagement: React.FC = () => { {/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
-

{getUITextSync("menu.type.title")}

-
+ + + {getUITextSync("menu.type.title")} + + {
-
+ +
{/* 우측 메인 영역 - 메뉴 목록 (80%) */}
-
-

- {getMenuTypeString()} {getUITextSync("menu.list.title")} -

-
- - {/* 검색 및 필터 영역 */} -
+ + + + {getMenuTypeString()} {getUITextSync("menu.list.title")} + + + + {/* 검색 및 필터 영역 */} +
@@ -997,52 +1002,54 @@ export const MenuManagement: React.FC = () => {
-
+
-
-
-
- {getUITextSync("menu.list.total", { count: getCurrentMenus().length })} -
-
- - {selectedMenus.size > 0 && ( - + {selectedMenus.size > 0 && ( + )} - - )} +
+
+
-
- -
+ +
@@ -1050,8 +1057,15 @@ export const MenuManagement: React.FC = () => { {/* 화면 할당 탭 */} - - + + + + 화면 할당 + + + + + diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 7d537098..5ac4c6cb 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, Suspense } from "react"; +import { useState, Suspense, useEffect } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { @@ -197,8 +197,27 @@ function AppLayoutInner({ children }: AppLayoutProps) { const searchParams = useSearchParams(); const { user, logout, refreshUserData } = useAuth(); const { userMenus, adminMenus, loading, refreshMenus } = useMenu(); - const [sidebarOpen, setSidebarOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(true); const [expandedMenus, setExpandedMenus] = useState>(new Set()); + const [isMobile, setIsMobile] = useState(false); + + // 화면 크기 감지 및 사이드바 초기 상태 설정 + useEffect(() => { + const checkIsMobile = () => { + const mobile = window.innerWidth < 1024; // lg 브레이크포인트 + setIsMobile(mobile); + // 모바일에서만 사이드바를 닫음 + if (mobile) { + setSidebarOpen(false); + } else { + setSidebarOpen(true); + } + }; + + checkIsMobile(); + window.addEventListener('resize', checkIsMobile); + return () => window.removeEventListener('resize', checkIsMobile); + }, []); // 프로필 관련 로직 const { @@ -253,15 +272,10 @@ function AppLayoutInner({ children }: AppLayoutProps) { ? `/screens/${firstScreen.screenId}?mode=admin` : `/screens/${firstScreen.screenId}`; - console.log("🎯 메뉴에서 화면으로 이동:", { - menuName: menu.name, - screenId: firstScreen.screenId, - isAdminMode, - targetPath: screenPath, - }); - router.push(screenPath); - setSidebarOpen(false); + if (isMobile) { + setSidebarOpen(false); + } return; } } catch (error) { @@ -271,10 +285,11 @@ function AppLayoutInner({ children }: AppLayoutProps) { // 할당된 화면이 없고 URL이 있으면 기존 URL로 이동 if (menu.url && menu.url !== "#") { router.push(menu.url); - setSidebarOpen(false); + if (isMobile) { + setSidebarOpen(false); + } } else { // URL도 없고 할당된 화면도 없으면 경고 메시지 - console.warn("메뉴에 URL이나 할당된 화면이 없습니다:", menu); toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다."); } } @@ -295,7 +310,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { await logout(); router.push("/login"); } catch (error) { - console.error("로그아웃 실패:", error); + // 로그아웃 실패 시 처리 } }; @@ -306,7 +321,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { return (
0 ? "ml-6" : ""}`} onClick={() => handleMenuClick(menu)} > -
+
{menu.icon} - {menu.name} + {menu.name}
{menu.hasChildren && (
@@ -339,8 +354,10 @@ function AppLayoutInner({ children }: AppLayoutProps) { }`} onClick={() => handleMenuClick(child)} > - {child.icon} - {child.name} +
+ {child.icon} + {child.name} +
))}
@@ -369,22 +386,29 @@ function AppLayoutInner({ children }: AppLayoutProps) { {/* MainHeader 컴포넌트 사용 */} setSidebarOpen(!sidebarOpen)} + onSidebarToggle={() => { + // 모바일에서만 토글 동작 + if (isMobile) { + setSidebarOpen(!sidebarOpen); + } + }} onProfileClick={openProfileModal} onLogout={handleLogout} />
{/* 모바일 사이드바 오버레이 */} - {sidebarOpen && ( + {sidebarOpen && isMobile && (
setSidebarOpen(false)} /> )} {/* 왼쪽 사이드바 */} {/* 가운데 컨텐츠 영역 */} -
{children}
+
{children}
{/* 프로필 수정 모달 */} diff --git a/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx b/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx index 93475d3f..8c553b3c 100644 --- a/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx +++ b/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx @@ -232,7 +232,7 @@ export const EnhancedInteractiveScreenViewer: React.FC = ( // 라벨 스타일 적용 const labelStyle = { fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: component.style?.labelFontWeight || "500", backgroundColor: component.style?.labelBackgroundColor || "transparent", padding: component.style?.labelPadding || "0", diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 409c6056..5260b3e5 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -32,6 +32,7 @@ interface RealtimePreviewProps { selectedScreen?: any; onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러 onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러 + onConfigChange?: (config: any) => void; // 설정 변경 핸들러 } // 동적 위젯 타입 아이콘 (레지스트리에서 조회) @@ -73,6 +74,7 @@ export const RealtimePreviewDynamic: React.FC = ({ selectedScreen, onZoneComponentDrop, onZoneClick, + onConfigChange, }) => { const { id, type, position, size, style: componentStyle } = component; @@ -89,8 +91,12 @@ export const RealtimePreviewDynamic: React.FC = ({ const baseStyle = { left: `${position.x}px`, top: `${position.y}px`, - width: `${size?.width || 100}px`, - height: `${size?.height || 36}px`, + width: component.componentConfig?.type === "table-list" + ? `${Math.max(size?.width || 400, 400)}px` // table-list는 최소 400px + : `${size?.width || 100}px`, + height: component.componentConfig?.type === "table-list" + ? `${Math.max(size?.height || 300, 300)}px` // table-list는 최소 300px + : `${size?.height || 36}px`, zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상 ...componentStyle, }; @@ -120,7 +126,9 @@ export const RealtimePreviewDynamic: React.FC = ({ onDragEnd={handleDragEnd} > {/* 동적 컴포넌트 렌더링 */} -
+
= ({ selectedScreen={selectedScreen} onZoneComponentDrop={onZoneComponentDrop} onZoneClick={onZoneClick} + onConfigChange={onConfigChange} />
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 8680c0cd..000ebc44 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -664,6 +664,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType, webType: col.webType || col.web_type, + input_type: col.inputType || col.input_type, // 🎯 input_type 필드 추가 widgetType: col.widgetType || col.widget_type || col.webType || col.web_type, isNullable: col.isNullable || col.is_nullable, required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", @@ -1003,7 +1004,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1082,7 +1083,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1133,7 +1134,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1184,7 +1185,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1273,7 +1274,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1563,7 +1564,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "4px", }, @@ -1652,7 +1653,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", }, @@ -1843,7 +1844,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelFontSize: "12px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "6px", }, @@ -1886,7 +1887,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, // 테이블 패널에서 드래그한 컴포넌트는 라벨을 기본적으로 표시 labelFontSize: "12px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "6px", }, @@ -3157,11 +3158,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD {/* 실제 작업 캔버스 (해상도 크기) */}
{ if (e.target === e.currentTarget && !selectionDrag.wasSelecting) { setSelectedComponent(null); @@ -3270,6 +3275,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD selectedScreen={selectedScreen} // onZoneComponentDrop 제거 onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) + onConfigChange={(config) => { + console.log("📤 테이블 설정 변경을 상세설정에 알림:", config); + // 여기서 DetailSettingsPanel의 상태를 업데이트하거나 + // 컴포넌트의 componentConfig를 업데이트할 수 있습니다 + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} > {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} {(component.type === "group" || component.type === "container" || component.type === "area") && @@ -3350,6 +3362,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD selectedScreen={selectedScreen} // onZoneComponentDrop 제거 onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (자식 컴포넌트용) + onConfigChange={(config) => { + console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} /> ); })} diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 7c819eb2..10cabc52 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -134,7 +134,25 @@ export const ButtonConfigPanel: React.FC = ({ component, updateConfig("entityName", e.target.value)} - placeholder="예: User, Company, Product" + id="referenceTable" + value={localValues.referenceTable} + onChange={(e) => updateConfig("referenceTable", e.target.value)} + placeholder="예: user_info, company_info" className="mt-1" />
- {/* API 엔드포인트 */} + {/* 조인 컬럼 (값 필드) */}
-
- {/* 필드 설정 */} -
-
- - updateConfig("valueField", e.target.value)} - placeholder="id" - className="mt-1" - /> + {/* 표시 컬럼들 (다중 선택) */} +
+ + + {/* 현재 선택된 표시 컬럼들 */} +
+ {localValues.displayColumns.map((column, index) => ( +
+ + {column} + +
+ ))} + + {localValues.displayColumns.length === 0 && ( +
표시할 컬럼을 추가해주세요
+ )}
-
- + {/* 새 표시 컬럼 추가 */} +
updateConfig("displayField", e.target.value)} - placeholder="name" - className="mt-1" + value={newDisplayColumn} + onChange={(e) => setNewDisplayColumn(e.target.value)} + placeholder="컬럼명 입력 (예: user_name, dept_name)" + className="flex-1" /> +
+ +
+ • 여러 컬럼을 선택하면 "{localValues.separator || " - "}"로 구분하여 표시됩니다 +
• 예: 이름{localValues.separator || " - "}부서명 +
+
+ + {/* 구분자 설정 */} +
+ + updateConfig("separator", e.target.value)} + placeholder=" - " + className="mt-1" + />
{/* 표시 형식 */} @@ -225,93 +260,28 @@ export const EntityTypeConfigPanel: React.FC = ({ co />
- {/* 옵션들 */} -
-
- - updateConfig("searchable", !!checked)} - /> -
- -
- - updateConfig("multiple", !!checked)} - /> -
- -
- - updateConfig("allowClear", !!checked)} - /> -
-
- - {/* 최대 선택 개수 (다중 선택 시) */} - {localValues.multiple && ( -
- - updateConfig("maxSelections", e.target.value ? Number(e.target.value) : undefined)} - className="mt-1" - placeholder="제한 없음" - /> -
- )} - {/* 필터 관리 */}
{/* 기존 필터 목록 */}
- {(safeConfig.filters || []).map((filter, index) => ( -
+ {Object.entries(safeConfig.filters || {}).map(([field, value]) => ( +
updateFilter(index, "field", e.target.value)} + value={field} + onChange={(e) => updateFilter(field, e.target.value, value as string)} placeholder="필드명" className="flex-1" /> - + = updateFilter(index, "value", e.target.value)} + value={value as string} + onChange={(e) => updateFilter(field, field, e.target.value)} placeholder="값" className="flex-1" /> -
@@ -326,21 +296,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder="필드명" className="flex-1" /> - + = setNewFilter((prev) => ({ ...prev, value: e.target.value }))} @@ -352,7 +308,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co
-
총 {(safeConfig.filters || []).length}개 필터
+
총 {Object.keys(safeConfig.filters || {}).length}개 필터
{/* 미리보기 */} @@ -360,31 +316,33 @@ export const EntityTypeConfigPanel: React.FC = ({ co
- {localValues.searchable && } +
- {localValues.placeholder || `${localValues.entityName || "엔터티"}를 선택하세요`} + {localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
- 엔터티: {localValues.entityName || "없음"}, API: {localValues.apiEndpoint || "없음"}, 값필드:{" "} - {localValues.valueField}, 표시필드: {localValues.displayField} - {localValues.multiple && `, 다중선택`} - {localValues.searchable && `, 검색가능`} + 참조테이블: {localValues.referenceTable || "없음"}, 조인컬럼: {localValues.referenceColumn} +
+ 표시컬럼:{" "} + {localValues.displayColumns.length > 0 + ? localValues.displayColumns.join(localValues.separator || " - ") + : "없음"}
{/* 안내 메시지 */}
-
엔터티 참조 설정
+
엔터티 타입 설정 가이드
- • 엔터티 참조는 다른 테이블의 데이터를 선택할 때 사용됩니다 + • 참조 테이블: 데이터를 가져올 다른 테이블 이름 +
조인 컬럼: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID) +
표시 컬럼: 사용자에게 보여질 컬럼들 (여러 개 가능)
- • API 엔드포인트를 통해 데이터를 동적으로 로드합니다 -
- • 필터를 사용하여 표시할 데이터를 제한할 수 있습니다 -
• 값 필드는 실제 저장되는 값, 표시 필드는 사용자에게 보여지는 값입니다 + • 여러 표시 컬럼 설정 시 화면마다 다르게 표시할 수 있습니다 +
• 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시
diff --git a/frontend/lib/api/entityJoin.ts b/frontend/lib/api/entityJoin.ts index ab531b29..dee758a5 100644 --- a/frontend/lib/api/entityJoin.ts +++ b/frontend/lib/api/entityJoin.ts @@ -67,6 +67,7 @@ export const entityJoinApi = { sourceColumn: string; joinAlias: string; }>; + screenEntityConfigs?: Record; // 🎯 화면별 엔티티 설정 } = {}, ): Promise => { const searchParams = new URLSearchParams(); @@ -93,6 +94,7 @@ export const entityJoinApi = { ...params, search: params.search ? JSON.stringify(params.search) : undefined, additionalJoinColumns: params.additionalJoinColumns ? JSON.stringify(params.additionalJoinColumns) : undefined, + screenEntityConfigs: params.screenEntityConfigs ? JSON.stringify(params.screenEntityConfigs) : undefined, // 🎯 화면별 엔티티 설정 }, }); return response.data.data; diff --git a/frontend/lib/hooks/useEntityJoinOptimization.ts b/frontend/lib/hooks/useEntityJoinOptimization.ts index 18e63275..1446d1c3 100644 --- a/frontend/lib/hooks/useEntityJoinOptimization.ts +++ b/frontend/lib/hooks/useEntityJoinOptimization.ts @@ -56,6 +56,9 @@ export function useEntityJoinOptimization(columnMeta: Record()); // 공통 코드 카테고리 추출 (메모이제이션) const codeCategories = useMemo(() => { @@ -175,29 +178,41 @@ export function useEntityJoinOptimization(columnMeta: Record ({ - code_value: item.code_value, - code_name: item.code_name, - })), - ); + if (totalRequests.current % 10 === 1) { + console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`); + console.log( + `🔍 캐시 배열 내용:`, + syncResult.map((item) => ({ + code_value: item.code_value, + code_name: item.code_name, + })), + ); + } // 배열에서 해당 code_value를 가진 항목 찾기 const foundCode = syncResult.find( @@ -205,7 +220,13 @@ export function useEntityJoinOptimization(columnMeta: Record void; [key: string]: any; }): React.ReactElement; } @@ -170,6 +172,7 @@ export const DynamicComponentRenderer: React.FC = selectedRowsData, onSelectedRowsChange, refreshKey, + onConfigChange, ...safeProps } = props; @@ -224,6 +227,8 @@ export const DynamicComponentRenderer: React.FC = selectedRows={selectedRows} selectedRowsData={selectedRowsData} onSelectedRowsChange={onSelectedRowsChange} + // 설정 변경 핸들러 전달 + onConfigChange={onConfigChange} refreshKey={refreshKey} /> ); diff --git a/frontend/lib/registry/components/accordion-basic/AccordionBasicComponent.tsx b/frontend/lib/registry/components/accordion-basic/AccordionBasicComponent.tsx index 76ced3ae..ad919d95 100644 --- a/frontend/lib/registry/components/accordion-basic/AccordionBasicComponent.tsx +++ b/frontend/lib/registry/components/accordion-basic/AccordionBasicComponent.tsx @@ -661,7 +661,7 @@ export const AccordionBasicComponent: React.FC = ( top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 1b751b7c..caf2d5f5 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -86,12 +86,69 @@ export const ButtonPrimaryComponent: React.FC = ({ }; }, []); + // 삭제 액션 감지 로직 (실제 필드명 사용) + const isDeleteAction = () => { + const deleteKeywords = ['삭제', 'delete', 'remove', '제거', 'del']; + return ( + component.componentConfig?.action?.type === 'delete' || + component.config?.action?.type === 'delete' || + component.webTypeConfig?.actionType === 'delete' || + component.text?.toLowerCase().includes('삭제') || + component.text?.toLowerCase().includes('delete') || + component.label?.toLowerCase().includes('삭제') || + component.label?.toLowerCase().includes('delete') || + deleteKeywords.some(keyword => + component.config?.buttonText?.toLowerCase().includes(keyword) || + component.config?.text?.toLowerCase().includes(keyword) + ) + ); + }; + + // 삭제 액션일 때 라벨 색상 자동 설정 + useEffect(() => { + if (isDeleteAction() && !component.style?.labelColor) { + // 삭제 액션이고 라벨 색상이 설정되지 않은 경우 빨간색으로 자동 설정 + if (component.style) { + component.style.labelColor = '#ef4444'; + } else { + component.style = { labelColor: '#ef4444' }; + } + } + }, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]); + // 컴포넌트 설정 const componentConfig = { ...config, ...component.config, } as ButtonPrimaryConfig; + // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) + const getLabelColor = () => { + if (isDeleteAction()) { + return component.style?.labelColor || '#ef4444'; // 빨간색 기본값 (Tailwind red-500) + } + return component.style?.labelColor || '#3b83f6'; // 기본 파란색 (Tailwind blue-500) + }; + + const buttonColor = getLabelColor(); + + // 그라데이션용 어두운 색상 계산 + const getDarkColor = (baseColor: string) => { + const hex = baseColor.replace('#', ''); + const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40); + const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40); + const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40); + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + }; + + const buttonDarkColor = getDarkColor(buttonColor); + + console.log("🎨 동적 색상 연동:", { + labelColor: component.style?.labelColor, + buttonColor, + buttonDarkColor, + }); + // 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환 const processedConfig = { ...componentConfig }; if (componentConfig.action && typeof componentConfig.action === "string") { @@ -368,26 +425,29 @@ export const ButtonPrimaryComponent: React.FC = ({ style={{ width: "100%", height: "100%", - minHeight: "100%", // 최소 높이 강제 적용 - maxHeight: "100%", // 최대 높이 제한 - border: "1px solid #3b82f6", - borderRadius: "4px", - backgroundColor: "#3b82f6", - color: "white", + minHeight: "100%", + maxHeight: "100%", + border: "none", + borderRadius: "8px", + background: componentConfig.disabled + ? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)" + : `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`, + color: componentConfig.disabled ? "#9ca3af" : "white", fontSize: "14px", - fontWeight: "500", + fontWeight: "600", cursor: componentConfig.disabled ? "not-allowed" : "pointer", outline: "none", - boxSizing: "border-box", // 패딩/보더 포함 크기 계산 - display: "flex", // flex로 변경 - alignItems: "center", // 세로 중앙 정렬 - justifyContent: "center", // 가로 중앙 정렬 - padding: "0", // 패딩 제거 - margin: "0", // 마진 제거 - lineHeight: "1", // 라인 높이 고정 - // 강제 높이 적용 + boxSizing: "border-box", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "0 16px", + margin: "0", + lineHeight: "1", minHeight: "36px", - height: "36px", + boxShadow: componentConfig.disabled + ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" + : `0 2px 4px 0 ${buttonColor}33`, // 33은 20% 투명도 // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), }} diff --git a/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx b/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx index 8c36e896..04802a64 100644 --- a/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx +++ b/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx @@ -84,7 +84,7 @@ export const CheckboxBasicComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), @@ -141,7 +141,7 @@ export const CheckboxBasicComponent: React.FC = ({ /> = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/lib/registry/components/divider-line/DividerLineComponent.tsx b/frontend/lib/registry/components/divider-line/DividerLineComponent.tsx index 9e82e2f4..136cb708 100644 --- a/frontend/lib/registry/components/divider-line/DividerLineComponent.tsx +++ b/frontend/lib/registry/components/divider-line/DividerLineComponent.tsx @@ -81,7 +81,7 @@ export const DividerLineComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index 514dbb66..6e3d39a8 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -84,7 +84,7 @@ export const FileUploadComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), diff --git a/frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx b/frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx index a1312552..5ba7e37c 100644 --- a/frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx +++ b/frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx @@ -81,7 +81,7 @@ export const ImageDisplayComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/lib/registry/components/number-input/NumberInputComponent.tsx b/frontend/lib/registry/components/number-input/NumberInputComponent.tsx index 40047392..05aa96b0 100644 --- a/frontend/lib/registry/components/number-input/NumberInputComponent.tsx +++ b/frontend/lib/registry/components/number-input/NumberInputComponent.tsx @@ -91,7 +91,7 @@ export const NumberInputComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/lib/registry/components/radio-basic/RadioBasicComponent.tsx b/frontend/lib/registry/components/radio-basic/RadioBasicComponent.tsx index a881e1a5..29fe0dda 100644 --- a/frontend/lib/registry/components/radio-basic/RadioBasicComponent.tsx +++ b/frontend/lib/registry/components/radio-basic/RadioBasicComponent.tsx @@ -84,7 +84,7 @@ export const RadioBasicComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), @@ -155,7 +155,7 @@ export const RadioBasicComponent: React.FC = ({ /> = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), diff --git a/frontend/lib/registry/components/slider-basic/SliderBasicComponent.tsx b/frontend/lib/registry/components/slider-basic/SliderBasicComponent.tsx index c7beb8f8..0dd4f35b 100644 --- a/frontend/lib/registry/components/slider-basic/SliderBasicComponent.tsx +++ b/frontend/lib/registry/components/slider-basic/SliderBasicComponent.tsx @@ -84,7 +84,7 @@ export const SliderBasicComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), @@ -149,7 +149,7 @@ export const SliderBasicComponent: React.FC = ({ width: "30%", textAlign: "center", fontSize: "14px", - color: "#374151", + color: "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), diff --git a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx index 51cfdb6b..b4be9bcb 100644 --- a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx @@ -22,6 +22,7 @@ interface SingleTableWithStickyProps { renderCheckboxCell: (row: any, index: number) => React.ReactNode; formatCellValue: (value: any, format?: string, columnName?: string) => string; getColumnWidth: (column: ColumnConfig) => number; + containerWidth?: string; // 컨테이너 너비 설정 } export const SingleTableWithSticky: React.FC = ({ @@ -39,12 +40,28 @@ export const SingleTableWithSticky: React.FC = ({ renderCheckboxCell, formatCellValue, getColumnWidth, + containerWidth, }) => { const checkboxConfig = tableConfig.checkbox || {}; return ( -
-
+
+
{visibleColumns.map((column, colIndex) => { @@ -81,6 +98,9 @@ export const SingleTableWithSticky: React.FC = ({ width: getColumnWidth(column), minWidth: getColumnWidth(column), maxWidth: getColumnWidth(column), + boxSizing: "border-box", + overflow: "hidden", + textOverflow: "ellipsis", // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), @@ -90,7 +110,7 @@ export const SingleTableWithSticky: React.FC = ({
{column.columnName === "__checkbox__" ? ( checkboxConfig.selectAll && ( - + ) ) : ( <> @@ -167,6 +187,11 @@ export const SingleTableWithSticky: React.FC = ({ minHeight: "40px", height: "40px", verticalAlign: "middle", + width: getColumnWidth(column), + boxSizing: "border-box", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 813d36d2..a3d63041 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from "react"; import { TableListConfig, ColumnConfig } from "./types"; +import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; @@ -22,7 +23,6 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; -import { Separator } from "@/components/ui/separator"; import { SingleTableWithSticky } from "./SingleTableWithSticky"; export interface TableListComponentProps { @@ -54,6 +54,9 @@ export interface TableListComponentProps { // 선택된 행 정보 전달 핸들러 onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; + // 설정 변경 핸들러 (상세설정과 연동) + onConfigChange?: (config: any) => void; + // 테이블 새로고침 키 refreshKey?: number; } @@ -75,6 +78,7 @@ export const TableListComponent: React.FC = ({ onFormDataChange, componentConfig, onSelectedRowsChange, + onConfigChange, refreshKey, }) => { // 컴포넌트 설정 @@ -84,6 +88,17 @@ export const TableListComponent: React.FC = ({ ...componentConfig, } as TableListConfig; + // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) + const buttonColor = component.style?.labelColor || '#3b83f6'; // 기본 파란색 + const buttonTextColor = component.config?.buttonTextColor || '#ffffff'; + const buttonStyle = { + backgroundColor: buttonColor, + color: buttonTextColor, + borderColor: buttonColor + }; + + // 디버깅 로그 제거 (성능상 이유로) + // 상태 관리 const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); @@ -98,13 +113,20 @@ export const TableListComponent: React.FC = ({ const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태 const [displayColumns, setDisplayColumns] = useState([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨) + + // 🎯 조인 컬럼 매핑 상태 + const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리) // 고급 필터 관련 state const [searchValues, setSearchValues] = useState>({}); // 체크박스 상태 관리 - const [selectedRows, setSelectedRows] = useState>(new Set()); // 선택된 행들의 키 집합 + const [selectedRows, setSelectedRows] = useState>(new Set()); + + // 드래그 상태 관리 + const [isDragging, setIsDragging] = useState(false); + const [draggedRowIndex, setDraggedRowIndex] = useState(null); // 선택된 행들의 키 집합 const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태 // 🎯 Entity 조인 최적화 훅 사용 @@ -116,10 +138,9 @@ export const TableListComponent: React.FC = ({ // 높이 계산 함수 (메모이제이션) const optimalHeight = useMemo(() => { - // 50개 이상일 때는 20개 기준으로 높이 고정하고 스크롤 생성 - // 50개 미만일 때는 실제 데이터 개수에 맞춰서 스크롤 없이 표시 - const actualDataCount = Math.min(data.length, localPageSize); - const displayPageSize = localPageSize >= 50 ? 20 : Math.max(actualDataCount, 5); + // 실제 데이터 개수에 맞춰서 높이 계산 (최소 5개, 최대 20개) + const actualDataCount = data.length; + const displayPageSize = Math.min(Math.max(actualDataCount, 5), 20); const headerHeight = 50; // 테이블 헤더 const rowHeight = 42; // 각 행 높이 @@ -135,7 +156,7 @@ export const TableListComponent: React.FC = ({ actualDataCount, localPageSize, displayPageSize, - willHaveScroll: localPageSize >= 50, + isDesignMode, titleHeight, searchHeight, headerHeight, @@ -156,25 +177,70 @@ export const TableListComponent: React.FC = ({ }); return calculatedHeight; - }, [data.length, localPageSize, tableConfig.filter?.enabled, tableConfig.showFooter, tableConfig.showHeader]); + }, []); - // 스타일 계산 + // 🎯 강제로 그리드 컬럼수에 맞는 크기 적용 (디자인 모드에서는 더 큰 크기 허용) + const gridColumns = component.gridColumns || 1; + let calculatedWidth: string; + + if (isDesignMode) { + // 디자인 모드에서는 더 큰 최소 크기 적용 + if (gridColumns === 1) { + calculatedWidth = "400px"; // 1컬럼일 때 400px (디자인 모드) + } else if (gridColumns === 2) { + calculatedWidth = "600px"; // 2컬럼일 때 600px (디자인 모드) + } else if (gridColumns <= 6) { + calculatedWidth = `${gridColumns * 250}px`; // 컬럼당 250px (디자인 모드) + } else { + calculatedWidth = "100%"; // 7컬럼 이상은 전체 + } + } else { + // 일반 모드는 기존 크기 유지 + if (gridColumns === 1) { + calculatedWidth = "200px"; // 1컬럼일 때 200px 고정 + } else if (gridColumns === 2) { + calculatedWidth = "400px"; // 2컬럼일 때 400px + } else if (gridColumns <= 6) { + calculatedWidth = `${gridColumns * 200}px`; // 컬럼당 200px + } else { + calculatedWidth = "100%"; // 7컬럼 이상은 전체 + } + } + + // 디버깅 로그 제거 (성능상 이유로) + + + // 스타일 계산 (컨테이너에 맞춤) const componentStyle: React.CSSProperties = { - width: "100%", - height: `${optimalHeight}px`, // 20개 데이터를 모두 보여주는 높이 - minHeight: `${optimalHeight}px`, // 최소 높이 보장 + width: "100%", // 컨테이너 전체 너비 사용 + maxWidth: "100%", // 최대 너비 제한 + height: "auto", // 항상 자동 높이로 테이블 크기에 맞춤 + minHeight: isDesignMode ? `${Math.min(optimalHeight, 400)}px` : `${optimalHeight}px`, // 최소 높이 보장 ...component.style, ...style, display: "flex", flexDirection: "column", boxSizing: "border-box", // 패딩/보더 포함한 크기 계산 + // overflow는 CSS 클래스로 처리 }; + // 🎯 tableContainerStyle 제거 - componentStyle만 사용 + // 디자인 모드 스타일 if (isDesignMode) { - componentStyle.border = "1px dashed #cbd5e1"; + componentStyle.border = "2px dashed #cbd5e1"; componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; - // minHeight 제거 - 실제 데이터에 맞는 높이 사용 + componentStyle.borderRadius = "8px"; + componentStyle.padding = "4px"; // 약간의 패딩으로 구분감 확보 + componentStyle.margin = "2px"; // 외부 여백으로 레이아웃과 구분 + // 🎯 컨테이너에 맞춤 + componentStyle.width = "calc(100% - 12px)"; // margin + padding 보정 + componentStyle.maxWidth = "calc(100% - 12px)"; + componentStyle.minWidth = "calc(100% - 12px)"; + componentStyle.overflow = "hidden !important"; // 넘치는 부분 숨김 (강제) + componentStyle.boxSizing = "border-box"; // 패딩 포함 크기 계산 + componentStyle.position = "relative"; // 위치 고정 + // 자동 높이로 테이블 전체를 감쌈 } // 컬럼 라벨 정보 가져오기 @@ -192,12 +258,12 @@ export const TableListComponent: React.FC = ({ // 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용 let displayLabel = column.displayName || column.columnName; - // Entity 타입이고 display_column이 있는 경우 - if (column.webType === "entity" && column.displayColumn) { - // 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 displayColumn 사용 - displayLabel = column.displayColumnLabel || column.displayColumn; + // Entity 타입인 경우 + if (column.webType === "entity") { + // 우선 기준 테이블의 컬럼 라벨을 사용 + displayLabel = column.displayName || column.columnName; console.log( - `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn})`, + `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (기준 테이블 라벨 사용)`, ); } @@ -254,13 +320,77 @@ export const TableListComponent: React.FC = ({ // Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들) const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || []; - const additionalJoinColumns = entityJoinColumns.map((col) => ({ - sourceTable: col.entityJoinInfo!.sourceTable, - sourceColumn: col.entityJoinInfo!.sourceColumn, - joinAlias: col.entityJoinInfo!.joinAlias, - })); + // 🎯 조인 탭에서 추가한 컬럼들도 포함 (실제로 존재하는 컬럼만) + const joinTabColumns = + tableConfig.columns?.filter( + (col) => + !col.isEntityJoin && + col.columnName.includes("_") && + (col.columnName.includes("dept_code_") || + col.columnName.includes("_dept_code") || + col.columnName.includes("_company_") || + col.columnName.includes("_user_")), // 조인 탭에서 추가한 컬럼 패턴들 + ) || []; + + console.log( + "🔍 조인 탭 컬럼들:", + joinTabColumns.map((c) => c.columnName), + ); + + const additionalJoinColumns = [ + ...entityJoinColumns.map((col) => ({ + sourceTable: col.entityJoinInfo!.sourceTable, + sourceColumn: col.entityJoinInfo!.sourceColumn, + joinAlias: col.entityJoinInfo!.joinAlias, + })), + // 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) + ...joinTabColumns + .filter((col) => { + // 실제 API 응답에 존재하는 컬럼만 필터링 + const validJoinColumns = ["dept_code_name", "dept_name"]; + const isValid = validJoinColumns.includes(col.columnName); + if (!isValid) { + console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`); + } + return isValid; + }) + .map((col) => { + // 실제 존재하는 조인 컬럼만 처리 + let sourceTable = tableConfig.selectedTable; + let sourceColumn = col.columnName; + + if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { + sourceTable = "dept_info"; + sourceColumn = "dept_code"; + } + + console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); + + return { + sourceTable: sourceTable || tableConfig.selectedTable || "", + sourceColumn: sourceColumn, + joinAlias: col.columnName, + }; + }), + ]; + + // 🎯 화면별 엔티티 표시 설정 생성 + const screenEntityConfigs: Record = {}; + entityJoinColumns.forEach((col) => { + if (col.entityDisplayConfig) { + const sourceColumn = col.entityJoinInfo!.sourceColumn; + screenEntityConfigs[sourceColumn] = { + displayColumns: col.entityDisplayConfig.displayColumns, + separator: col.entityDisplayConfig.separator || " - ", + }; + } + }); + + console.log("🔗 Entity 조인 컬럼:", entityJoinColumns); + console.log("🔗 조인 탭 컬럼:", joinTabColumns); console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns); + console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs); const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page: currentPage, @@ -329,9 +459,14 @@ export const TableListComponent: React.FC = ({ sortOrder: sortDirection, enableEntityJoin: true, // 🎯 Entity 조인 활성화 additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼 + screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정 }); if (result) { + console.log("🎯 API 응답 결과:", result); + console.log("🎯 데이터 개수:", result.data?.length || 0); + console.log("🎯 전체 페이지:", result.totalPages); + console.log("🎯 총 아이템:", result.total); setData(result.data || []); setTotalPages(result.totalPages || 1); setTotalItems(result.total || 0); @@ -369,12 +504,88 @@ export const TableListComponent: React.FC = ({ } } - // 🎯 Entity 조인된 컬럼 처리 + // 🎯 Entity 조인된 컬럼 처리 - 사용자가 설정한 컬럼들만 사용 let processedColumns = [...(tableConfig.columns || [])]; // 초기 컬럼이 있으면 먼저 설정 if (processedColumns.length > 0) { - setDisplayColumns(processedColumns); + console.log( + "🔍 사용자 설정 컬럼들:", + processedColumns.map((c) => c.columnName), + ); + + // 🎯 API 응답과 비교하여 존재하지 않는 컬럼 필터링 + if (result.data.length > 0) { + const actualApiColumns = Object.keys(result.data[0]); + console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns); + + // 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답) + // 실제 API 응답에 존재하는 컬럼만 매핑 + const newJoinColumnMapping: Record = { + dept_code_dept_code: "dept_code", // user_info.dept_code + dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음) + dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음) + dept_code_name: "dept_code_name", // dept_info.dept_name + dept_name: "dept_name", // dept_info.dept_name + status: "status", // user_info.status + }; + + // 🎯 조인 컬럼 매핑 상태 업데이트 + setJoinColumnMapping(newJoinColumnMapping); + + console.log("🔍 조인 컬럼 매핑 테이블:", newJoinColumnMapping); + console.log("🔍 실제 API 응답 컬럼들:", actualApiColumns); + + // 🎯 컬럼명 매핑 및 유효성 검사 + const validColumns = processedColumns + .map((col) => { + // 체크박스는 그대로 유지 + if (col.columnName === "__checkbox__") return col; + + // 조인 컬럼 매핑 적용 + const mappedColumnName = newJoinColumnMapping[col.columnName] || col.columnName; + + console.log(`🔍 컬럼 매핑 처리: ${col.columnName} → ${mappedColumnName}`); + + // API 응답에 존재하는지 확인 + const existsInApi = actualApiColumns.includes(mappedColumnName); + + if (!existsInApi) { + console.log(`🔍 제거될 컬럼: ${col.columnName} → ${mappedColumnName} (API에 존재하지 않음)`); + return null; + } + + // 컬럼명이 변경된 경우 업데이트 + if (mappedColumnName !== col.columnName) { + console.log(`🔄 컬럼명 매핑: ${col.columnName} → ${mappedColumnName}`); + return { + ...col, + columnName: mappedColumnName, + }; + } + + console.log(`✅ 컬럼 유지: ${col.columnName}`); + return col; + }) + .filter((col) => col !== null) as ColumnConfig[]; + + if (validColumns.length !== processedColumns.length) { + console.log( + "🔍 필터링된 컬럼들:", + validColumns.map((c) => c.columnName), + ); + console.log( + "🔍 제거된 컬럼들:", + processedColumns + .filter((col) => { + const mappedName = newJoinColumnMapping[col.columnName] || col.columnName; + return !actualApiColumns.includes(mappedName) && col.columnName !== "__checkbox__"; + }) + .map((c) => c.columnName), + ); + processedColumns = validColumns; + } + } } if (result.entityJoinInfo?.joinConfigs) { result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { @@ -398,11 +609,11 @@ export const TableListComponent: React.FC = ({ }); } - // 컬럼 정보가 없으면 첫 번째 데이터 행에서 추출 + // 🎯 컬럼 설정이 없으면 API 응답 기반으로 생성 if ((!processedColumns || processedColumns.length === 0) && result.data.length > 0) { const autoColumns: ColumnConfig[] = Object.keys(result.data[0]).map((key, index) => ({ columnName: key, - displayName: columnLabels[key] || key, // 라벨명 우선 사용 + displayName: columnLabels[key] || key, visible: true, sortable: true, searchable: true, @@ -411,6 +622,11 @@ export const TableListComponent: React.FC = ({ order: index, })); + console.log( + "🎯 자동 생성된 컬럼들:", + autoColumns.map((c) => c.columnName), + ); + // 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림) if (onFormDataChange) { onFormDataChange({ @@ -426,6 +642,9 @@ export const TableListComponent: React.FC = ({ // 🎯 표시할 컬럼 상태 업데이트 setDisplayColumns(processedColumns); + console.log("🎯 displayColumns 업데이트됨:", processedColumns); + console.log("🎯 데이터 개수:", result.data?.length || 0); + console.log("🎯 전체 데이터:", result.data); } } catch (err) { console.error("테이블 데이터 로딩 오류:", err); @@ -439,6 +658,20 @@ export const TableListComponent: React.FC = ({ // 페이지 변경 const handlePageChange = (newPage: number) => { setCurrentPage(newPage); + + // 상세설정에 현재 페이지 정보 알림 (필요한 경우) + if (onConfigChange && tableConfig.pagination) { + console.log("📤 테이블에서 페이지 변경을 상세설정에 알림:", newPage); + onConfigChange({ + ...tableConfig, + pagination: { + ...tableConfig.pagination, + currentPage: newPage, // 현재 페이지 정보 추가 + }, + }); + } else if (!onConfigChange) { + console.warn("⚠️ onConfigChange가 정의되지 않음 - 페이지 변경 상세설정과 연동 불가"); + } }; // 정렬 변경 @@ -602,6 +835,22 @@ export const TableListComponent: React.FC = ({ } }, [refreshKey]); + // 상세설정에서 페이지네이션 설정 변경 시 로컬 상태 동기화 + useEffect(() => { + // 페이지 크기 동기화 + if (tableConfig.pagination?.pageSize && tableConfig.pagination.pageSize !== localPageSize) { + console.log("🔄 상세설정에서 페이지 크기 변경 감지:", tableConfig.pagination.pageSize); + setLocalPageSize(tableConfig.pagination.pageSize); + setCurrentPage(1); // 페이지를 1로 리셋 + } + + // 현재 페이지 동기화 (상세설정에서 페이지를 직접 변경한 경우) + if (tableConfig.pagination?.currentPage && tableConfig.pagination.currentPage !== currentPage) { + console.log("🔄 상세설정에서 현재 페이지 변경 감지:", tableConfig.pagination.currentPage); + setCurrentPage(tableConfig.pagination.currentPage); + } + }, [tableConfig.pagination?.pageSize, tableConfig.pagination?.currentPage]); + // 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가 + 숨김 기능) const visibleColumns = useMemo(() => { // 기본값 처리: checkbox 설정이 없으면 기본값 사용 @@ -614,9 +863,22 @@ export const TableListComponent: React.FC = ({ let columns: ColumnConfig[] = []; - if (!displayColumns || displayColumns.length === 0) { - // displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용 - if (!tableConfig.columns) return []; + // displayColumns가 있으면 우선 사용 (Entity 조인 적용된 컬럼들) + if (displayColumns && displayColumns.length > 0) { + // 디버깅 로그 제거 (성능상 이유로) + const filteredColumns = displayColumns.filter((col) => { + // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 + if (isDesignMode) { + return col.visible; // 디자인 모드에서는 visible만 체크 + } else { + return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 + } + }); + // 디버깅 로그 제거 (성능상 이유로) + columns = filteredColumns.sort((a, b) => a.order - b.order); + } else if (tableConfig.columns && tableConfig.columns.length > 0) { + // displayColumns가 없으면 기본 컬럼 사용 + // 디버깅 로그 제거 (성능상 이유로) columns = tableConfig.columns .filter((col) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 @@ -628,16 +890,8 @@ export const TableListComponent: React.FC = ({ }) .sort((a, b) => a.order - b.order); } else { - columns = displayColumns - .filter((col) => { - // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 - if (isDesignMode) { - return col.visible; // 디자인 모드에서는 visible만 체크 - } else { - return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 - } - }) - .sort((a, b) => a.order - b.order); + console.log("🎯 사용할 컬럼이 없음"); + return []; } // 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가 @@ -663,8 +917,9 @@ export const TableListComponent: React.FC = ({ } } + // 디버깅 로그 제거 (성능상 이유로) return columns; - }, [displayColumns, tableConfig.columns, tableConfig.checkbox]); + }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); // columnsByPosition은 SingleTableWithSticky에서 사용하지 않으므로 제거 // 기존 테이블에서만 필요한 경우 다시 추가 가능 @@ -731,7 +986,7 @@ export const TableListComponent: React.FC = ({ return null; } - return ; + return ; }; // 체크박스 셀 렌더링 @@ -756,6 +1011,7 @@ export const TableListComponent: React.FC = ({ checked={isSelected} onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)} aria-label={`행 ${index + 1} 선택`} + style={{ zIndex: 1 }} /> ); }; @@ -765,30 +1021,14 @@ export const TableListComponent: React.FC = ({ return (value: any, format?: string, columnName?: string) => { if (value === null || value === undefined) return ""; - // 디버깅: 모든 값 변환 시도를 로깅 - if ( - columnName && - (columnName === "contract_type" || columnName === "domestic_foreign" || columnName === "status") - ) { - console.log(`🔍 값 변환 시도: ${columnName}="${value}"`, { - columnMeta: columnMeta[columnName], - hasColumnMeta: !!columnMeta[columnName], - webType: columnMeta[columnName]?.webType, - codeCategory: columnMeta[columnName]?.codeCategory, - globalColumnMeta: Object.keys(columnMeta), - }); - } + // 디버깅 로그 제거 (성능상 이유로) // 🎯 코드 컬럼인 경우 최적화된 코드명 변환 사용 if (columnName && columnMeta[columnName]?.webType === "code" && columnMeta[columnName]?.codeCategory) { const categoryCode = columnMeta[columnName].codeCategory!; const convertedValue = optimizedConvertCode(categoryCode, String(value)); - if (convertedValue !== String(value)) { - console.log(`🔄 코드 변환 성공: ${columnName}[${categoryCode}] ${value} → ${convertedValue}`); - } else { - console.log(`⚠️ 코드 변환 실패: ${columnName}[${categoryCode}] ${value} → ${convertedValue} (값 동일)`); - } + // 코드 변환 로그 제거 (성능상 이유로) value = convertedValue; } @@ -821,6 +1061,82 @@ export const TableListComponent: React.FC = ({ } }; + // 드래그 핸들러 (그리드 스냅 지원) + const handleRowDragStart = (e: React.DragEvent, row: any, index: number) => { + setIsDragging(true); + setDraggedRowIndex(index); + + // 드래그 데이터에 그리드 정보 포함 + const dragData = { + ...row, + _dragType: 'table-row', + _gridSize: { width: 4, height: 1 }, // 기본 그리드 크기 (4칸 너비, 1칸 높이) + _snapToGrid: true + }; + + e.dataTransfer.setData('application/json', JSON.stringify(dragData)); + e.dataTransfer.effectAllowed = 'copy'; // move 대신 copy로 변경 + + // 드래그 이미지를 더 깔끔하게 + const dragElement = e.currentTarget as HTMLElement; + + // 커스텀 드래그 이미지 생성 (저장 버튼과 어울리는 스타일) + const dragImage = document.createElement('div'); + dragImage.style.position = 'absolute'; + dragImage.style.top = '-1000px'; + dragImage.style.left = '-1000px'; + dragImage.style.background = 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'; + dragImage.style.color = 'white'; + dragImage.style.padding = '12px 16px'; + dragImage.style.borderRadius = '8px'; + dragImage.style.fontSize = '14px'; + dragImage.style.fontWeight = '600'; + dragImage.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.4)'; + dragImage.style.display = 'flex'; + dragImage.style.alignItems = 'center'; + dragImage.style.gap = '8px'; + dragImage.style.minWidth = '200px'; + dragImage.style.whiteSpace = 'nowrap'; + + // 아이콘과 텍스트 추가 + const firstValue = Object.values(row)[0] || 'Row'; + dragImage.innerHTML = ` +
📋
+ ${firstValue} +
4×1
+ `; + + document.body.appendChild(dragImage); + e.dataTransfer.setDragImage(dragImage, 20, 20); + + // 정리 + setTimeout(() => { + if (document.body.contains(dragImage)) { + document.body.removeChild(dragImage); + } + }, 0); + }; + + const handleRowDragEnd = (e: React.DragEvent) => { + setIsDragging(false); + setDraggedRowIndex(null); + }; + // DOM에 전달할 수 있는 기본 props만 정의 const domProps = { onClick: handleClick, @@ -832,11 +1148,15 @@ export const TableListComponent: React.FC = ({ if (isDesignMode && !tableConfig.selectedTable) { return (
-
-
- -
테이블 리스트
-
설정 패널에서 테이블을 선택해주세요
+
+
+
+ +
+
테이블 리스트
+
+ 설정 패널에서 테이블을 선택해주세요 +
@@ -844,56 +1164,64 @@ export const TableListComponent: React.FC = ({ } return ( -
+
{/* 헤더 */} {tableConfig.showHeader && ( -
+
{(tableConfig.title || tableLabel) && ( -

{tableConfig.title || tableLabel}

+

+ {tableConfig.title || tableLabel} +

)}
-
+
{/* 선택된 항목 정보 표시 */} {selectedRows.size > 0 && ( -
- {selectedRows.size}개 선택됨 +
+ {selectedRows.size}개 선택됨
)} - {/* 검색 - 기존 방식은 주석처리 */} - {/* {tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && ( + {/* 새로고침 */} +
@@ -902,141 +1230,256 @@ export const TableListComponent: React.FC = ({ {/* 고급 검색 필터 - 항상 표시 (컬럼 정보 기반 자동 생성) */} {tableConfig.filter?.enabled && visibleColumns && visibleColumns.length > 0 && ( <> - -
+
+ ({ columnName: col.columnName, - webType: columnMeta[col.columnName]?.webType || "text", + widgetType: (columnMeta[col.columnName]?.webType || "text") as WebType, displayName: columnLabels[col.columnName] || col.displayName || col.columnName, codeCategory: columnMeta[col.columnName]?.codeCategory, isVisible: col.visible, // 추가 메타데이터 전달 (필터 자동 생성용) - web_type: columnMeta[col.columnName]?.webType || "text", + web_type: (columnMeta[col.columnName]?.webType || "text") as WebType, column_name: col.columnName, column_label: columnLabels[col.columnName] || col.displayName || col.columnName, code_category: columnMeta[col.columnName]?.codeCategory, }))} - tableName={tableConfig.selectedTable} - /> + tableName={tableConfig.selectedTable} + /> +
)} {/* 테이블 컨텐츠 */} -
= 50 ? "flex-1 overflow-auto" : ""}`}> +
= 50 ? "flex-1" : ""}`} + style={{ + width: "100%", + maxWidth: "100%", + boxSizing: "border-box" + }} + > {loading ? ( -
-
- -
데이터를 불러오는 중...
+
+
+
+
+ +
+
+
+
데이터를 불러오는 중...
+
잠시만 기다려주세요
) : error ? ( -
-
-
오류가 발생했습니다
-
{error}
+
+
+
+
+ ! +
+
+
오류가 발생했습니다
+
{error}
) : needsHorizontalScroll ? ( // 가로 스크롤이 필요한 경우 - 단일 테이블에서 sticky 컬럼 사용 - +
+ +
) : ( // 기존 테이블 (가로 스크롤이 필요 없는 경우) -
- - - {visibleColumns.map((column) => ( - column.sortable && handleSort(column.columnName)} - > - {column.columnName === "__checkbox__" ? ( - renderCheckboxHeader() - ) : ( -
- {columnLabels[column.columnName] || column.displayName} - {column.sortable && ( -
- {sortColumn === column.columnName ? ( - sortDirection === "asc" ? ( - +
+
+ + + {visibleColumns.map((column, colIndex) => ( + column.sortable && handleSort(column.columnName)} + > + {column.columnName === "__checkbox__" ? ( + renderCheckboxHeader() + ) : ( +
+ + {columnLabels[column.columnName] || column.displayName} + + {column.sortable && ( +
+ {sortColumn === column.columnName ? ( + sortDirection === "asc" ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} -
- )} -
- )} -
- ))} -
-
+ + )} + + )} + + )} + + ))} + + {data.length === 0 ? ( - - 데이터가 없습니다 + +
+
+ +
+
데이터가 없습니다
+
조건을 변경하거나 새로운 데이터를 추가해보세요
+
) : ( data.map((row, index) => ( handleRowDragStart(e, row, index)} + onDragEnd={handleRowDragEnd} className={cn( - "h-10 cursor-pointer leading-none", - tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50", - tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50", + "group relative h-12 cursor-pointer transition-all duration-200 border-b border-gray-100", + // 기본 스타일 + tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-orange-200 hover:to-orange-300/90 hover:shadow-sm", + tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-100/80", + // 드래그 상태 스타일 (미묘하게) + draggedRowIndex === index && "bg-gradient-to-r from-blue-50 to-blue-100/40 shadow-sm border-blue-200", + isDragging && draggedRowIndex !== index && "opacity-70", + // 드래그 가능 표시 + !isDesignMode && "hover:cursor-grab active:cursor-grabbing" )} - style={{ minHeight: "40px", height: "40px", lineHeight: "1" }} + style={{ + minHeight: "48px", + height: "48px", + lineHeight: "1", + width: "100%", + maxWidth: "100%" + }} onClick={() => handleRowClick(row)} > - {visibleColumns.map((column) => ( + {visibleColumns.map((column, colIndex) => ( {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) - : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} + : (() => { + // 🎯 매핑된 컬럼명으로 데이터 찾기 + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + const cellValue = row[mappedColumnName]; + if (index === 0) { + // 디버깅 로그 제거 (성능상 이유로) + } + const formattedValue = formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + + // 첫 번째 컬럼에 드래그 핸들과 아바타 추가 + const isFirstColumn = colIndex === (visibleColumns[0]?.columnName === "__checkbox__" ? 1 : 0); + + return ( +
+ {isFirstColumn && !isDesignMode && ( +
+ {/* 그리드 스냅 가이드 아이콘 */} +
+
+
+
+
+
+
+
+
+
+
+ )} + + {formattedValue} + +
+ ); + })()}
))}
@@ -1044,40 +1487,76 @@ export const TableListComponent: React.FC = ({ )}
+
)}
{/* 푸터/페이지네이션 */} {tableConfig.showFooter && tableConfig.pagination?.enabled && ( -
-
- {tableConfig.pagination?.showPageInfo && ( - - 전체 {totalItems.toLocaleString()}건 중 {(currentPage - 1) * localPageSize + 1}- - {Math.min(currentPage * localPageSize, totalItems)} 표시 +
+ {/* 페이지 정보 - 가운데 정렬 */} + {tableConfig.pagination?.showPageInfo && ( +
+
+ + 전체 {totalItems.toLocaleString()}건 중{" "} + + {(currentPage - 1) * localPageSize + 1}-{Math.min(currentPage * localPageSize, totalItems)} + {" "} + 표시 - )} -
+
+ )} -
- {/* 페이지 크기 선택 */} - {tableConfig.pagination?.showSizeSelector && ( + {/* 페이지 크기 선택과 페이지네이션 버튼 - 가운데 정렬 */} +
+ {/* 페이지 크기 선택 - 임시로 항상 표시 (테스트용) */} + {true && ( updateEntityDisplaySeparator(column.columnName, e.target.value)} + className="h-7 text-xs" + placeholder=" - " + /> +
+ + {/* 기본 테이블 컬럼 */} +
+ +
+ {entityDisplayConfigs[column.columnName].sourceColumns.map((col) => ( +
+ + toggleEntityDisplayColumn(column.columnName, col.columnName) + } + className="h-3 w-3" + /> + +
+ ))} +
+
+ + {/* 조인 테이블 컬럼 */} +
+ +
+ {entityDisplayConfigs[column.columnName].joinColumns.map((col) => ( +
+ + toggleEntityDisplayColumn(column.columnName, col.columnName) + } + className="h-3 w-3" + /> + +
+ ))} +
+
+ + {/* 선택된 컬럼 미리보기 */} + {entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && ( +
+ +
+ {entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => ( + + + {colName} + + {idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && ( + + {entityDisplayConfigs[column.columnName].separator} + + )} + + ))} +
+
+ )} +
+ )} +
+ ))} + + + )} + {!screenTableName ? ( @@ -820,6 +1306,20 @@ export const TableListConfigPanel: React.FC = ({ />
+ {/* 엔티티 타입 컬럼 표시 */} + {column.isEntityJoin && ( +
+
+ + 엔티티 타입 + + + 표시 컬럼 설정은 상단의 "🎯 엔티티 컬럼 표시 설정" 섹션에서 하세요 + +
+
+ )} +
handleChange("color", e.target.value)} />
diff --git a/frontend/lib/registry/components/text-display/index.ts b/frontend/lib/registry/components/text-display/index.ts index c86255f1..9280aa0b 100644 --- a/frontend/lib/registry/components/text-display/index.ts +++ b/frontend/lib/registry/components/text-display/index.ts @@ -24,7 +24,7 @@ export const TextDisplayDefinition = createComponentDefinition({ text: "텍스트를 입력하세요", fontSize: "14px", fontWeight: "normal", - color: "#374151", + color: "#3b83f6", textAlign: "left", }, defaultSize: { width: 150, height: 24 }, diff --git a/frontend/lib/registry/components/text-input/TextInputComponent.tsx b/frontend/lib/registry/components/text-input/TextInputComponent.tsx index 4a5aabf6..f4fe7a9e 100644 --- a/frontend/lib/registry/components/text-input/TextInputComponent.tsx +++ b/frontend/lib/registry/components/text-input/TextInputComponent.tsx @@ -190,7 +190,7 @@ export const TextInputComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx index 04128d74..482280b0 100644 --- a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx +++ b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx @@ -84,7 +84,7 @@ export const TextareaBasicComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), diff --git a/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx b/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx index 8183e1c0..f71a4127 100644 --- a/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx +++ b/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx @@ -84,7 +84,7 @@ export const ToggleSwitchComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), @@ -173,7 +173,7 @@ export const ToggleSwitchComponent: React.FC = ({
{ const element = e.currentTarget; - element.style.borderColor = "#3b82f6"; - element.style.backgroundColor = "rgba(59, 130, 246, 0.02)"; - element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)"; + // 🎯 컴포넌트가 있는 존은 호버 효과 최소화 + if (zoneChildren.length > 0) { + element.style.backgroundColor = "rgba(59, 130, 246, 0.01)"; + } else { + element.style.borderColor = "#3b82f6"; + element.style.backgroundColor = "rgba(59, 130, 246, 0.02)"; + element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)"; + } }} onMouseLeave={(e) => { const element = e.currentTarget; - element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; - element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; + if (zoneChildren.length > 0) { + // 컴포넌트가 있는 존 복원 + element.style.borderColor = "transparent"; + element.style.backgroundColor = isDesignMode ? "rgba(248, 250, 252, 0.3)" : "rgba(248, 250, 252, 0.5)"; + } else { + // 빈 존 복원 + element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; + element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; + } element.style.boxShadow = "none"; }} onDrop={this.handleDrop(zone.id)} diff --git a/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx b/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx index 43488c51..e108b7c7 100644 --- a/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx +++ b/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx @@ -148,7 +148,7 @@ const AccordionSection: React.FC<{ const headerStyle: React.CSSProperties = { padding: "12px 16px", backgroundColor: isDesignMode ? "#3b82f6" : "#f8fafc", - color: isDesignMode ? "white" : "#374151", + color: isDesignMode ? "white" : "#3b83f6", border: "1px solid #e2e8f0", borderBottom: isExpanded ? "none" : "1px solid #e2e8f0", cursor: "pointer", diff --git a/frontend/lib/registry/utils/hotReload.ts b/frontend/lib/registry/utils/hotReload.ts index c4688100..a2f61a29 100644 --- a/frontend/lib/registry/utils/hotReload.ts +++ b/frontend/lib/registry/utils/hotReload.ts @@ -14,6 +14,10 @@ let hotReloadListeners: Array<() => void> = []; * Hot Reload 시스템 초기화 */ export function initializeHotReload(): void { + // 핫 리로드 시스템 임시 비활성화 (디버깅 목적) + console.log("🔥 컴포넌트 Hot Reload 시스템 비활성화됨 (디버깅 모드)"); + return; + if (process.env.NODE_ENV !== "development" || typeof window === "undefined") { return; } @@ -55,11 +59,15 @@ function setupDevServerEventListener(): void { const originalLog = console.log; let reloadPending = false; - // console.log 메시지를 감지하여 Hot Reload 트리거 + // console.log 메시지를 감지하여 Hot Reload 트리거 (특정 메시지만) console.log = (...args: any[]) => { const message = args.join(" "); - if (message.includes("compiled") || message.includes("Fast Refresh") || message.includes("component")) { + // 핫 리로드를 트리거할 특정 메시지만 감지 (디버깅 로그는 제외) + if ((message.includes("compiled") || message.includes("Fast Refresh")) && + !message.includes("🔍") && !message.includes("🎯") && !message.includes("📤") && + !message.includes("📥") && !message.includes("⚠️") && !message.includes("🔄") && + !message.includes("✅") && !message.includes("🔧") && !message.includes("📋")) { if (!reloadPending) { reloadPending = true; setTimeout(() => { diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index 8ff6fd55..fa464377 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -110,6 +110,8 @@ export const DynamicComponentConfigPanel: React.FC = screenTableName, tableColumns, }) => { + console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`); + const [ConfigPanelComponent, setConfigPanelComponent] = React.useState | null>(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); @@ -180,10 +182,21 @@ export const DynamicComponentConfigPanel: React.FC = ); } + console.log(`🔧 DynamicComponentConfigPanel 렌더링:`, { + componentId, + ConfigPanelComponent: ConfigPanelComponent?.name, + config, + configType: typeof config, + configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object', + screenTableName, + tableColumns: Array.isArray(tableColumns) ? tableColumns.length : tableColumns + }); + return ( diff --git a/frontend/scripts/create-component.js b/frontend/scripts/create-component.js index 674f48d7..83d5c852 100755 --- a/frontend/scripts/create-component.js +++ b/frontend/scripts/create-component.js @@ -661,7 +661,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > @@ -709,7 +709,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > @@ -785,7 +785,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/types/component.ts b/frontend/types/component.ts index 34a8cd92..ea29cc5f 100644 --- a/frontend/types/component.ts +++ b/frontend/types/component.ts @@ -68,6 +68,9 @@ export interface ComponentRendererProps { // 새로운 기능들 autoGeneration?: AutoGenerationConfig; // 자동생성 설정 hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) + + // 설정 변경 핸들러 + onConfigChange?: (config: any) => void; [key: string]: any; } @@ -317,7 +320,7 @@ export const COMPONENT_CATEGORIES_INFO = { [ComponentCategory.CHART]: { name: "차트", description: "데이터 시각화 컴포넌트", - color: "#06b6d4", + color: "#3b83f6", }, [ComponentCategory.FORM]: { name: "폼", @@ -347,7 +350,7 @@ export const COMPONENT_CATEGORIES_INFO = { [ComponentCategory.CONTAINER]: { name: "컨테이너", description: "다른 컴포넌트를 담는 컨테이너", - color: "#374151", + color: "#3b83f6", }, [ComponentCategory.SYSTEM]: { name: "시스템", diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 0d8649a2..80aa6e72 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -192,10 +192,13 @@ export interface FileTypeConfig { export interface EntityTypeConfig { referenceTable: string; referenceColumn: string; - displayColumn: string; + displayColumns: string[]; // 여러 표시 컬럼을 배열로 변경 + displayColumn?: string; // 하위 호환성을 위해 유지 (deprecated) searchColumns?: string[]; filters?: Record; placeholder?: string; + displayFormat?: "simple" | "detailed" | "custom"; // 표시 형식 + separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ') } /** diff --git a/frontend/types/table-management.ts b/frontend/types/table-management.ts index d5bcf127..bd2cac09 100644 --- a/frontend/types/table-management.ts +++ b/frontend/types/table-management.ts @@ -46,6 +46,7 @@ export interface UnifiedColumnInfo { // 입력 설정 inputType: "direct" | "auto"; + input_type?: string; // 🎯 데이터베이스의 input_type 필드 (entity, text, number 등) detailSettings?: Record; // JSON 파싱된 객체 description?: string;