diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 280051d0..5557d8b5 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -134,8 +134,8 @@ export class EntityJoinService { `๐Ÿ”ง ๊ธฐ์กด display_column ์‚ฌ์šฉ: ${column.column_name} โ†’ ${displayColumn}` ); } else { - // display_column์ด "none"์ด๊ฑฐ๋‚˜ ์—†๋Š” ๊ฒฝ์šฐ ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ ๊ฐ€์ ธ์˜ค๊ธฐ - logger.info(`๐Ÿ” ${referenceTable}์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ ์กฐํšŒ ์ค‘...`); + // display_column์ด "none"์ด๊ฑฐ๋‚˜ ์—†๋Š” ๊ฒฝ์šฐ ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ์šฉ ์ปฌ๋Ÿผ ์ž๋™ ๊ฐ์ง€ + logger.info(`๐Ÿ” ${referenceTable}์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ ์ž๋™ ๊ฐ์ง€ ์ค‘...`); // ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ const tableColumnsResult = await query<{ column_name: string }>( @@ -148,10 +148,34 @@ export class EntityJoinService { ); if (tableColumnsResult.length > 0) { - displayColumns = tableColumnsResult.map((col) => col.column_name); + const allColumns = tableColumnsResult.map((col) => col.column_name); + + // ๐Ÿ†• ํ‘œ์‹œ์šฉ ์ปฌ๋Ÿผ ์ž๋™ ๊ฐ์ง€ (์šฐ์„ ์ˆœ์œ„ ์ˆœ์„œ) + // 1. *_name ์ปฌ๋Ÿผ (item_name, customer_name ๋“ฑ) + // 2. name ์ปฌ๋Ÿผ + // 3. label ์ปฌ๋Ÿผ + // 4. title ์ปฌ๋Ÿผ + // 5. ์ฐธ์กฐ ์ปฌ๋Ÿผ (referenceColumn) + const nameColumn = allColumns.find( + (col) => col.endsWith("_name") && col !== "company_name" + ); + const simpleNameColumn = allColumns.find((col) => col === "name"); + const labelColumn = allColumns.find( + (col) => col === "label" || col.endsWith("_label") + ); + const titleColumn = allColumns.find((col) => col === "title"); + + // ์šฐ์„ ์ˆœ์œ„์— ๋”ฐ๋ผ ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ ํƒ + const displayColumn = + nameColumn || + simpleNameColumn || + labelColumn || + titleColumn || + referenceColumn; + displayColumns = [displayColumn]; + logger.info( - `โœ… ${referenceTable}์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ ์ž๋™ ํฌํ•จ (${displayColumns.length}๊ฐœ):`, - displayColumns.join(", ") + `โœ… ${referenceTable}์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ ์ž๋™ ๊ฐ์ง€: ${displayColumn} (์ „์ฒด ${allColumns.length}๊ฐœ ์ค‘)` ); } else { // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ ๋ชป ์ฐพ์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 781a9498..9a8623a0 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -798,7 +798,12 @@ export class TableManagementService { ); // ๐Ÿ”ฅ ํ•ด๋‹น ์ปฌ๋Ÿผ์„ ์‚ฌ์šฉํ•˜๋Š” ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์˜ widgetType๋„ ์—…๋ฐ์ดํŠธ - await this.syncScreenLayoutsInputType(tableName, columnName, inputType, companyCode); + await this.syncScreenLayoutsInputType( + tableName, + columnName, + inputType, + companyCode + ); // ๐Ÿ”ฅ ์บ์‹œ ๋ฌดํšจํ™”: ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ์บ์‹œ ์‚ญ์ œ const cacheKeyPattern = `${CacheKeys.TABLE_COLUMNS(tableName, 1, 1000)}_${companyCode}`; @@ -928,7 +933,11 @@ export class TableManagementService { `UPDATE screen_layouts SET properties = $1, component_type = $2 WHERE layout_id = $3`, - [JSON.stringify(updatedProperties), newComponentType, layout.layout_id] + [ + JSON.stringify(updatedProperties), + newComponentType, + layout.layout_id, + ] ); logger.info( @@ -1299,18 +1308,30 @@ export class TableManagementService { try { // ๐Ÿ”ง ํŒŒ์ดํ”„๋กœ ๊ตฌ๋ถ„๋œ ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ (๋‹ค์ค‘์„ ํƒ ๋˜๋Š” ๋‚ ์งœ ๋ฒ”์œ„) if (typeof value === "string" && value.includes("|")) { - const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName); - + const columnInfo = await this.getColumnWebTypeInfo( + tableName, + columnName + ); + // ๋‚ ์งœ ํƒ€์ž…์ด๋ฉด ๋‚ ์งœ ๋ฒ”์œ„๋กœ ์ฒ˜๋ฆฌ - if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) { + if ( + columnInfo && + (columnInfo.webType === "date" || columnInfo.webType === "datetime") + ) { return this.buildDateRangeCondition(columnName, value, paramIndex); } - + // ๊ทธ ์™ธ ํƒ€์ž…์ด๋ฉด ๋‹ค์ค‘์„ ํƒ(IN ์กฐ๊ฑด)์œผ๋กœ ์ฒ˜๋ฆฌ - const multiValues = value.split("|").filter((v: string) => v.trim() !== ""); + const multiValues = value + .split("|") + .filter((v: string) => v.trim() !== ""); if (multiValues.length > 0) { - const placeholders = multiValues.map((_: string, idx: number) => `$${paramIndex + idx}`).join(", "); - logger.info(`๐Ÿ” ๋‹ค์ค‘์„ ํƒ ํ•„ํ„ฐ ์ ์šฉ: ${columnName} IN (${multiValues.join(", ")})`); + const placeholders = multiValues + .map((_: string, idx: number) => `$${paramIndex + idx}`) + .join(", "); + logger.info( + `๐Ÿ” ๋‹ค์ค‘์„ ํƒ ํ•„ํ„ฐ ์ ์šฉ: ${columnName} IN (${multiValues.join(", ")})` + ); return { whereClause: `${columnName}::text IN (${placeholders})`, values: multiValues, @@ -1320,10 +1341,20 @@ export class TableManagementService { } // ๐Ÿ”ง ๋‚ ์งœ ๋ฒ”์œ„ ๊ฐ์ฒด {from, to} ์ฒดํฌ - if (typeof value === "object" && value !== null && ("from" in value || "to" in value)) { + if ( + typeof value === "object" && + value !== null && + ("from" in value || "to" in value) + ) { // ๋‚ ์งœ ๋ฒ”์œ„ ๊ฐ์ฒด๋Š” ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ - const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName); - if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) { + const columnInfo = await this.getColumnWebTypeInfo( + tableName, + columnName + ); + if ( + columnInfo && + (columnInfo.webType === "date" || columnInfo.webType === "datetime") + ) { return this.buildDateRangeCondition(columnName, value, paramIndex); } } @@ -1356,9 +1387,10 @@ export class TableManagementService { // ์ปฌ๋Ÿผ ํƒ€์ž… ์ •๋ณด ์กฐํšŒ const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName); - logger.info(`๐Ÿ” [buildAdvancedSearchCondition] ${tableName}.${columnName}`, - `webType=${columnInfo?.webType || 'NULL'}`, - `inputType=${columnInfo?.inputType || 'NULL'}`, + logger.info( + `๐Ÿ” [buildAdvancedSearchCondition] ${tableName}.${columnName}`, + `webType=${columnInfo?.webType || "NULL"}`, + `inputType=${columnInfo?.inputType || "NULL"}`, `actualValue=${JSON.stringify(actualValue)}`, `operator=${operator}` ); @@ -1464,16 +1496,20 @@ export class TableManagementService { // ๋ฌธ์ž์—ด ํ˜•์‹์˜ ๋‚ ์งœ ๋ฒ”์œ„ ํŒŒ์‹ฑ ("YYYY-MM-DD|YYYY-MM-DD") if (typeof value === "string" && value.includes("|")) { const [fromStr, toStr] = value.split("|"); - + if (fromStr && fromStr.trim() !== "") { // VARCHAR ์ปฌ๋Ÿผ์„ DATE๋กœ ์บ์ŠคํŒ…ํ•˜์—ฌ ๋น„๊ต - conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`); + conditions.push( + `${columnName}::date >= $${paramIndex + paramCount}::date` + ); values.push(fromStr.trim()); paramCount++; } if (toStr && toStr.trim() !== "") { // ์ข…๋ฃŒ์ผ์€ ํ•ด๋‹น ๋‚ ์งœ์˜ 23:59:59๊นŒ์ง€ ํฌํ•จ - conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`); + conditions.push( + `${columnName}::date <= $${paramIndex + paramCount}::date` + ); values.push(toStr.trim()); paramCount++; } @@ -1482,17 +1518,21 @@ export class TableManagementService { else if (typeof value === "object" && value !== null) { if (value.from) { // VARCHAR ์ปฌ๋Ÿผ์„ DATE๋กœ ์บ์ŠคํŒ…ํ•˜์—ฌ ๋น„๊ต - conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`); + conditions.push( + `${columnName}::date >= $${paramIndex + paramCount}::date` + ); values.push(value.from); paramCount++; } if (value.to) { // ์ข…๋ฃŒ์ผ์€ ํ•ด๋‹น ๋‚ ์งœ์˜ 23:59:59๊นŒ์ง€ ํฌํ•จ - conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`); + conditions.push( + `${columnName}::date <= $${paramIndex + paramCount}::date` + ); values.push(value.to); paramCount++; } - } + } // ๋‹จ์ผ ๋‚ ์งœ ๊ฒ€์ƒ‰ else if (typeof value === "string" && value.trim() !== "") { conditions.push(`${columnName}::date = $${paramIndex}::date`); @@ -1658,9 +1698,11 @@ export class TableManagementService { paramCount: 0, }; } - + // IN ์ ˆ๋กœ ์—ฌ๋Ÿฌ ๊ฐ’ ๊ฒ€์ƒ‰ - const placeholders = value.map((_, idx) => `$${paramIndex + idx}`).join(", "); + const placeholders = value + .map((_, idx) => `$${paramIndex + idx}`) + .join(", "); return { whereClause: `${columnName} IN (${placeholders})`, values: value, @@ -1776,20 +1818,25 @@ export class TableManagementService { [tableName, columnName] ); - logger.info(`๐Ÿ” [getColumnWebTypeInfo] ${tableName}.${columnName} ์กฐํšŒ ๊ฒฐ๊ณผ:`, { - found: !!result, - web_type: result?.web_type, - input_type: result?.input_type, - }); + logger.info( + `๐Ÿ” [getColumnWebTypeInfo] ${tableName}.${columnName} ์กฐํšŒ ๊ฒฐ๊ณผ:`, + { + found: !!result, + web_type: result?.web_type, + input_type: result?.input_type, + } + ); if (!result) { - logger.warn(`โš ๏ธ [getColumnWebTypeInfo] ์ปฌ๋Ÿผ ์ •๋ณด ์—†์Œ: ${tableName}.${columnName}`); + logger.warn( + `โš ๏ธ [getColumnWebTypeInfo] ์ปฌ๋Ÿผ ์ •๋ณด ์—†์Œ: ${tableName}.${columnName}` + ); return null; } // web_type์ด ์—†์œผ๋ฉด input_type์„ ์‚ฌ์šฉ (๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜) const webType = result.web_type || result.input_type || ""; - + const columnInfo = { webType: webType, inputType: result.input_type || "", @@ -1799,7 +1846,9 @@ export class TableManagementService { displayColumn: result.display_column || undefined, }; - logger.info(`โœ… [getColumnWebTypeInfo] ๋ฐ˜ํ™˜๊ฐ’: webType=${columnInfo.webType}, inputType=${columnInfo.inputType}`); + logger.info( + `โœ… [getColumnWebTypeInfo] ๋ฐ˜ํ™˜๊ฐ’: webType=${columnInfo.webType}, inputType=${columnInfo.inputType}` + ); return columnInfo; } catch (error) { logger.error( @@ -1913,6 +1962,15 @@ export class TableManagementService { continue; } + // ๐Ÿ†• ์กฐ์ธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…)์€ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์กฐํšŒ์—์„œ ์ œ์™ธ + // Entity ์กฐ์ธ ์กฐํšŒ์—์„œ๋งŒ ์ฒ˜๋ฆฌ๋จ + if (column.includes(".")) { + logger.info( + `๐Ÿ” ์กฐ์ธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ${column} ๊ธฐ๋ณธ ์กฐํšŒ์—์„œ ์ œ์™ธ (Entity ์กฐ์ธ์—์„œ ์ฒ˜๋ฆฌ)` + ); + continue; + } + // ์•ˆ์ „ํ•œ ์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ (SQL ์ธ์ ์…˜ ๋ฐฉ์ง€) const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, ""); @@ -2741,7 +2799,11 @@ export class TableManagementService { WHERE "${referenceColumn}" IS NOT NULL`; // ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด์ด ์žˆ์œผ๋ฉด ์ ์šฉ (์˜ˆ: ํŠน์ • ๊ฑฐ๋ž˜์ฒ˜์˜ ํ’ˆ๋ชฉ๋งŒ ์ œ์™ธ) - if (filterColumn && filterValue !== undefined && filterValue !== null) { + if ( + filterColumn && + filterValue !== undefined && + filterValue !== null + ) { excludeSubquery += ` AND "${filterColumn}" = '${String(filterValue).replace(/'/g, "''")}'`; } @@ -2934,16 +2996,22 @@ export class TableManagementService { }), ]; + // ๐Ÿ†• ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช… ํ˜•์‹๋„ Entity ๊ฒ€์ƒ‰์œผ๋กœ ์ธ์‹ + const hasJoinTableSearch = + options.search && + Object.keys(options.search).some((key) => key.includes(".")); + const hasEntitySearch = options.search && - Object.keys(options.search).some((key) => + (Object.keys(options.search).some((key) => allEntityColumns.includes(key) - ); + ) || + hasJoinTableSearch); if (hasEntitySearch) { const entitySearchKeys = options.search - ? Object.keys(options.search).filter((key) => - allEntityColumns.includes(key) + ? Object.keys(options.search).filter( + (key) => allEntityColumns.includes(key) || key.includes(".") ) : []; logger.info( @@ -2988,47 +3056,113 @@ export class TableManagementService { if (options.search) { for (const [key, value] of Object.entries(options.search)) { + // ๊ฒ€์ƒ‰๊ฐ’ ์ถ”์ถœ (๊ฐ์ฒด ํ˜•ํƒœ์ผ ์ˆ˜ ์žˆ์Œ) + let searchValue = value; + if ( + typeof value === "object" && + value !== null && + "value" in value + ) { + searchValue = value.value; + } + + // ๋นˆ ๊ฐ’์ด๋ฉด ์Šคํ‚ต + if ( + searchValue === "__ALL__" || + searchValue === "" || + searchValue === null || + searchValue === undefined + ) { + continue; + } + + const safeValue = String(searchValue).replace(/'/g, "''"); + + // ๐Ÿ†• ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช… ํ˜•์‹ ์ฒ˜๋ฆฌ (์˜ˆ: item_info.item_name) + if (key.includes(".")) { + const [refTable, refColumn] = key.split("."); + + // aliasMap์—์„œ ๋ณ„์นญ ์ฐพ๊ธฐ (ํ…Œ์ด๋ธ”๋ช…:์†Œ์Šค์ปฌ๋Ÿผ ํ˜•์‹) + let foundAlias: string | undefined; + for (const [aliasKey, alias] of aliasMap.entries()) { + if (aliasKey.startsWith(`${refTable}:`)) { + foundAlias = alias; + break; + } + } + + if (foundAlias) { + whereConditions.push( + `${foundAlias}.${refColumn}::text ILIKE '%${safeValue}%'` + ); + entitySearchColumns.push(`${key} (${refTable}.${refColumn})`); + logger.info( + `๐ŸŽฏ ์กฐ์ธ ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰: ${key} โ†’ ${refTable}.${refColumn} LIKE '%${safeValue}%' (๋ณ„์นญ: ${foundAlias})` + ); + } else { + logger.warn( + `โš ๏ธ ์กฐ์ธ ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰ ์‹คํŒจ: ${key} - ๋ณ„์นญ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ` + ); + } + continue; + } + const joinConfig = joinConfigs.find( (config) => config.aliasColumn === key ); if (joinConfig) { // ๊ธฐ๋ณธ Entity ์กฐ์ธ ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ: ์กฐ์ธ๋œ ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ์—์„œ ๊ฒ€์ƒ‰ - const alias = aliasMap.get(joinConfig.referenceTable); + const aliasKey = `${joinConfig.referenceTable}:${joinConfig.sourceColumn}`; + const alias = aliasMap.get(aliasKey); whereConditions.push( - `${alias}.${joinConfig.displayColumn} ILIKE '%${value}%'` + `${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'` ); entitySearchColumns.push( `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` ); logger.info( - `๐ŸŽฏ Entity ์กฐ์ธ ๊ฒ€์ƒ‰: ${key} โ†’ ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${value}%' (๋ณ„์นญ: ${alias})` + `๐ŸŽฏ Entity ์กฐ์ธ ๊ฒ€์ƒ‰: ${key} โ†’ ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (๋ณ„์นญ: ${alias})` ); } else if (key === "writer_dept_code") { // writer_dept_code: user_info.dept_code์—์„œ ๊ฒ€์ƒ‰ - const userAlias = aliasMap.get("user_info"); - whereConditions.push( - `${userAlias}.dept_code ILIKE '%${value}%'` - ); - entitySearchColumns.push(`${key} (user_info.dept_code)`); - logger.info( - `๐ŸŽฏ ์ถ”๊ฐ€ Entity ์กฐ์ธ ๊ฒ€์ƒ‰: ${key} โ†’ user_info.dept_code LIKE '%${value}%' (๋ณ„์นญ: ${userAlias})` + const userAliasKey = Array.from(aliasMap.keys()).find((k) => + k.startsWith("user_info:") ); + const userAlias = userAliasKey + ? aliasMap.get(userAliasKey) + : undefined; + if (userAlias) { + whereConditions.push( + `${userAlias}.dept_code ILIKE '%${safeValue}%'` + ); + entitySearchColumns.push(`${key} (user_info.dept_code)`); + logger.info( + `๐ŸŽฏ ์ถ”๊ฐ€ Entity ์กฐ์ธ ๊ฒ€์ƒ‰: ${key} โ†’ user_info.dept_code LIKE '%${safeValue}%' (๋ณ„์นญ: ${userAlias})` + ); + } } else if (key === "company_code_status") { // company_code_status: company_info.status์—์„œ ๊ฒ€์ƒ‰ - const companyAlias = aliasMap.get("company_info"); - whereConditions.push( - `${companyAlias}.status ILIKE '%${value}%'` - ); - entitySearchColumns.push(`${key} (company_info.status)`); - logger.info( - `๐ŸŽฏ ์ถ”๊ฐ€ Entity ์กฐ์ธ ๊ฒ€์ƒ‰: ${key} โ†’ company_info.status LIKE '%${value}%' (๋ณ„์นญ: ${companyAlias})` + const companyAliasKey = Array.from(aliasMap.keys()).find((k) => + k.startsWith("company_info:") ); + const companyAlias = companyAliasKey + ? aliasMap.get(companyAliasKey) + : undefined; + if (companyAlias) { + whereConditions.push( + `${companyAlias}.status ILIKE '%${safeValue}%'` + ); + entitySearchColumns.push(`${key} (company_info.status)`); + logger.info( + `๐ŸŽฏ ์ถ”๊ฐ€ Entity ์กฐ์ธ ๊ฒ€์ƒ‰: ${key} โ†’ company_info.status LIKE '%${safeValue}%' (๋ณ„์นญ: ${companyAlias})` + ); + } } else { // ์ผ๋ฐ˜ ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ: ๋ฉ”์ธ ํ…Œ์ด๋ธ”์—์„œ ๊ฒ€์ƒ‰ - whereConditions.push(`main.${key} ILIKE '%${value}%'`); + whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`); logger.info( - `๐Ÿ” ์ผ๋ฐ˜ ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰: ${key} โ†’ main.${key} LIKE '%${value}%'` + `๐Ÿ” ์ผ๋ฐ˜ ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰: ${key} โ†’ main.${key} LIKE '%${safeValue}%'` ); } } @@ -3168,6 +3302,59 @@ export class TableManagementService { } try { + // ๐Ÿ†• ์กฐ์ธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰ ์ฒ˜๋ฆฌ (์˜ˆ: item_info.item_name) + if (columnName.includes(".")) { + const [refTable, refColumn] = columnName.split("."); + + // ๊ฒ€์ƒ‰๊ฐ’ ์ถ”์ถœ + let searchValue = value; + if (typeof value === "object" && value !== null && "value" in value) { + searchValue = value.value; + } + + if ( + searchValue === "__ALL__" || + searchValue === "" || + searchValue === null + ) { + continue; + } + + // ๐Ÿ” column_labels์—์„œ ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ ์„ค์ • ์ฐพ๊ธฐ + // ์˜ˆ: item_info ํ…Œ์ด๋ธ”์„ ์ฐธ์กฐํ•˜๋Š” ์ปฌ๋Ÿผ ์ฐพ๊ธฐ (item_code โ†’ item_info) + const entityColumnResult = await query<{ + column_name: string; + reference_table: string; + reference_column: string; + }>( + `SELECT column_name, reference_table, reference_column + FROM column_labels + WHERE table_name = $1 + AND input_type = 'entity' + AND reference_table = $2 + LIMIT 1`, + [tableName, refTable] + ); + + if (entityColumnResult.length > 0) { + // ์กฐ์ธ ๋ณ„์นญ ์ƒ์„ฑ (entityJoinService.ts์™€ ๋™์ผํ•œ ํŒจํ„ด: ํ…Œ์ด๋ธ”๋ช… ์•ž 3๊ธ€์ž) + const joinAlias = refTable.substring(0, 3); + + // ์กฐ์ธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์œผ๋กœ ๊ฒ€์ƒ‰ ์กฐ๊ฑด ์ƒ์„ฑ + const safeValue = String(searchValue).replace(/'/g, "''"); + const condition = `${joinAlias}.${refColumn}::text ILIKE '%${safeValue}%'`; + + logger.info(`๐Ÿ” ์กฐ์ธ ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰ ์กฐ๊ฑด: ${condition}`); + conditions.push(condition); + } else { + logger.warn( + `โš ๏ธ ์กฐ์ธ ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰ ์‹คํŒจ: ${columnName} - ์—”ํ‹ฐํ‹ฐ ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ` + ); + } + + continue; + } + // ๊ณ ๊ธ‰ ๊ฒ€์ƒ‰ ์กฐ๊ฑด ๊ตฌ์„ฑ const searchCondition = await this.buildAdvancedSearchCondition( tableName, @@ -4282,7 +4469,10 @@ export class TableManagementService { ); return result.length > 0; } catch (error) { - logger.error(`์ปฌ๋Ÿผ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ ์‹คํŒจ: ${tableName}.${columnName}`, error); + logger.error( + `์ปฌ๋Ÿผ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ ์‹คํŒจ: ${tableName}.${columnName}`, + error + ); return false; } } diff --git a/frontend/components/screen/table-options/FilterPanel.tsx b/frontend/components/screen/table-options/FilterPanel.tsx index 69395942..1b6104b6 100644 --- a/frontend/components/screen/table-options/FilterPanel.tsx +++ b/frontend/components/screen/table-options/FilterPanel.tsx @@ -10,17 +10,13 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Checkbox } from "@/components/ui/checkbox"; -import { Plus, X } from "lucide-react"; -import { TableFilter } from "@/types/table-options"; +import { Layers } from "lucide-react"; +import { TableFilter, GroupSumConfig } from "@/types/table-options"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; interface Props { isOpen: boolean; @@ -77,17 +73,37 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied const [columnFilters, setColumnFilters] = useState([]); const [selectAll, setSelectAll] = useState(false); + // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • + const [groupSumEnabled, setGroupSumEnabled] = useState(false); + const [groupByColumn, setGroupByColumn] = useState(""); + // localStorage์—์„œ ์ €์žฅ๋œ ํ•„ํ„ฐ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ useEffect(() => { if (table?.columns && table?.tableName) { // ํ™”๋ฉด๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ํ•„ํ„ฐ ์„ค์ • ์ €์žฅ - const storageKey = screenId + const storageKey = screenId ? `table_filters_${table.tableName}_screen_${screenId}` : `table_filters_${table.tableName}`; const savedFilters = localStorage.getItem(storageKey); - + + // ๐Ÿ†• ๊ทธ๋ฃนํ•‘ ์„ค์ •๋„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + const groupSumKey = screenId + ? `table_groupsum_${table.tableName}_screen_${screenId}` + : `table_groupsum_${table.tableName}`; + const savedGroupSum = localStorage.getItem(groupSumKey); + + if (savedGroupSum) { + try { + const parsed = JSON.parse(savedGroupSum) as GroupSumConfig; + setGroupSumEnabled(parsed.enabled); + setGroupByColumn(parsed.groupByColumn || ""); + } catch (error) { + console.error("๊ทธ๋ฃนํ•‘ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ:", error); + } + } + let filters: ColumnFilterConfig[]; - + if (savedFilters) { try { const parsed = JSON.parse(savedFilters) as ColumnFilterConfig[]; @@ -96,13 +112,15 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied .filter((col) => col.filterable !== false) .map((col) => { const saved = parsed.find((f) => f.columnName === col.columnName); - return saved || { - columnName: col.columnName, - columnLabel: col.columnLabel, - inputType: col.inputType || "text", - enabled: false, - filterType: mapInputTypeToFilterType(col.inputType || "text"), - }; + return ( + saved || { + columnName: col.columnName, + columnLabel: col.columnLabel, + inputType: col.inputType || "text", + enabled: false, + filterType: mapInputTypeToFilterType(col.inputType || "text"), + } + ); }); } catch (error) { console.error("์ €์žฅ๋œ ํ•„ํ„ฐ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ:", error); @@ -127,26 +145,20 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied filterType: mapInputTypeToFilterType(col.inputType || "text"), })); } - + setColumnFilters(filters); } }, [table?.columns, table?.tableName]); // inputType์„ filterType์œผ๋กœ ๋งคํ•‘ - const mapInputTypeToFilterType = ( - inputType: string - ): "text" | "number" | "date" | "select" => { + const mapInputTypeToFilterType = (inputType: string): "text" | "number" | "date" | "select" => { if (inputType.includes("number") || inputType.includes("decimal")) { return "number"; } if (inputType.includes("date") || inputType.includes("time")) { return "date"; } - if ( - inputType.includes("select") || - inputType.includes("code") || - inputType.includes("category") - ) { + if (inputType.includes("select") || inputType.includes("code") || inputType.includes("category")) { return "select"; } return "text"; @@ -155,31 +167,20 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied // ์ „์ฒด ์„ ํƒ/ํ•ด์ œ const toggleSelectAll = (checked: boolean) => { setSelectAll(checked); - setColumnFilters((prev) => - prev.map((filter) => ({ ...filter, enabled: checked })) - ); + setColumnFilters((prev) => prev.map((filter) => ({ ...filter, enabled: checked }))); }; // ๊ฐœ๋ณ„ ํ•„ํ„ฐ ํ† ๊ธ€ const toggleFilter = (columnName: string) => { setColumnFilters((prev) => - prev.map((filter) => - filter.columnName === columnName - ? { ...filter, enabled: !filter.enabled } - : filter - ) + prev.map((filter) => (filter.columnName === columnName ? { ...filter, enabled: !filter.enabled } : filter)), ); }; // ํ•„ํ„ฐ ํƒ€์ž… ๋ณ€๊ฒฝ - const updateFilterType = ( - columnName: string, - filterType: "text" | "number" | "date" | "select" - ) => { + const updateFilterType = (columnName: string, filterType: "text" | "number" | "date" | "select") => { setColumnFilters((prev) => - prev.map((filter) => - filter.columnName === columnName ? { ...filter, filterType } : filter - ) + prev.map((filter) => (filter.columnName === columnName ? { ...filter, filterType } : filter)), ); }; @@ -198,44 +199,76 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied // localStorage์— ์ €์žฅ (ํ™”๋ฉด๋ณ„๋กœ ๋…๋ฆฝ์ ) if (table?.tableName) { - const storageKey = screenId + const storageKey = screenId ? `table_filters_${table.tableName}_screen_${screenId}` : `table_filters_${table.tableName}`; localStorage.setItem(storageKey, JSON.stringify(columnFilters)); + + // ๐Ÿ†• ๊ทธ๋ฃนํ•‘ ์„ค์ • ์ €์žฅ + const groupSumKey = screenId + ? `table_groupsum_${table.tableName}_screen_${screenId}` + : `table_groupsum_${table.tableName}`; + + if (groupSumEnabled && groupByColumn) { + const selectedColumn = columnFilters.find((f) => f.columnName === groupByColumn); + const groupSumConfig: GroupSumConfig = { + enabled: true, + groupByColumn: groupByColumn, + groupByColumnLabel: selectedColumn?.columnLabel, + }; + localStorage.setItem(groupSumKey, JSON.stringify(groupSumConfig)); + table?.onGroupSumChange?.(groupSumConfig); + } else { + localStorage.removeItem(groupSumKey); + table?.onGroupSumChange?.(null); + } } table?.onFilterChange(activeFilters); - + // ์ฝœ๋ฐฑ์œผ๋กœ ํ™œ์„ฑํ™”๋œ ํ•„ํ„ฐ ์ •๋ณด ์ „๋‹ฌ onFiltersApplied?.(activeFilters); - + onClose(); }; // ์ดˆ๊ธฐํ™” (์ฆ‰์‹œ ์ €์žฅ ๋ฐ ์ ์šฉ) const clearFilters = () => { - const clearedFilters = columnFilters.map((filter) => ({ - ...filter, - enabled: false + const clearedFilters = columnFilters.map((filter) => ({ + ...filter, + enabled: false, })); - + setColumnFilters(clearedFilters); setSelectAll(false); - + + // ๐Ÿ†• ๊ทธ๋ฃนํ•‘ ์„ค์ • ์ดˆ๊ธฐํ™” + setGroupSumEnabled(false); + setGroupByColumn(""); + // localStorage์—์„œ ์ œ๊ฑฐ (ํ™”๋ฉด๋ณ„๋กœ ๋…๋ฆฝ์ ) if (table?.tableName) { - const storageKey = screenId + const storageKey = screenId ? `table_filters_${table.tableName}_screen_${screenId}` : `table_filters_${table.tableName}`; localStorage.removeItem(storageKey); + + // ๐Ÿ†• ๊ทธ๋ฃนํ•‘ ์„ค์ •๋„ ์ œ๊ฑฐ + const groupSumKey = screenId + ? `table_groupsum_${table.tableName}_screen_${screenId}` + : `table_groupsum_${table.tableName}`; + localStorage.removeItem(groupSumKey); } - + // ๋นˆ ํ•„ํ„ฐ ๋ฐฐ์—ด๋กœ ์ ์šฉ table?.onFilterChange([]); - + + // ๐Ÿ†• ๊ทธ๋ฃนํ•‘ ํ•ด์ œ + table?.onGroupSumChange?.(null); + // ์ฝœ๋ฐฑ์œผ๋กœ ๋นˆ ํ•„ํ„ฐ ์ •๋ณด ์ „๋‹ฌ onFiltersApplied?.([]); - + // ์ฆ‰์‹œ ๋‹ซ๊ธฐ onClose(); }; @@ -246,9 +279,7 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied - - ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์„ค์ • - + ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์„ค์ • ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋กœ ์‚ฌ์šฉํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”. ์„ ํƒํ•œ ์ปฌ๋Ÿผ์˜ ๊ฒ€์ƒ‰ ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. @@ -256,17 +287,12 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied
{/* ์ „์ฒด ์„ ํƒ/ํ•ด์ œ */} -
+
- - toggleSelectAll(checked as boolean) - } - /> + toggleSelectAll(checked as boolean)} /> ์ „์ฒด ์„ ํƒ/ํ•ด์ œ
-
+
{enabledCount} / {columnFilters.length}๊ฐœ
@@ -277,30 +303,21 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied {columnFilters.map((filter) => (
{/* ์ฒดํฌ๋ฐ•์Šค */} - toggleFilter(filter.columnName)} - /> + toggleFilter(filter.columnName)} /> {/* ์ปฌ๋Ÿผ ์ •๋ณด */}
-
- {filter.columnLabel} -
-
- {filter.columnName} -
+
{filter.columnLabel}
+
{filter.columnName}
{/* ํ•„ํ„ฐ ํƒ€์ž… ์„ ํƒ */} + + + + + {columnFilters.map((filter) => ( + + {filter.columnLabel} + + ))} + + +
+ )} +
+ {/* ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ */} -
+
๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”
- -
); }; - diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index ba911c3c..e8014327 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -34,7 +34,7 @@ import { } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { useTableOptions } from "@/contexts/TableOptionsContext"; -import { TableFilter, ColumnVisibility } from "@/types/table-options"; +import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options"; import { useAuth } from "@/hooks/useAuth"; export interface SplitPanelLayoutComponentProps extends ComponentRendererProps { @@ -73,12 +73,69 @@ export const SplitPanelLayoutComponent: React.FC return true; }; + // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ๋ช… ๋ณ€ํ™˜ ํ—ฌํผ + // "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•์‹์„ "์›๋ณธ์ปฌ๋Ÿผ_์กฐ์ธ์ปฌ๋Ÿผ๋ช…" ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ + const getEntityJoinValue = useCallback( + (item: any, columnName: string, entityColumnMap?: Record): any => { + // ์ง์ ‘ ๋งค์นญ ์‹œ๋„ + if (item[columnName] !== undefined) { + return item[columnName]; + } + + // "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•์‹์ธ ๊ฒฝ์šฐ (์˜ˆ: item_info.item_name) + if (columnName.includes(".")) { + const [tableName, fieldName] = columnName.split("."); + + // ๐Ÿ” ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’ ์ถ”์ถœ + // ์˜ˆ: item_info.item_name, item_info.standard, item_info.unit + + // 1๏ธโƒฃ ์†Œ์Šค ์ปฌ๋Ÿผ ์ถ”๋ก  (item_info โ†’ item_code, warehouse_info โ†’ warehouse_id ๋“ฑ) + const inferredSourceColumn = tableName.replace("_info", "_code").replace("_mng", "_id"); + + // 2๏ธโƒฃ ์ •ํ™•ํ•œ ํ‚ค ๋งคํ•‘ ์‹œ๋„: ์†Œ์Šค์ปฌ๋Ÿผ_ํ•„๋“œ๋ช… + // ์˜ˆ: item_code_item_name, item_code_standard, item_code_unit + const exactKey = `${inferredSourceColumn}_${fieldName}`; + if (item[exactKey] !== undefined) { + return item[exactKey]; + } + + // 3๏ธโƒฃ ๋ณ„์นญ ํŒจํ„ด: ์†Œ์Šค์ปฌ๋Ÿผ_name (๊ธฐ๋ณธ ํ‘œ์‹œ ์ปฌ๋Ÿผ์šฉ) + // ์˜ˆ: item_code_name (item_name์˜ ๋ณ„์นญ) + if (fieldName === "item_name" || fieldName === "name") { + const aliasKey = `${inferredSourceColumn}_name`; + if (item[aliasKey] !== undefined) { + return item[aliasKey]; + } + } + + // 4๏ธโƒฃ entityColumnMap์—์„œ ๋งคํ•‘ ์ฐพ๊ธฐ (ํ™”๋ฉด ์„ค์ •์—์„œ ์ง€์ •๋œ ๊ฒฝ์šฐ) + if (entityColumnMap && entityColumnMap[tableName]) { + const sourceColumn = entityColumnMap[tableName]; + const joinedColumnName = `${sourceColumn}_${fieldName}`; + if (item[joinedColumnName] !== undefined) { + return item[joinedColumnName]; + } + } + + // 5๏ธโƒฃ ํ…Œ์ด๋ธ”๋ช…_์ปฌ๋Ÿผ๋ช… ํ˜•์‹์œผ๋กœ ์‹œ๋„ + const underscoreKey = `${tableName}_${fieldName}`; + if (item[underscoreKey] !== undefined) { + return item[underscoreKey]; + } + } + + return undefined; + }, + [], + ); + // TableOptions Context const { registerTable, unregisterTable } = useTableOptions(); const [leftFilters, setLeftFilters] = useState([]); const [leftGrouping, setLeftGrouping] = useState([]); const [leftColumnVisibility, setLeftColumnVisibility] = useState([]); const [leftColumnOrder, setLeftColumnOrder] = useState([]); // ๐Ÿ”ง ์ปฌ๋Ÿผ ์ˆœ์„œ + const [leftGroupSumConfig, setLeftGroupSumConfig] = useState(null); // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • const [rightFilters, setRightFilters] = useState([]); const [rightGrouping, setRightGrouping] = useState([]); const [rightColumnVisibility, setRightColumnVisibility] = useState([]); @@ -125,6 +182,88 @@ export const SplitPanelLayoutComponent: React.FC const [leftWidth, setLeftWidth] = useState(splitRatio); const containerRef = React.useRef(null); + // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ๋œ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ + const summedLeftData = useMemo(() => { + console.log("๐Ÿ” [๊ทธ๋ฃนํ•ฉ์‚ฐ] leftGroupSumConfig:", leftGroupSumConfig); + + // ๊ทธ๋ฃนํ•‘์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ๊ฑฐ๋‚˜ ๊ทธ๋ฃน ๊ธฐ์ค€ ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + if (!leftGroupSumConfig?.enabled || !leftGroupSumConfig?.groupByColumn) { + console.log("๐Ÿ” [๊ทธ๋ฃนํ•ฉ์‚ฐ] ๊ทธ๋ฃนํ•‘ ๋น„ํ™œ์„ฑํ™” - ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜"); + return leftData; + } + + const groupByColumn = leftGroupSumConfig.groupByColumn; + const groupMap = new Map(); + + // ์กฐ์ธ ์ปฌ๋Ÿผ์ธ์ง€ ํ™•์ธํ•˜๊ณ  ์‹ค์ œ ํ‚ค ์ถ”๋ก  + const getActualKey = (columnName: string, item: any): string => { + if (columnName.includes(".")) { + const [refTable, fieldName] = columnName.split("."); + const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); + const exactKey = `${inferredSourceColumn}_${fieldName}`; + console.log("๐Ÿ” [๊ทธ๋ฃนํ•ฉ์‚ฐ] ์กฐ์ธ ์ปฌ๋Ÿผ ํ‚ค ๋ณ€ํ™˜:", { columnName, exactKey, hasKey: item[exactKey] !== undefined }); + if (item[exactKey] !== undefined) return exactKey; + if (fieldName === "item_name" || fieldName === "name") { + const aliasKey = `${inferredSourceColumn}_name`; + if (item[aliasKey] !== undefined) return aliasKey; + } + } + return columnName; + }; + + // ์ˆซ์ž ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜ + const isNumericValue = (value: any): boolean => { + if (value === null || value === undefined || value === "") return false; + const num = parseFloat(String(value)); + return !isNaN(num) && isFinite(num); + }; + + // ๊ทธ๋ฃนํ•‘ ์ˆ˜ํ–‰ + leftData.forEach((item) => { + const actualKey = getActualKey(groupByColumn, item); + const groupValue = String(item[actualKey] || item[groupByColumn] || ""); + + // ์›๋ณธ ID ์ถ”์ถœ (id, ID, ๋˜๋Š” ์ฒซ ๋ฒˆ์งธ ๊ฐ’) + const originalId = item.id || item.ID || Object.values(item)[0]; + + if (!groupMap.has(groupValue)) { + // ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์„ ๊ธฐ์ค€์œผ๋กœ ์ดˆ๊ธฐํ™” + ์›๋ณธ ID ๋ฐฐ์—ด + ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด + groupMap.set(groupValue, { + ...item, + _groupCount: 1, + _originalIds: [originalId], + _originalItems: [item], // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ „์ฒด ์ €์žฅ + }); + } else { + const existing = groupMap.get(groupValue); + existing._groupCount += 1; + existing._originalIds.push(originalId); + existing._originalItems.push(item); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + + // ๋ชจ๋“  ํ‚ค์— ๋Œ€ํ•ด ์ˆซ์ž๋ฉด ํ•ฉ์‚ฐ + Object.keys(item).forEach((key) => { + const value = item[key]; + if (isNumericValue(value) && key !== groupByColumn && !key.endsWith("_id") && !key.includes("code")) { + const numValue = parseFloat(String(value)); + const existingValue = parseFloat(String(existing[key] || 0)); + existing[key] = existingValue + numValue; + } + }); + + groupMap.set(groupValue, existing); + } + }); + + const result = Array.from(groupMap.values()); + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ๊ฒฐ๊ณผ:", { + ์›๋ณธ๊ฐœ์ˆ˜: leftData.length, + ๊ทธ๋ฃน๊ฐœ์ˆ˜: result.length, + ๊ทธ๋ฃน๊ธฐ์ค€: groupByColumn, + }); + + return result; + }, [leftData, leftGroupSumConfig]); + // ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ // height ์ฒ˜๋ฆฌ: ์ด๋ฏธ px ๋‹จ์œ„๋ฉด ๊ทธ๋Œ€๋กœ, ์ˆซ์ž๋ฉด px ์ถ”๊ฐ€ const getHeightValue = () => { @@ -433,14 +572,77 @@ export const SplitPanelLayoutComponent: React.FC // ๐ŸŽฏ ํ•„ํ„ฐ ์กฐ๊ฑด์„ API์— ์ „๋‹ฌ (entityJoinApi ์‚ฌ์šฉ) const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; + // ๐Ÿ†• "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•์‹์˜ ์กฐ์ธ ์ปฌ๋Ÿผ๋“ค์„ additionalJoinColumns๋กœ ๋ณ€ํ™˜ + const configuredColumns = componentConfig.leftPanel?.columns || []; + const additionalJoinColumns: Array<{ + sourceTable: string; + sourceColumn: string; + referenceTable: string; + joinAlias: string; + }> = []; + + // ์†Œ์Šค ์ปฌ๋Ÿผ ๋งคํ•‘ (item_info โ†’ item_code, warehouse_info โ†’ warehouse_id ๋“ฑ) + const sourceColumnMap: Record = {}; + + configuredColumns.forEach((col: any) => { + const colName = typeof col === "string" ? col : col.name || col.columnName; + if (colName && colName.includes(".")) { + const [refTable, refColumn] = colName.split("."); + // ์†Œ์Šค ์ปฌ๋Ÿผ ์ถ”๋ก  (item_info โ†’ item_code) + const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); + + // ์ด๋ฏธ ์ถ”๊ฐ€๋œ ์กฐ์ธ์ธ์ง€ ํ™•์ธ (๋™์ผ ํ…Œ์ด๋ธ”, ๋™์ผ ์†Œ์Šค์ปฌ๋Ÿผ) + const existingJoin = additionalJoinColumns.find( + (j) => j.referenceTable === refTable && j.sourceColumn === inferredSourceColumn, + ); + + if (!existingJoin) { + // ์ƒˆ๋กœ์šด ์กฐ์ธ ์ถ”๊ฐ€ (์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ) + additionalJoinColumns.push({ + sourceTable: leftTableName, + sourceColumn: inferredSourceColumn, + referenceTable: refTable, + joinAlias: `${inferredSourceColumn}_${refColumn}`, + }); + sourceColumnMap[refTable] = inferredSourceColumn; + } + + // ์ถ”๊ฐ€ ์ปฌ๋Ÿผ๋„ ๋ณ„๋„๋กœ ์š”์ฒญ (item_code_standard, item_code_unit ๋“ฑ) + // ๋‹จ, ์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ๊ณผ ๋‹ค๋ฅธ ๊ฒฝ์šฐ๋งŒ + const existingAliases = additionalJoinColumns + .filter((j) => j.referenceTable === refTable) + .map((j) => j.joinAlias); + const newAlias = `${sourceColumnMap[refTable] || inferredSourceColumn}_${refColumn}`; + + if (!existingAliases.includes(newAlias)) { + additionalJoinColumns.push({ + sourceTable: leftTableName, + sourceColumn: sourceColumnMap[refTable] || inferredSourceColumn, + referenceTable: refTable, + joinAlias: newAlias, + }); + } + } + }); + + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] additionalJoinColumns:", additionalJoinColumns); + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] configuredColumns:", configuredColumns); + const result = await entityJoinApi.getTableDataWithJoins(leftTableName, { page: 1, size: 100, search: filters, // ํ•„ํ„ฐ ์กฐ๊ฑด ์ „๋‹ฌ enableEntityJoin: true, // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ํ™œ์„ฑํ™” dataFilter: componentConfig.leftPanel?.dataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ „๋‹ฌ + additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // ๐Ÿ†• ์ถ”๊ฐ€ ์กฐ์ธ ์ปฌ๋Ÿผ }); + // ๐Ÿ” ๋””๋ฒ„๊น…: API ์‘๋‹ต ๋ฐ์ดํ„ฐ์˜ ํ‚ค ํ™•์ธ + if (result.data && result.data.length > 0) { + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] API ์‘๋‹ต ์ฒซ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ ํ‚ค:", Object.keys(result.data[0])); + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] API ์‘๋‹ต ์ฒซ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ:", result.data[0]); + } + // ๊ฐ€๋‚˜๋‹ค์ˆœ ์ •๋ ฌ (์ขŒ์ธก ํŒจ๋„์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ ๊ธฐ์ค€) const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; if (leftColumn && result.data.length > 0) { @@ -466,6 +668,8 @@ export const SplitPanelLayoutComponent: React.FC } }, [ componentConfig.leftPanel?.tableName, + componentConfig.leftPanel?.columns, + componentConfig.leftPanel?.dataFilter, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, @@ -502,6 +706,68 @@ export const SplitPanelLayoutComponent: React.FC const keys = componentConfig.rightPanel?.relation?.keys; const leftTable = componentConfig.leftPanel?.tableName; + // ๐Ÿ†• ๊ทธ๋ฃน ํ•ฉ์‚ฐ๋œ ํ•ญ๋ชฉ์ธ ๊ฒฝ์šฐ: ์›๋ณธ ๋ฐ์ดํ„ฐ๋“ค๋กœ ์šฐ์ธก ํŒจ๋„ ํ‘œ์‹œ + if (leftItem._originalItems && leftItem._originalItems.length > 0) { + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๊ทธ๋ฃน ํ•ฉ์‚ฐ ํ•ญ๋ชฉ - ์›๋ณธ ๊ฐœ์ˆ˜:", leftItem._originalItems.length); + + // ์ •๋ ฌ ๊ธฐ์ค€ ์ปฌ๋Ÿผ (๋ณตํ•ฉํ‚ค์˜ leftColumn๋“ค) + const sortColumns = keys?.map((k: any) => k.leftColumn).filter(Boolean) || []; + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ์ •๋ ฌ ๊ธฐ์ค€ ์ปฌ๋Ÿผ:", sortColumns); + + // ์ •๋ ฌ ํ•จ์ˆ˜ + const sortByKeys = (data: any[]) => { + if (sortColumns.length === 0) return data; + return [...data].sort((a, b) => { + for (const col of sortColumns) { + const aVal = String(a[col] || ""); + const bVal = String(b[col] || ""); + const cmp = aVal.localeCompare(bVal, "ko-KR"); + if (cmp !== 0) return cmp; + } + return 0; + }); + }; + + // ์›๋ณธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์šฐ์ธก ํŒจ๋„์— ํ‘œ์‹œ (์ด๋ ฅ ํ…Œ์ด๋ธ”๊ณผ ๋™์ผ ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ) + if (leftTable === rightTableName) { + const sortedData = sortByKeys(leftItem._originalItems); + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๋™์ผ ํ…Œ์ด๋ธ” - ์ •๋ ฌ๋œ ์›๋ณธ ๋ฐ์ดํ„ฐ:", sortedData.length); + setRightData(sortedData); + return; + } + + // ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ: ์›๋ณธ ID๋“ค๋กœ ์กฐํšŒ + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + const allResults: any[] = []; + + // ๊ฐ ์›๋ณธ ํ•ญ๋ชฉ์— ๋Œ€ํ•ด ์กฐํšŒ + for (const originalItem of leftItem._originalItems) { + const searchConditions: Record = {}; + keys?.forEach((key: any) => { + if (key.leftColumn && key.rightColumn && originalItem[key.leftColumn] !== undefined) { + searchConditions[key.rightColumn] = originalItem[key.leftColumn]; + } + }); + + if (Object.keys(searchConditions).length > 0) { + const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { + search: searchConditions, + enableEntityJoin: true, + size: 1000, + }); + if (result.data) { + allResults.push(...result.data); + } + } + } + + // ์ •๋ ฌ ์ ์šฉ + const sortedResults = sortByKeys(allResults); + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๊ทธ๋ฃน ํ•ฉ์‚ฐ - ์šฐ์ธก ํŒจ๋„ ์ •๋ ฌ๋œ ๋ฐ์ดํ„ฐ:", sortedResults.length); + setRightData(sortedResults); + return; + } + // ๐Ÿ†• ๋ณตํ•ฉํ‚ค ์ง€์› if (keys && keys.length > 0 && leftTable) { // ๋ณตํ•ฉํ‚ค: ์—ฌ๋Ÿฌ ์กฐ๊ฑด์œผ๋กœ ํ•„ํ„ฐ๋ง @@ -642,7 +908,28 @@ export const SplitPanelLayoutComponent: React.FC const uniqueValues = new Set(); leftData.forEach((item) => { - const value = item[columnName]; + // ๐Ÿ†• ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ (item_info.standard โ†’ item_code_standard) + let value: any; + + if (columnName.includes(".")) { + // ์กฐ์ธ ์ปฌ๋Ÿผ: getEntityJoinValue์™€ ๋™์ผํ•œ ๋กœ์ง ์ ์šฉ + const [refTable, fieldName] = columnName.split("."); + const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); + + // ์ •ํ™•ํ•œ ํ‚ค๋กœ ๋จผ์ € ์‹œ๋„ + const exactKey = `${inferredSourceColumn}_${fieldName}`; + value = item[exactKey]; + + // ๊ธฐ๋ณธ ๋ณ„์นญ ํŒจํ„ด ์‹œ๋„ (item_code_name) + if (value === undefined && (fieldName === "item_name" || fieldName === "name")) { + const aliasKey = `${inferredSourceColumn}_name`; + value = item[aliasKey]; + } + } else { + // ์ผ๋ฐ˜ ์ปฌ๋Ÿผ + value = item[columnName]; + } + if (value !== null && value !== undefined && value !== "") { // _name ํ•„๋“œ ์šฐ์„  ์‚ฌ์šฉ (category/entity type) const displayValue = item[`${columnName}_name`] || value; @@ -666,6 +953,15 @@ export const SplitPanelLayoutComponent: React.FC const leftTableId = `split-panel-left-${component.id}`; // ๐Ÿ”ง ํ™”๋ฉด์— ํ‘œ์‹œ๋˜๋Š” ์ปฌ๋Ÿผ ์‚ฌ์šฉ (columns ์†์„ฑ) const configuredColumns = componentConfig.leftPanel?.columns || []; + + // ๐Ÿ†• ์„ค์ •์—์„œ ์ง€์ •ํ•œ ๋ผ๋ฒจ ๋งต ์ƒ์„ฑ + const configuredLabels: Record = {}; + configuredColumns.forEach((col: any) => { + if (typeof col === "object" && col.name && col.label) { + configuredLabels[col.name] = col.label; + } + }); + const displayColumns = configuredColumns .map((col: any) => { if (typeof col === "string") return col; @@ -683,7 +979,8 @@ export const SplitPanelLayoutComponent: React.FC tableName: leftTableName, columns: displayColumns.map((col: string) => ({ columnName: col, - columnLabel: leftColumnLabels[col] || col, + // ๐Ÿ†• ์šฐ์„ ์ˆœ์œ„: 1) ์„ค์ •์—์„œ ์ง€์ •ํ•œ ๋ผ๋ฒจ 2) DB ๋ผ๋ฒจ 3) ์ปฌ๋Ÿผ๋ช… + columnLabel: configuredLabels[col] || leftColumnLabels[col] || col, inputType: "text", visible: true, width: 150, @@ -695,6 +992,7 @@ export const SplitPanelLayoutComponent: React.FC onColumnVisibilityChange: setLeftColumnVisibility, onColumnOrderChange: setLeftColumnOrder, // ๐Ÿ”ง ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ ์ถ”๊ฐ€ getColumnUniqueValues: getLeftColumnUniqueValues, // ๐Ÿ”ง ๊ณ ์œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ ํ•จ์ˆ˜ ์ถ”๊ฐ€ + onGroupSumChange: setLeftGroupSumConfig, // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • ์ฝœ๋ฐฑ }); return () => unregisterTable(leftTableId); @@ -1651,16 +1949,25 @@ export const SplitPanelLayoutComponent: React.FC ) : ( (() => { + // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ + const dataSource = summedLeftData; + console.log( + "๐Ÿ” [ํ…Œ์ด๋ธ”๋ชจ๋“œ ๋ Œ๋”๋ง] dataSource ๊ฐœ์ˆ˜:", + dataSource.length, + "leftGroupSumConfig:", + leftGroupSumConfig, + ); + // ๐Ÿ”ง ๋กœ์ปฌ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ ์šฉ const filteredData = leftSearchQuery - ? leftData.filter((item) => { + ? dataSource.filter((item) => { const searchLower = leftSearchQuery.toLowerCase(); return Object.entries(item).some(([key, value]) => { if (value === null || value === undefined) return false; return String(value).toLowerCase().includes(searchLower); }); }) - : leftData; + : dataSource; // ๐Ÿ”ง ๊ฐ€์‹œ์„ฑ ์ฒ˜๋ฆฌ๋œ ์ปฌ๋Ÿผ ์‚ฌ์šฉ const columnsToShow = @@ -1737,7 +2044,7 @@ export const SplitPanelLayoutComponent: React.FC > {formatCellValue( col.name, - item[col.name], + getEntityJoinValue(item, col.name), leftCategoryMappings, col.format, )} @@ -1796,7 +2103,12 @@ export const SplitPanelLayoutComponent: React.FC className="px-3 py-2 text-sm whitespace-nowrap text-gray-900" style={{ textAlign: col.align || "left" }} > - {formatCellValue(col.name, item[col.name], leftCategoryMappings, col.format)} + {formatCellValue( + col.name, + getEntityJoinValue(item, col.name), + leftCategoryMappings, + col.format, + )} ))} @@ -1851,16 +2163,25 @@ export const SplitPanelLayoutComponent: React.FC ) : ( (() => { + // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ + const dataToDisplay = summedLeftData; + console.log( + "๐Ÿ” [๋ Œ๋”๋ง] dataToDisplay ๊ฐœ์ˆ˜:", + dataToDisplay.length, + "leftGroupSumConfig:", + leftGroupSumConfig, + ); + // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ง (ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ) const filteredLeftData = leftSearchQuery - ? leftData.filter((item) => { + ? dataToDisplay.filter((item) => { const searchLower = leftSearchQuery.toLowerCase(); return Object.entries(item).some(([key, value]) => { if (value === null || value === undefined) return false; return String(value).toLowerCase().includes(searchLower); }); }) - : leftData; + : dataToDisplay; // ์žฌ๊ท€ ๋ Œ๋”๋ง ํ•จ์ˆ˜ const renderTreeItem = (item: any, index: number): React.ReactNode => { @@ -2108,23 +2429,53 @@ export const SplitPanelLayoutComponent: React.FC if (isTableMode) { // ํ…Œ์ด๋ธ” ๋ชจ๋“œ ๋ Œ๋”๋ง const displayColumns = componentConfig.rightPanel?.columns || []; - const columnsToShow = - displayColumns.length > 0 - ? displayColumns.map((col) => ({ - ...col, - label: rightColumnLabels[col.name] || col.label || col.name, - format: col.format, // ๐Ÿ†• ํฌ๋งท ์„ค์ • ์œ ์ง€ - })) - : Object.keys(filteredData[0] || {}) - .filter((key) => shouldShowField(key)) - .slice(0, 5) - .map((key) => ({ - name: key, - label: rightColumnLabels[key] || key, - width: 150, - align: "left" as const, - format: undefined, // ๐Ÿ†• ๊ธฐ๋ณธ๊ฐ’ - })); + + // ๐Ÿ†• ๊ทธ๋ฃน ํ•ฉ์‚ฐ ๋ชจ๋“œ์ผ ๋•Œ: ๋ณตํ•ฉํ‚ค ์ปฌ๋Ÿผ์„ ์šฐ์„  ํ‘œ์‹œ + const relationKeys = componentConfig.rightPanel?.relation?.keys || []; + const keyColumns = relationKeys.map((k: any) => k.leftColumn).filter(Boolean); + const isGroupedMode = selectedLeftItem?._originalItems?.length > 0; + + let columnsToShow: any[] = []; + + if (displayColumns.length > 0) { + // ์„ค์ •๋œ ์ปฌ๋Ÿผ ์‚ฌ์šฉ + columnsToShow = displayColumns.map((col) => ({ + ...col, + label: rightColumnLabels[col.name] || col.label || col.name, + format: col.format, + })); + + // ๐Ÿ†• ๊ทธ๋ฃน ํ•ฉ์‚ฐ ๋ชจ๋“œ์ด๊ณ , ํ‚ค ์ปฌ๋Ÿผ์ด ํ‘œ์‹œ ๋ชฉ๋ก์— ์—†์œผ๋ฉด ๋งจ ์•ž์— ์ถ”๊ฐ€ + if (isGroupedMode && keyColumns.length > 0) { + const existingColNames = columnsToShow.map((c) => c.name); + const missingKeyColumns = keyColumns.filter((k: string) => !existingColNames.includes(k)); + + if (missingKeyColumns.length > 0) { + const keyColsToAdd = missingKeyColumns.map((colName: string) => ({ + name: colName, + label: rightColumnLabels[colName] || colName, + width: 120, + align: "left" as const, + format: undefined, + _isKeyColumn: true, // ๊ตฌ๋ถ„์šฉ ํ”Œ๋ž˜๊ทธ + })); + columnsToShow = [...keyColsToAdd, ...columnsToShow]; + console.log("๐Ÿ”— [์šฐ์ธกํŒจ๋„] ๊ทธ๋ฃน๋ชจ๋“œ - ํ‚ค ์ปฌ๋Ÿผ ์ถ”๊ฐ€:", missingKeyColumns); + } + } + } else { + // ๊ธฐ๋ณธ ์ปฌ๋Ÿผ ์ž๋™ ์ƒ์„ฑ + columnsToShow = Object.keys(filteredData[0] || {}) + .filter((key) => shouldShowField(key)) + .slice(0, 5) + .map((key) => ({ + name: key, + label: rightColumnLabels[key] || key, + width: 150, + align: "left" as const, + format: undefined, + })); + } return (
@@ -2150,11 +2501,14 @@ export const SplitPanelLayoutComponent: React.FC {col.label} ))} - {!isDesignMode && ( - - ์ž‘์—… - - )} + {/* ์ˆ˜์ • ๋˜๋Š” ์‚ญ์ œ ๋ฒ„ํŠผ์ด ํ•˜๋‚˜๋ผ๋„ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์„ ๋•Œ๋งŒ ์ž‘์—… ์ปฌ๋Ÿผ ํ‘œ์‹œ */} + {!isDesignMode && + ((componentConfig.rightPanel?.editButton?.enabled ?? true) || + (componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && ( + + ์ž‘์—… + + )} @@ -2169,43 +2523,51 @@ export const SplitPanelLayoutComponent: React.FC className="px-3 py-2 text-sm whitespace-nowrap text-gray-900" style={{ textAlign: col.align || "left" }} > - {formatCellValue(col.name, item[col.name], rightCategoryMappings, col.format)} + {formatCellValue( + col.name, + getEntityJoinValue(item, col.name), + rightCategoryMappings, + col.format, + )} ))} - {!isDesignMode && ( - -
- {(componentConfig.rightPanel?.editButton?.enabled ?? true) && ( - - )} - {(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && ( - - )} -
- - )} + {/* ์ˆ˜์ • ๋˜๋Š” ์‚ญ์ œ ๋ฒ„ํŠผ์ด ํ•˜๋‚˜๋ผ๋„ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์„ ๋•Œ๋งŒ ์ž‘์—… ์…€ ํ‘œ์‹œ */} + {!isDesignMode && + ((componentConfig.rightPanel?.editButton?.enabled ?? true) || + (componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && ( + +
+ {(componentConfig.rightPanel?.editButton?.enabled ?? true) && ( + + )} + {(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && ( + + )} +
+ + )} ); })} @@ -2240,78 +2602,16 @@ export const SplitPanelLayoutComponent: React.FC firstValues = rightColumns .slice(0, summaryCount) .map((col) => { - // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ (์˜ˆ: item_info.item_number โ†’ item_number ๋˜๋Š” item_id_name) - let value = item[col.name]; - if (value === undefined && col.name.includes(".")) { - const columnName = col.name.split(".").pop(); - // 1์ฐจ: ์ปฌ๋Ÿผ๋ช… ๊ทธ๋Œ€๋กœ (์˜ˆ: item_number) - value = item[columnName || ""]; - // 2์ฐจ: item_info.item_number โ†’ item_id_name ๋˜๋Š” item_id_item_number ํ˜•์‹ ํ™•์ธ - if (value === undefined) { - const parts = col.name.split("."); - if (parts.length === 2) { - const refTable = parts[0]; // item_info - const refColumn = parts[1]; // item_number ๋˜๋Š” item_name - // FK ์ปฌ๋Ÿผ๋ช… ์ถ”๋ก : item_info โ†’ item_id - const fkColumn = refTable.replace("_info", "").replace("_mng", "") + "_id"; - - // ๋ฐฑ์—”๋“œ์—์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ณ„์นญ ํŒจํ„ด: - // 1) item_id_name (๊ธฐ๋ณธ referenceColumn) - // 2) item_id_item_name (์ถ”๊ฐ€ ์ปฌ๋Ÿผ) - if ( - refColumn === refTable.replace("_info", "").replace("_mng", "") + "_number" || - refColumn === refTable.replace("_info", "").replace("_mng", "") + "_code" - ) { - // ๊ธฐ๋ณธ ์ฐธ์กฐ ์ปฌ๋Ÿผ (item_number, customer_code ๋“ฑ) - const aliasKey = fkColumn + "_name"; - value = item[aliasKey]; - } else { - // ์ถ”๊ฐ€ ์ปฌ๋Ÿผ (item_name, customer_name ๋“ฑ) - const aliasKey = `${fkColumn}_${refColumn}`; - value = item[aliasKey]; - } - } - } - } + // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ (getEntityJoinValue ์‚ฌ์šฉ) + const value = getEntityJoinValue(item, col.name); return [col.name, value, col.label] as [string, any, string]; }) .filter(([_, value]) => value !== null && value !== undefined && value !== ""); allValues = rightColumns .map((col) => { - // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ - let value = item[col.name]; - if (value === undefined && col.name.includes(".")) { - const columnName = col.name.split(".").pop(); - // 1์ฐจ: ์ปฌ๋Ÿผ๋ช… ๊ทธ๋Œ€๋กœ - value = item[columnName || ""]; - // 2์ฐจ: {fk_column}_name ๋˜๋Š” {fk_column}_{ref_column} ํ˜•์‹ ํ™•์ธ - if (value === undefined) { - const parts = col.name.split("."); - if (parts.length === 2) { - const refTable = parts[0]; // item_info - const refColumn = parts[1]; // item_number ๋˜๋Š” item_name - // FK ์ปฌ๋Ÿผ๋ช… ์ถ”๋ก : item_info โ†’ item_id - const fkColumn = refTable.replace("_info", "").replace("_mng", "") + "_id"; - - // ๋ฐฑ์—”๋“œ์—์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ณ„์นญ ํŒจํ„ด: - // 1) item_id_name (๊ธฐ๋ณธ referenceColumn) - // 2) item_id_item_name (์ถ”๊ฐ€ ์ปฌ๋Ÿผ) - if ( - refColumn === refTable.replace("_info", "").replace("_mng", "") + "_number" || - refColumn === refTable.replace("_info", "").replace("_mng", "") + "_code" - ) { - // ๊ธฐ๋ณธ ์ฐธ์กฐ ์ปฌ๋Ÿผ - const aliasKey = fkColumn + "_name"; - value = item[aliasKey]; - } else { - // ์ถ”๊ฐ€ ์ปฌ๋Ÿผ - const aliasKey = `${fkColumn}_${refColumn}`; - value = item[aliasKey]; - } - } - } - } + // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ (getEntityJoinValue ์‚ฌ์šฉ) + const value = getEntityJoinValue(item, col.name); return [col.name, value, col.label] as [string, any, string]; }) .filter(([_, value]) => value !== null && value !== undefined && value !== ""); diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index f68b8383..7bb56779 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -47,18 +47,14 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Label } from "@/components/ui/label"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; import { SingleTableWithSticky } from "./SingleTableWithSticky"; import { CardModeRenderer } from "./CardModeRenderer"; import { TableOptionsModal } from "@/components/common/TableOptionsModal"; import { useTableOptions } from "@/contexts/TableOptionsContext"; -import { TableFilter, ColumnVisibility } from "@/types/table-options"; +import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options"; import { useAuth } from "@/hooks/useAuth"; import { useScreenContextOptional } from "@/contexts/ScreenContext"; import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext"; @@ -145,18 +141,18 @@ const debouncedApiCall = (key: string, fn: (...args: T) => P interface FilterCondition { id: string; column: string; - operator: - | "equals" - | "notEquals" - | "contains" - | "notContains" - | "startsWith" - | "endsWith" - | "greaterThan" - | "lessThan" - | "greaterOrEqual" - | "lessOrEqual" - | "isEmpty" + operator: + | "equals" + | "notEquals" + | "contains" + | "notContains" + | "startsWith" + | "endsWith" + | "greaterThan" + | "lessThan" + | "greaterOrEqual" + | "lessOrEqual" + | "isEmpty" | "isNotEmpty"; value: string; } @@ -299,7 +295,7 @@ export const TableListComponent: React.FC = ({ // ํ™”๋ฉด ์ปจํ…์ŠคํŠธ (๋ฐ์ดํ„ฐ ์ œ๊ณต์ž๋กœ ๋“ฑ๋ก) const screenContext = useScreenContextOptional(); - + // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ (๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก) const splitPanelContext = useSplitPanelContext(); // ๐Ÿ†• ScreenContext์—์„œ splitPanelPosition ๊ฐ€์ ธ์˜ค๊ธฐ (์ค‘์ฒฉ ํ™”๋ฉด์—์„œ๋„ ์ž‘๋™) @@ -322,12 +318,12 @@ export const TableListComponent: React.FC = ({ newSearchValues[filter.columnName] = filter.value; } }); - + // console.log("๐Ÿ” [TableListComponent] filters โ†’ searchValues:", { // filters: filters.length, // searchValues: newSearchValues, // }); - + setSearchValues(newSearchValues); setCurrentPage(1); // ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ฒซ ํŽ˜์ด์ง€๋กœ }, [filters]); @@ -342,7 +338,7 @@ export const TableListComponent: React.FC = ({ if (tableConfig.selectedTable && currentUserId) { const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`; const savedSettings = localStorage.getItem(storageKey); - + if (savedSettings) { try { const parsed = JSON.parse(savedSettings) as ColumnVisibility[]; @@ -357,11 +353,9 @@ export const TableListComponent: React.FC = ({ // columnVisibility ๋ณ€๊ฒฝ ์‹œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ฐ ๊ฐ€์‹œ์„ฑ ์ ์šฉ useEffect(() => { if (columnVisibility.length > 0) { - const newOrder = columnVisibility - .map((cv) => cv.columnName) - .filter((name) => name !== "__checkbox__"); // ์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ + const newOrder = columnVisibility.map((cv) => cv.columnName).filter((name) => name !== "__checkbox__"); // ์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ setColumnOrder(newOrder); - + // localStorage์— ์ €์žฅ (์‚ฌ์šฉ์ž๋ณ„) if (tableConfig.selectedTable && currentUserId) { const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`; @@ -428,18 +422,18 @@ export const TableListComponent: React.FC = ({ const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - + // ๐Ÿ†• ์ปฌ๋Ÿผ ํ—ค๋” ํ•„ํ„ฐ ์ƒํƒœ (์ƒ๋‹จ์—์„œ ์„ ์–ธ) - const [headerFilters, setHeaderFilters] = useState>>({}); + const [headerFilters, setHeaderFilters] = useState>>({}); const [openFilterColumn, setOpenFilterColumn] = useState(null); // ๐Ÿ†• Filter Builder (๊ณ ๊ธ‰ ํ•„ํ„ฐ) ๊ด€๋ จ ์ƒํƒœ - filteredData๋ณด๋‹ค ๋จผ์ € ์ •์˜ํ•ด์•ผ ํ•จ const [filterGroups, setFilterGroups] = useState([]); - + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ์šฐ์ธก์— ์ด๋ฏธ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ํ•„ํ„ฐ๋ง (์ขŒ์ธก ํ…Œ์ด๋ธ”์—๋งŒ ์ ์šฉ) + ํ—ค๋” ํ•„ํ„ฐ const filteredData = useMemo(() => { let result = data; - + // 1. ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก์— ์žˆ๊ณ , ์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ํ•„ํ„ฐ๋ง if (splitPanelPosition === "left" && splitPanelContext?.addedItemIds && splitPanelContext.addedItemIds.size > 0) { const addedIds = splitPanelContext.addedItemIds; @@ -448,17 +442,17 @@ export const TableListComponent: React.FC = ({ return !addedIds.has(rowId); }); } - + // 2. ํ—ค๋” ํ•„ํ„ฐ ์ ์šฉ (joinColumnMapping ์‚ฌ์šฉ ์•ˆ ํ•จ - ์ง์ ‘ ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ) if (Object.keys(headerFilters).length > 0) { result = result.filter((row) => { return Object.entries(headerFilters).every(([columnName, values]) => { if (values.size === 0) return true; - + // ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ๋ช… ์‹œ๋„ const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : ""; - + return values.has(cellStr); }); }); @@ -469,11 +463,11 @@ export const TableListComponent: React.FC = ({ result = result.filter((row) => { return filterGroups.every((group) => { const validConditions = group.conditions.filter( - (c) => c.column && (c.operator === "isEmpty" || c.operator === "isNotEmpty" || c.value) + (c) => c.column && (c.operator === "isEmpty" || c.operator === "isNotEmpty" || c.value), ); if (validConditions.length === 0) return true; - const evaluateCondition = (value: any, condition: typeof group.conditions[0]): boolean => { + const evaluateCondition = (value: any, condition: (typeof group.conditions)[0]): boolean => { const strValue = value !== null && value !== undefined ? String(value).toLowerCase() : ""; const condValue = condition.value.toLowerCase(); @@ -515,7 +509,7 @@ export const TableListComponent: React.FC = ({ }); }); } - + return result; }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]); @@ -561,9 +555,9 @@ export const TableListComponent: React.FC = ({ const tableContainerRef = useRef(null); // ๐Ÿ†• ์ธ๋ผ์ธ ์…€ ํŽธ์ง‘ ๊ด€๋ จ ์ƒํƒœ - const [editingCell, setEditingCell] = useState<{ - rowIndex: number; - colIndex: number; + const [editingCell, setEditingCell] = useState<{ + rowIndex: number; + colIndex: number; columnName: string; originalValue: any; } | null>(null); @@ -572,13 +566,18 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘ ๊ด€๋ จ ์ƒํƒœ const [editMode, setEditMode] = useState<"immediate" | "batch">("immediate"); // ํŽธ์ง‘ ๋ชจ๋“œ - const [pendingChanges, setPendingChanges] = useState>(new Map()); // key: `${rowIndex}-${columnName}` + const [pendingChanges, setPendingChanges] = useState< + Map< + string, + { + rowIndex: number; + columnName: string; + originalValue: any; + newValue: any; + primaryKeyValue: any; + } + > + >(new Map()); // key: `${rowIndex}-${columnName}` const [localEditedData, setLocalEditedData] = useState>>({}); // ๋กœ์ปฌ ์ˆ˜์ • ๋ฐ์ดํ„ฐ // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ด€๋ จ ์ƒํƒœ @@ -610,6 +609,9 @@ export const TableListComponent: React.FC = ({ const [groupByColumns, setGroupByColumns] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); + // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • ์ƒํƒœ + const [groupSumConfig, setGroupSumConfig] = useState(null); + // ๐Ÿ†• Master-Detail ๊ด€๋ จ ์ƒํƒœ const [expandedRows, setExpandedRows] = useState>(new Set()); // ํ™•์žฅ๋œ ํ–‰ ํ‚ค ๋ชฉ๋ก const [detailData, setDetailData] = useState>({}); // ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์บ์‹œ @@ -639,7 +641,9 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• Real-Time Updates ๊ด€๋ จ ์ƒํƒœ const [isRealTimeEnabled] = useState((tableConfig as any).realTimeUpdates ?? false); - const [wsConnectionStatus, setWsConnectionStatus] = useState<"connecting" | "connected" | "disconnected">("disconnected"); + const [wsConnectionStatus, setWsConnectionStatus] = useState<"connecting" | "connected" | "disconnected">( + "disconnected", + ); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); @@ -670,7 +674,7 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ์ฒ˜๋ฆฌ (์…€๋ ‰ํŠธ๋ฐ•์Šค ๋“ฑ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ๊ฐ’์œผ๋กœ ํ•„ํ„ฐ๋ง) useEffect(() => { const linkedFilters = tableConfig.linkedFilters; - + if (!linkedFilters || linkedFilters.length === 0 || !screenContext) { return; } @@ -689,7 +693,7 @@ export const TableListComponent: React.FC = ({ if (selectedData && selectedData.length > 0) { const sourceField = filter.sourceField || "value"; const value = selectedData[0][sourceField]; - + if (value !== linkedFilterValues[filter.targetColumn]) { newFilterValues[filter.targetColumn] = value; hasChanges = true; @@ -703,13 +707,13 @@ export const TableListComponent: React.FC = ({ if (hasChanges) { console.log("๐Ÿ”— [TableList] ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ๊ฐ’ ๋ณ€๊ฒฝ:", newFilterValues); setLinkedFilterValues(newFilterValues); - + // searchValues์— ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ๊ฐ’ ๋ณ‘ํ•ฉ - setSearchValues(prev => ({ + setSearchValues((prev) => ({ ...prev, - ...newFilterValues + ...newFilterValues, })); - + // ์ฒซ ํŽ˜์ด์ง€๋กœ ์ด๋™ setCurrentPage(1); } @@ -730,7 +734,7 @@ export const TableListComponent: React.FC = ({ const dataProvider: DataProvidable = { componentId: component.id, componentType: "table-list", - + getSelectedData: () => { // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ์—์„œ ์„ ํƒ๋œ ํ–‰๋งŒ ๋ฐ˜ํ™˜ (์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ์ œ์™ธ) const selectedData = filteredData.filter((row) => { @@ -739,12 +743,12 @@ export const TableListComponent: React.FC = ({ }); return selectedData; }, - + getAllData: () => { // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ return filteredData; }, - + clearSelection: () => { setSelectedRows(new Set()); setIsAllSelected(false); @@ -755,7 +759,7 @@ export const TableListComponent: React.FC = ({ const dataReceiver: DataReceivable = { componentId: component.id, componentType: "table", - + receiveData: async (receivedData: any[], config: DataReceiverConfig) => { console.log("๐Ÿ“ฅ TableList ๋ฐ์ดํ„ฐ ์ˆ˜์‹ :", { componentId: component.id, @@ -782,8 +786,8 @@ export const TableListComponent: React.FC = ({ case "merge": // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ‘ํ•ฉ (ID ๊ธฐ๋ฐ˜) - const existingMap = new Map(data.map(item => [item.id, item])); - receivedData.forEach(item => { + const existingMap = new Map(data.map((item) => [item.id, item])); + receivedData.forEach((item) => { if (item.id && existingMap.has(item.id)) { // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ existingMap.set(item.id, { ...existingMap.get(item.id), ...item }); @@ -799,7 +803,7 @@ export const TableListComponent: React.FC = ({ // ์ƒํƒœ ์—…๋ฐ์ดํŠธ setData(newData); - + // ์ด ์•„์ดํ…œ ์ˆ˜ ์—…๋ฐ์ดํŠธ setTotalItems(newData.length); @@ -809,7 +813,7 @@ export const TableListComponent: React.FC = ({ throw error; } }, - + getData: () => { return data; }, @@ -820,18 +824,19 @@ export const TableListComponent: React.FC = ({ if (screenContext && component.id) { screenContext.registerDataProvider(component.id, dataProvider); screenContext.registerDataReceiver(component.id, dataReceiver); - + return () => { screenContext.unregisterDataProvider(component.id); screenContext.unregisterDataReceiver(component.id); }; } }, [screenContext, component.id, data, selectedRows]); - + // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ์— ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก // useSplitPanelPosition ํ›…์œผ๋กœ ์œ„์น˜ ๊ฐ€์ ธ์˜ค๊ธฐ (์ค‘์ฒฉ๋œ ํ™”๋ฉด์—์„œ๋„ ์ž‘๋™) - const currentSplitPosition = splitPanelPosition || splitPanelContext?.getPositionByScreenId(screenId as number) || null; - + const currentSplitPosition = + splitPanelPosition || splitPanelContext?.getPositionByScreenId(screenId as number) || null; + useEffect(() => { if (splitPanelContext && component.id && currentSplitPosition) { const splitPanelReceiver = { @@ -843,7 +848,7 @@ export const TableListComponent: React.FC = ({ mode, position: currentSplitPosition, }); - + await dataReceiver.receiveData(incomingData, { targetComponentId: component.id, targetComponentType: "table-list", @@ -852,9 +857,9 @@ export const TableListComponent: React.FC = ({ }); }, }; - + splitPanelContext.registerReceiver(currentSplitPosition, component.id, splitPanelReceiver); - + return () => { splitPanelContext.unregisterReceiver(currentSplitPosition, component.id); }; @@ -863,11 +868,12 @@ export const TableListComponent: React.FC = ({ // ํ…Œ์ด๋ธ” ๋“ฑ๋ก (Context์— ๋“ฑ๋ก) const tableId = `table-list-${component.id}`; - + useEffect(() => { // tableConfig.columns๋ฅผ ์ง์ ‘ ์‚ฌ์šฉ (displayColumns๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์Œ) - const columnsToRegister = (tableConfig.columns || []) - .filter((col) => col.visible !== false && col.columnName !== "__checkbox__"); + const columnsToRegister = (tableConfig.columns || []).filter( + (col) => col.visible !== false && col.columnName !== "__checkbox__", + ); if (!tableConfig.selectedTable || !columnsToRegister || columnsToRegister.length === 0) { return; @@ -884,7 +890,7 @@ export const TableListComponent: React.FC = ({ const meta = columnMeta[columnName]; const inputType = meta?.inputType || "text"; - + // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ์ „์ฒด ์ •์˜๋œ ๊ฐ’ ์กฐํšŒ (๋ฐฑ์—”๋“œ API) if (inputType === "category") { try { @@ -892,25 +898,23 @@ export const TableListComponent: React.FC = ({ tableName: tableConfig.selectedTable, columnName, }); - + // API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ (์ฟ ํ‚ค ์ธ์ฆ ์ž๋™ ์ฒ˜๋ฆฌ) const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get( - `/table-categories/${tableConfig.selectedTable}/${columnName}/values` - ); - + const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); + if (response.data.success && response.data.data) { const categoryOptions = response.data.data.map((item: any) => ({ - value: item.valueCode, // ์นด๋ฉœ์ผ€์ด์Šค + value: item.valueCode, // ์นด๋ฉœ์ผ€์ด์Šค label: item.valueLabel, // ์นด๋ฉœ์ผ€์ด์Šค })); - + console.log("โœ… [getColumnUniqueValues] ์นดํ…Œ๊ณ ๋ฆฌ ์ „์ฒด ๊ฐ’:", { columnName, count: categoryOptions.length, options: categoryOptions, }); - + return categoryOptions; } else { console.warn("โš ๏ธ [getColumnUniqueValues] ์‘๋‹ต ํ˜•์‹ ์˜ค๋ฅ˜:", response.data); @@ -926,7 +930,7 @@ export const TableListComponent: React.FC = ({ // ์—๋Ÿฌ ์‹œ ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ fallback } } - + // ์ผ๋ฐ˜ ํƒ€์ž… ๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ ์กฐํšŒ ์‹คํŒจ ์‹œ: ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ const isLabelType = ["category", "entity", "code"].includes(inputType); const labelField = isLabelType ? `${columnName}_name` : columnName; @@ -942,7 +946,7 @@ export const TableListComponent: React.FC = ({ // ํ˜„์žฌ ๋กœ๋“œ๋œ ๋ฐ์ดํ„ฐ์—์„œ ๊ณ ์œ  ๊ฐ’ ์ถ”์ถœ const uniqueValuesMap = new Map(); // value -> label - + data.forEach((row) => { const value = row[columnName]; if (value !== null && value !== undefined && value !== "") { @@ -990,6 +994,7 @@ export const TableListComponent: React.FC = ({ onGroupChange: setGrouping, onColumnVisibilityChange: setColumnVisibility, getColumnUniqueValues, // ๊ณ ์œ  ๊ฐ’ ์กฐํšŒ ํ•จ์ˆ˜ ๋“ฑ๋ก + onGroupSumChange: setGroupSumConfig, // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • }; registerTable(registration); @@ -1006,7 +1011,7 @@ export const TableListComponent: React.FC = ({ columnWidths, tableLabel, data, // ๋ฐ์ดํ„ฐ ์ž์ฒด๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์žฌ๋“ฑ๋ก (๊ณ ์œ  ๊ฐ’ ์กฐํšŒ์šฉ) - totalItems, // ์ „์ฒด ํ•ญ๋ชฉ ์ˆ˜๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์žฌ๋“ฑ๋ก + totalItems, // ์ „์ฒด ํ•ญ๋ชฉ ์ˆ˜๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์žฌ๋“ฑ๋ก registerTable, unregisterTable, ]); @@ -1226,7 +1231,7 @@ export const TableListComponent: React.FC = ({ const cols = Object.entries(columnMeta) .filter(([_, meta]) => meta.inputType === "category") .map(([columnName, _]) => columnName); - + return cols; }, [columnMeta]); @@ -1251,7 +1256,7 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ: "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•ํƒœ์ธ์ง€ ํ™•์ธ let targetTable = tableConfig.selectedTable; let targetColumn = columnName; - + if (columnName.includes(".")) { const parts = columnName.split("."); targetTable = parts[0]; // ์กฐ์ธ๋œ ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: item_info) @@ -1278,7 +1283,7 @@ export const TableListComponent: React.FC = ({ if (response.data.success && response.data.data && Array.isArray(response.data.data)) { const mapping: Record = {}; - + response.data.data.forEach((item: any) => { // valueCode๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ํ‚ค๋กœ ์‚ฌ์šฉ const key = String(item.valueCode); @@ -1288,7 +1293,7 @@ export const TableListComponent: React.FC = ({ }; console.log(` ๐Ÿ”‘ [${columnName}] "${key}" => "${item.valueLabel}" (์ƒ‰์ƒ: ${item.color})`); }); - + if (Object.keys(mapping).length > 0) { // ๐Ÿ†• ์›๋ž˜ ์ปฌ๋Ÿผ๋ช…(item_info.material)์œผ๋กœ ๋งคํ•‘ ์ €์žฅ mappings[columnName] = mapping; @@ -1321,33 +1326,35 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ์˜ inputType ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ๋ฐ ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ๋กœ๋“œ // 1. "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•ํƒœ์˜ ์กฐ์ธ ์ปฌ๋Ÿผ ์ถ”์ถœ - const joinedColumns = tableConfig.columns - ?.filter((col) => col.columnName?.includes(".")) - .map((col) => col.columnName) || []; - + const joinedColumns = + tableConfig.columns?.filter((col) => col.columnName?.includes(".")).map((col) => col.columnName) || []; + // 2. additionalJoinInfo๊ฐ€ ์žˆ๋Š” ์ปฌ๋Ÿผ๋„ ์ถ”์ถœ (์˜ˆ: item_code_material โ†’ item_info.material) - const additionalJoinColumns = tableConfig.columns - ?.filter((col: any) => col.additionalJoinInfo?.referenceTable) - .map((col: any) => ({ - columnName: col.columnName, // ์˜ˆ: item_code_material - referenceTable: col.additionalJoinInfo.referenceTable, // ์˜ˆ: item_info - // joinAlias์—์„œ ์‹ค์ œ ์ปฌ๋Ÿผ๋ช… ์ถ”์ถœ (item_code_material โ†’ material) - actualColumn: col.additionalJoinInfo.joinAlias?.replace(`${col.additionalJoinInfo.sourceColumn}_`, '') || col.columnName, - })) || []; - + const additionalJoinColumns = + tableConfig.columns + ?.filter((col: any) => col.additionalJoinInfo?.referenceTable) + .map((col: any) => ({ + columnName: col.columnName, // ์˜ˆ: item_code_material + referenceTable: col.additionalJoinInfo.referenceTable, // ์˜ˆ: item_info + // joinAlias์—์„œ ์‹ค์ œ ์ปฌ๋Ÿผ๋ช… ์ถ”์ถœ (item_code_material โ†’ material) + actualColumn: + col.additionalJoinInfo.joinAlias?.replace(`${col.additionalJoinInfo.sourceColumn}_`, "") || + col.columnName, + })) || []; + console.log("๐Ÿ” [TableList] additionalJoinInfo ์ปฌ๋Ÿผ:", additionalJoinColumns); - + // ์กฐ์ธ ํ…Œ์ด๋ธ”๋ณ„๋กœ ๊ทธ๋ฃนํ™” const joinedTableColumns: Record = {}; - + // "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•ํƒœ ์ฒ˜๋ฆฌ for (const joinedColumn of joinedColumns) { const parts = joinedColumn.split("."); if (parts.length !== 2) continue; - + const joinedTable = parts[0]; const joinedColumnName = parts[1]; - + if (!joinedTableColumns[joinedTable]) { joinedTableColumns[joinedTable] = []; } @@ -1356,7 +1363,7 @@ export const TableListComponent: React.FC = ({ actualColumn: joinedColumnName, }); } - + // additionalJoinInfo ํ˜•ํƒœ ์ฒ˜๋ฆฌ for (const col of additionalJoinColumns) { if (!joinedTableColumns[col.referenceTable]) { @@ -1367,41 +1374,43 @@ export const TableListComponent: React.FC = ({ actualColumn: col.actualColumn, // ์˜ˆ: material }); } - + console.log("๐Ÿ” [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ”๋ณ„ ์ปฌ๋Ÿผ:", joinedTableColumns); - + // ์กฐ์ธ๋œ ํ…Œ์ด๋ธ”๋ณ„๋กœ inputType ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ const newJoinedColumnMeta: Record = {}; - + for (const [joinedTable, columns] of Object.entries(joinedTableColumns)) { try { // ์กฐ์ธ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ inputType ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ด๋ฏธ import๋œ tableTypeApi ์‚ฌ์šฉ) const inputTypes = await tableTypeApi.getColumnInputTypes(joinedTable); - + console.log(`๐Ÿ“ก [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ” inputType ๋กœ๋“œ [${joinedTable}]:`, inputTypes); - + for (const col of columns) { const inputTypeInfo = inputTypes.find((it: any) => it.columnName === col.actualColumn); - + // ์ปฌ๋Ÿผ๋ช… ๊ทธ๋Œ€๋กœ ์ €์žฅ (item_code_material ๋˜๋Š” item_info.material) newJoinedColumnMeta[col.columnName] = { inputType: inputTypeInfo?.inputType, }; - - console.log(` ๐Ÿ”— [${col.columnName}] (์‹ค์ œ: ${col.actualColumn}) inputType: ${inputTypeInfo?.inputType || "unknown"}`); - + + console.log( + ` ๐Ÿ”— [${col.columnName}] (์‹ค์ œ: ${col.actualColumn}) inputType: ${inputTypeInfo?.inputType || "unknown"}`, + ); + // inputType์ด category์ธ ๊ฒฝ์šฐ ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ๋กœ๋“œ if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) { try { console.log(`๐Ÿ“ก [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ” ์นดํ…Œ๊ณ ๋ฆฌ ๋กœ๋“œ ์‹œ๋„ [${col.columnName}]:`, { url: `/table-categories/${joinedTable}/${col.actualColumn}/values`, }); - + const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`); - + if (response.data.success && response.data.data && Array.isArray(response.data.data)) { const mapping: Record = {}; - + response.data.data.forEach((item: any) => { const key = String(item.valueCode); mapping[key] = { @@ -1409,7 +1418,7 @@ export const TableListComponent: React.FC = ({ color: item.color, }; }); - + if (Object.keys(mapping).length > 0) { mappings[col.columnName] = mapping; console.log(`โœ… [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ” ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ๋กœ๋“œ ์™„๋ฃŒ [${col.columnName}]:`, { @@ -1426,7 +1435,7 @@ export const TableListComponent: React.FC = ({ console.error(`โŒ [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ” inputType ๋กœ๋“œ ์‹คํŒจ [${joinedTable}]:`, error); } } - + // ์กฐ์ธ ์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ if (Object.keys(newJoinedColumnMeta).length > 0) { setJoinedColumnMeta(newJoinedColumnMeta); @@ -1438,7 +1447,7 @@ export const TableListComponent: React.FC = ({ mappingsKeys: Object.keys(mappings), mappings, }); - + if (Object.keys(mappings).length > 0) { setCategoryMappings(mappings); setCategoryMappingsKey((prev) => prev + 1); @@ -1452,7 +1461,12 @@ export const TableListComponent: React.FC = ({ }; loadCategoryMappings(); - }, [tableConfig.selectedTable, categoryColumns.length, JSON.stringify(categoryColumns), JSON.stringify(tableConfig.columns)]); // ๋” ๋ช…ํ™•ํ•œ ์˜์กด์„ฑ + }, [ + tableConfig.selectedTable, + categoryColumns.length, + JSON.stringify(categoryColumns), + JSON.stringify(tableConfig.columns), + ]); // ๋” ๋ช…ํ™•ํ•œ ์˜์กด์„ฑ // ======================================== // ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ @@ -1464,7 +1478,7 @@ export const TableListComponent: React.FC = ({ isDesignMode, currentPage, }); - + if (!tableConfig.selectedTable || isDesignMode) { setData([]); setTotalPages(0); @@ -1481,34 +1495,35 @@ export const TableListComponent: React.FC = ({ const sortBy = sortColumn || undefined; const sortOrder = sortDirection; const search = searchTerm || undefined; - + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€์ผ ๋•Œ) let linkedFilterValues: Record = {}; let hasLinkedFiltersConfigured = false; // ์—ฐ๊ฒฐ ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ let hasSelectedLeftData = false; // ์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€ - + console.log("๐Ÿ” [TableList] ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ ํ™•์ธ:", { hasSplitPanelContext: !!splitPanelContext, tableName: tableConfig.selectedTable, selectedLeftData: splitPanelContext?.selectedLeftData, linkedFilters: splitPanelContext?.linkedFilters, }); - + if (splitPanelContext) { // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • ์—ฌ๋ถ€ ํ™•์ธ (ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” ํ•„ํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€) const linkedFiltersConfig = splitPanelContext.linkedFilters || []; hasLinkedFiltersConfigured = linkedFiltersConfig.some( - (filter) => filter.targetColumn?.startsWith(tableConfig.selectedTable + ".") || - filter.targetColumn === tableConfig.selectedTable + (filter) => + filter.targetColumn?.startsWith(tableConfig.selectedTable + ".") || + filter.targetColumn === tableConfig.selectedTable, ); - + // ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์„ ํƒ ์—ฌ๋ถ€ ํ™•์ธ - hasSelectedLeftData = splitPanelContext.selectedLeftData && - Object.keys(splitPanelContext.selectedLeftData).length > 0; - + hasSelectedLeftData = + splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0; + const allLinkedFilters = splitPanelContext.getLinkedFilterValues(); console.log("๐Ÿ”— [TableList] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์›๋ณธ:", allLinkedFilters); - + // ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” ํ•„ํ„ฐ๋งŒ ์ถ”์ถœ (ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช… ํ˜•์‹์—์„œ) for (const [key, value] of Object.entries(allLinkedFilters)) { if (key.includes(".")) { @@ -1526,7 +1541,7 @@ export const TableListComponent: React.FC = ({ console.log("๐Ÿ”— [TableList] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์ ์šฉ:", linkedFilterValues); } } - + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€๋งŒ ์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ // โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ (๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์Œ) if (hasLinkedFiltersConfigured && !hasSelectedLeftData) { @@ -1536,7 +1551,7 @@ export const TableListComponent: React.FC = ({ setLoading(false); return; } - + // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ์™€ ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๋ณ‘ํ•ฉ const filters = { ...(Object.keys(searchValues).length > 0 ? searchValues : {}), @@ -1545,18 +1560,19 @@ export const TableListComponent: React.FC = ({ const hasFilters = Object.keys(filters).length > 0; // ๐Ÿ†• REST API ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ฒ˜๋ฆฌ - const isRestApiTable = tableConfig.selectedTable.startsWith("restapi_") || tableConfig.selectedTable.startsWith("_restapi_"); - + const isRestApiTable = + tableConfig.selectedTable.startsWith("restapi_") || tableConfig.selectedTable.startsWith("_restapi_"); + let response: any; - + if (isRestApiTable) { // REST API ๋ฐ์ดํ„ฐ ์†Œ์Šค์ธ ๊ฒฝ์šฐ const connectionIdMatch = tableConfig.selectedTable.match(/restapi_(\d+)/); const connectionId = connectionIdMatch ? parseInt(connectionIdMatch[1]) : null; - + if (connectionId) { console.log("๐ŸŒ [TableList] REST API ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ˜ธ์ถœ", { connectionId }); - + // REST API ์—ฐ๊ฒฐ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ๋ฐ ๋ฐ์ดํ„ฐ ์กฐํšŒ const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection"); const restApiData = await ExternalRestApiConnectionAPI.fetchData( @@ -1564,16 +1580,16 @@ export const TableListComponent: React.FC = ({ undefined, // endpoint - ์—ฐ๊ฒฐ ์ •๋ณด์—์„œ ๊ฐ€์ ธ์˜ด "response", // jsonPath - ๊ธฐ๋ณธ๊ฐ’ response ); - + response = { data: restApiData.rows || [], total: restApiData.total || restApiData.rows?.length || 0, totalPages: Math.ceil((restApiData.total || restApiData.rows?.length || 0) / pageSize), }; - - console.log("โœ… [TableList] REST API ์‘๋‹ต:", { - dataLength: response.data.length, - total: response.total + + console.log("โœ… [TableList] REST API ์‘๋‹ต:", { + dataLength: response.data.length, + total: response.total, }); } else { throw new Error("REST API ์—ฐ๊ฒฐ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); @@ -1604,114 +1620,114 @@ export const TableListComponent: React.FC = ({ console.log("๐ŸŽฏ [TableList] ํ™”๋ฉด๋ณ„ ์—”ํ‹ฐํ‹ฐ ์„ค์ •:", screenEntityConfigs); - // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ ์ฒ˜๋ฆฌ (๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ œ์™ธ) - let excludeFilterParam: any = undefined; - if (tableConfig.excludeFilter?.enabled) { - const excludeConfig = tableConfig.excludeFilter; - let filterValue: any = undefined; - - // ํ•„ํ„ฐ ๊ฐ’ ์†Œ์Šค์— ๋”ฐ๋ผ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (์šฐ์„ ์ˆœ์œ„: formData > URL > ๋ถ„ํ• ํŒจ๋„) - if (excludeConfig.filterColumn && excludeConfig.filterValueField) { - const fieldName = excludeConfig.filterValueField; - - // 1์ˆœ์œ„: props๋กœ ์ „๋‹ฌ๋ฐ›์€ formData์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ชจ๋‹ฌ์—์„œ ์‚ฌ์šฉ) - if (propFormData && propFormData[fieldName]) { - filterValue = propFormData[fieldName]; - console.log("๐Ÿ”— [TableList] formData์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { - field: fieldName, - value: filterValue, - }); - } - // 2์ˆœ์œ„: URL ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ - else if (typeof window !== "undefined") { - const urlParams = new URLSearchParams(window.location.search); - filterValue = urlParams.get(fieldName); - if (filterValue) { - console.log("๐Ÿ”— [TableList] URL์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { + // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ ์ฒ˜๋ฆฌ (๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ œ์™ธ) + let excludeFilterParam: any = undefined; + if (tableConfig.excludeFilter?.enabled) { + const excludeConfig = tableConfig.excludeFilter; + let filterValue: any = undefined; + + // ํ•„ํ„ฐ ๊ฐ’ ์†Œ์Šค์— ๋”ฐ๋ผ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (์šฐ์„ ์ˆœ์œ„: formData > URL > ๋ถ„ํ• ํŒจ๋„) + if (excludeConfig.filterColumn && excludeConfig.filterValueField) { + const fieldName = excludeConfig.filterValueField; + + // 1์ˆœ์œ„: props๋กœ ์ „๋‹ฌ๋ฐ›์€ formData์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ชจ๋‹ฌ์—์„œ ์‚ฌ์šฉ) + if (propFormData && propFormData[fieldName]) { + filterValue = propFormData[fieldName]; + console.log("๐Ÿ”— [TableList] formData์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { field: fieldName, value: filterValue, }); } - } - // 3์ˆœ์œ„: ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ - if (!filterValue && splitPanelContext?.selectedLeftData) { - filterValue = splitPanelContext.selectedLeftData[fieldName]; - if (filterValue) { - console.log("๐Ÿ”— [TableList] ๋ถ„ํ• ํŒจ๋„์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { - field: fieldName, - value: filterValue, - }); + // 2์ˆœ์œ„: URL ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + else if (typeof window !== "undefined") { + const urlParams = new URLSearchParams(window.location.search); + filterValue = urlParams.get(fieldName); + if (filterValue) { + console.log("๐Ÿ”— [TableList] URL์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { + field: fieldName, + value: filterValue, + }); + } + } + // 3์ˆœ์œ„: ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + if (!filterValue && splitPanelContext?.selectedLeftData) { + filterValue = splitPanelContext.selectedLeftData[fieldName]; + if (filterValue) { + console.log("๐Ÿ”— [TableList] ๋ถ„ํ• ํŒจ๋„์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { + field: fieldName, + value: filterValue, + }); + } } } - } - - if (filterValue || !excludeConfig.filterColumn) { - excludeFilterParam = { - enabled: true, - referenceTable: excludeConfig.referenceTable, - referenceColumn: excludeConfig.referenceColumn, - sourceColumn: excludeConfig.sourceColumn, - filterColumn: excludeConfig.filterColumn, - filterValue: filterValue, - }; - console.log("๐Ÿšซ [TableList] ์ œ์™ธ ํ•„ํ„ฐ ์ ์šฉ:", excludeFilterParam); - } - } - // ๐ŸŽฏ ํ•ญ์ƒ entityJoinApi ์‚ฌ์šฉ (writer ์ปฌ๋Ÿผ ์ž๋™ ์กฐ์ธ ์ง€์›) - response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { - page, - size: pageSize, - sortBy, - sortOrder, - search: hasFilters ? filters : undefined, - enableEntityJoin: true, - additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined, - screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // ๐ŸŽฏ ํ™”๋ฉด๋ณ„ ์—”ํ‹ฐํ‹ฐ ์„ค์ • ์ „๋‹ฌ - dataFilter: tableConfig.dataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ „๋‹ฌ - excludeFilter: excludeFilterParam, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ ์ „๋‹ฌ - }); - - // ์‹ค์ œ ๋ฐ์ดํ„ฐ์˜ item_number๋งŒ ์ถ”์ถœํ•˜์—ฌ ์ค‘๋ณต ํ™•์ธ - const itemNumbers = (response.data || []).map((item: any) => item.item_number); - const uniqueItemNumbers = [...new Set(itemNumbers)]; - - // console.log("โœ… [TableList] API ์‘๋‹ต ๋ฐ›์Œ"); - // console.log(` - dataLength: ${response.data?.length || 0}`); - // console.log(` - total: ${response.total}`); - // console.log(` - itemNumbers: ${JSON.stringify(itemNumbers)}`); - // console.log(` - uniqueItemNumbers: ${JSON.stringify(uniqueItemNumbers)}`); - // console.log(` - isDuplicated: ${itemNumbers.length !== uniqueItemNumbers.length}`); - - setData(response.data || []); - setTotalPages(response.totalPages || 0); - setTotalItems(response.total || 0); - setError(null); - - // ๐ŸŽฏ Store์— ํ•„ํ„ฐ ์กฐ๊ฑด ์ €์žฅ (์—‘์…€ ๋‹ค์šด๋กœ๋“œ์šฉ) - // tableConfig.columns ์‚ฌ์šฉ (visibleColumns๋Š” ์ด ์‹œ์ ์—์„œ ์•„์ง ์ •์˜๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ) - const cols = (tableConfig.columns || []).filter((col) => col.visible !== false); - const labels: Record = {}; - cols.forEach((col) => { - labels[col.columnName] = columnLabels[col.columnName] || col.columnName; - }); - - tableDisplayStore.setTableData( - tableConfig.selectedTable, - response.data || [], - cols.map((col) => col.columnName), - sortBy, - sortOrder, - { - filterConditions: filters, - searchTerm: search, - visibleColumns: cols.map((col) => col.columnName), - columnLabels: labels, - currentPage: page, - pageSize: pageSize, - totalItems: response.total || 0, + if (filterValue || !excludeConfig.filterColumn) { + excludeFilterParam = { + enabled: true, + referenceTable: excludeConfig.referenceTable, + referenceColumn: excludeConfig.referenceColumn, + sourceColumn: excludeConfig.sourceColumn, + filterColumn: excludeConfig.filterColumn, + filterValue: filterValue, + }; + console.log("๐Ÿšซ [TableList] ์ œ์™ธ ํ•„ํ„ฐ ์ ์šฉ:", excludeFilterParam); + } } - ); + + // ๐ŸŽฏ ํ•ญ์ƒ entityJoinApi ์‚ฌ์šฉ (writer ์ปฌ๋Ÿผ ์ž๋™ ์กฐ์ธ ์ง€์›) + response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { + page, + size: pageSize, + sortBy, + sortOrder, + search: hasFilters ? filters : undefined, + enableEntityJoin: true, + additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined, + screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // ๐ŸŽฏ ํ™”๋ฉด๋ณ„ ์—”ํ‹ฐํ‹ฐ ์„ค์ • ์ „๋‹ฌ + dataFilter: tableConfig.dataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ „๋‹ฌ + excludeFilter: excludeFilterParam, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ ์ „๋‹ฌ + }); + + // ์‹ค์ œ ๋ฐ์ดํ„ฐ์˜ item_number๋งŒ ์ถ”์ถœํ•˜์—ฌ ์ค‘๋ณต ํ™•์ธ + const itemNumbers = (response.data || []).map((item: any) => item.item_number); + const uniqueItemNumbers = [...new Set(itemNumbers)]; + + // console.log("โœ… [TableList] API ์‘๋‹ต ๋ฐ›์Œ"); + // console.log(` - dataLength: ${response.data?.length || 0}`); + // console.log(` - total: ${response.total}`); + // console.log(` - itemNumbers: ${JSON.stringify(itemNumbers)}`); + // console.log(` - uniqueItemNumbers: ${JSON.stringify(uniqueItemNumbers)}`); + // console.log(` - isDuplicated: ${itemNumbers.length !== uniqueItemNumbers.length}`); + + setData(response.data || []); + setTotalPages(response.totalPages || 0); + setTotalItems(response.total || 0); + setError(null); + + // ๐ŸŽฏ Store์— ํ•„ํ„ฐ ์กฐ๊ฑด ์ €์žฅ (์—‘์…€ ๋‹ค์šด๋กœ๋“œ์šฉ) + // tableConfig.columns ์‚ฌ์šฉ (visibleColumns๋Š” ์ด ์‹œ์ ์—์„œ ์•„์ง ์ •์˜๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ) + const cols = (tableConfig.columns || []).filter((col) => col.visible !== false); + const labels: Record = {}; + cols.forEach((col) => { + labels[col.columnName] = columnLabels[col.columnName] || col.columnName; + }); + + tableDisplayStore.setTableData( + tableConfig.selectedTable, + response.data || [], + cols.map((col) => col.columnName), + sortBy, + sortOrder, + { + filterConditions: filters, + searchTerm: search, + visibleColumns: cols.map((col) => col.columnName), + columnLabels: labels, + currentPage: page, + pageSize: pageSize, + totalItems: response.total || 0, + }, + ); } } catch (err: any) { console.error("๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:", err); @@ -1779,10 +1795,13 @@ export const TableListComponent: React.FC = ({ if (tableConfig.selectedTable && userId) { const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`; try { - localStorage.setItem(storageKey, JSON.stringify({ - column: newSortColumn, - direction: newSortDirection - })); + localStorage.setItem( + storageKey, + JSON.stringify({ + column: newSortColumn, + direction: newSortDirection, + }), + ); console.log("๐Ÿ’พ ์ •๋ ฌ ์ƒํƒœ ์ €์žฅ:", { column: newSortColumn, direction: newSortDirection }); } catch (error) { console.error("โŒ ์ •๋ ฌ ์ƒํƒœ ์ €์žฅ ์‹คํŒจ:", error); @@ -1864,16 +1883,16 @@ export const TableListComponent: React.FC = ({ // ์ „์—ญ ์ €์žฅ์†Œ์— ์ •๋ ฌ๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ if (tableConfig.selectedTable) { - const cleanColumnOrder = ( - columnOrder.length > 0 ? columnOrder : cols.map((c) => c.columnName) - ).filter((col) => col !== "__checkbox__"); - + const cleanColumnOrder = (columnOrder.length > 0 ? columnOrder : cols.map((c) => c.columnName)).filter( + (col) => col !== "__checkbox__", + ); + // ์ปฌ๋Ÿผ ๋ผ๋ฒจ ์ •๋ณด๋„ ํ•จ๊ป˜ ์ €์žฅ const labels: Record = {}; cols.forEach((col) => { labels[col.columnName] = columnLabels[col.columnName] || col.columnName; }); - + tableDisplayStore.setTableData( tableConfig.selectedTable, reorderedData, @@ -1952,7 +1971,7 @@ export const TableListComponent: React.FC = ({ originalData: row, additionalData: {}, })); - + useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems); console.log("โœ… [TableList] modalDataStore์— ๋ฐ์ดํ„ฐ ์ €์žฅ:", { dataSourceId: tableConfig.selectedTable, @@ -1996,7 +2015,7 @@ export const TableListComponent: React.FC = ({ originalData: row, additionalData: {}, })); - + useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems); console.log("โœ… [TableList] modalDataStore์— ์ „์ฒด ๋ฐ์ดํ„ฐ ์ €์žฅ:", { dataSourceId: tableConfig.selectedTable, @@ -2067,21 +2086,24 @@ export const TableListComponent: React.FC = ({ }; // ๐Ÿ†• ์…€ ๋”๋ธ”ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ (ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž…) - visibleColumns ์ •์˜ ํ›„ ์‚ฌ์šฉ - const handleCellDoubleClick = useCallback((rowIndex: number, colIndex: number, columnName: string, value: any) => { - // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ ๋ถˆ๊ฐ€ - if (columnName === "__checkbox__") return; + const handleCellDoubleClick = useCallback( + (rowIndex: number, colIndex: number, columnName: string, value: any) => { + // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ ๋ถˆ๊ฐ€ + if (columnName === "__checkbox__") return; - // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ - const column = visibleColumns.find((col) => col.columnName === columnName); - if (column?.editable === false) { - toast.warning(`'${column.displayName || columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); - return; - } + // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ + const column = visibleColumns.find((col) => col.columnName === columnName); + if (column?.editable === false) { + toast.warning(`'${column.displayName || columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); + return; + } - setEditingCell({ rowIndex, colIndex, columnName, originalValue: value }); - setEditingValue(value !== null && value !== undefined ? String(value) : ""); - setFocusedCell({ rowIndex, colIndex }); - }, [visibleColumns]); + setEditingCell({ rowIndex, colIndex, columnName, originalValue: value }); + setEditingValue(value !== null && value !== undefined ? String(value) : ""); + setFocusedCell({ rowIndex, colIndex }); + }, + [visibleColumns], + ); // ๐Ÿ†• ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… placeholder (์‹ค์ œ ๊ตฌํ˜„์€ visibleColumns ์ •์˜ ํ›„) const startEditingRef = useRef<() => void>(() => {}); @@ -2089,25 +2111,25 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ๊ฐ ์ปฌ๋Ÿผ์˜ ๊ณ ์œ ๊ฐ’ ๋ชฉ๋ก ๊ณ„์‚ฐ const columnUniqueValues = useMemo(() => { const result: Record = {}; - + if (data.length === 0) return result; (tableConfig.columns || []).forEach((column: { columnName: string }) => { if (column.columnName === "__checkbox__") return; - + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; const values = new Set(); - + data.forEach((row) => { const val = row[mappedColumnName]; if (val !== null && val !== undefined && val !== "") { values.add(String(val)); } }); - + result[column.columnName] = Array.from(values).sort(); }); - + return result; }, [data, tableConfig.columns, joinColumnMapping]); @@ -2116,13 +2138,13 @@ export const TableListComponent: React.FC = ({ setHeaderFilters((prev) => { const current = prev[columnName] || new Set(); const newSet = new Set(current); - + if (newSet.has(value)) { newSet.delete(value); } else { newSet.add(value); } - + return { ...prev, [columnName]: newSet }; }); }, []); @@ -2147,14 +2169,14 @@ export const TableListComponent: React.FC = ({ // ํ˜•์‹: { columnName: { type: 'sum' | 'avg' | 'count' | 'min' | 'max', label?: string } } const summaryConfig = useMemo(() => { const config: Record = {}; - + // tableConfig์—์„œ summary ์„ค์ • ์ฝ๊ธฐ if (tableConfig.summaries) { tableConfig.summaries.forEach((summary: { columnName: string; type: string; label?: string }) => { config[summary.columnName] = { type: summary.type, label: summary.label }; }); } - + return config; }, [tableConfig.summaries]); @@ -2308,7 +2330,16 @@ export const TableListComponent: React.FC = ({ } cancelEditing(); - }, [editingCell, editingValue, data, tableConfig.selectedTable, tableConfig.primaryKey, cancelEditing, editMode, pendingChanges.size]); + }, [ + editingCell, + editingValue, + data, + tableConfig.selectedTable, + tableConfig.primaryKey, + cancelEditing, + editMode, + pendingChanges.size, + ]); // ๐Ÿ†• ๋ฐฐ์น˜ ์ €์žฅ: ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ•œ๋ฒˆ์— ์ €์žฅ const saveBatchChanges = useCallback(async () => { @@ -2329,7 +2360,7 @@ export const TableListComponent: React.FC = ({ keyValue: change.primaryKeyValue, updateField: change.columnName, updateValue: change.newValue, - }) + }), ); await Promise.all(savePromises); @@ -2358,78 +2389,86 @@ export const TableListComponent: React.FC = ({ }, [pendingChanges.size]); // ๐Ÿ†• ํŠน์ • ์…€์ด ์ˆ˜์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - const isCellModified = useCallback((rowIndex: number, columnName: string) => { - return pendingChanges.has(`${rowIndex}-${columnName}`); - }, [pendingChanges]); + const isCellModified = useCallback( + (rowIndex: number, columnName: string) => { + return pendingChanges.has(`${rowIndex}-${columnName}`); + }, + [pendingChanges], + ); // ๐Ÿ†• ์ˆ˜์ •๋œ ์…€ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋กœ์ปฌ ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์šฐ์„ ) - const getDisplayValue = useCallback((row: any, rowIndex: number, columnName: string) => { - const localValue = localEditedData[rowIndex]?.[columnName]; - if (localValue !== undefined) { - return localValue; - } - return row[columnName]; - }, [localEditedData]); + const getDisplayValue = useCallback( + (row: any, rowIndex: number, columnName: string) => { + const localValue = localEditedData[rowIndex]?.[columnName]; + if (localValue !== undefined) { + return localValue; + } + return row[columnName]; + }, + [localEditedData], + ); // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ•จ์ˆ˜ - const validateValue = useCallback(( - value: any, - columnName: string, - row: any - ): string | null => { - // tableConfig.validation์—์„œ ์ปฌ๋Ÿผ๋ณ„ ๊ทœ์น™ ๊ฐ€์ ธ์˜ค๊ธฐ - const rules = (tableConfig as any).validation?.[columnName] as ValidationRule | undefined; - if (!rules) return null; + const validateValue = useCallback( + (value: any, columnName: string, row: any): string | null => { + // tableConfig.validation์—์„œ ์ปฌ๋Ÿผ๋ณ„ ๊ทœ์น™ ๊ฐ€์ ธ์˜ค๊ธฐ + const rules = (tableConfig as any).validation?.[columnName] as ValidationRule | undefined; + if (!rules) return null; - const strValue = value !== null && value !== undefined ? String(value) : ""; - const numValue = parseFloat(strValue); + const strValue = value !== null && value !== undefined ? String(value) : ""; + const numValue = parseFloat(strValue); - // ํ•„์ˆ˜ ๊ฒ€์‚ฌ - if (rules.required && (!strValue || strValue.trim() === "")) { - return rules.customMessage || "ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค."; - } + // ํ•„์ˆ˜ ๊ฒ€์‚ฌ + if (rules.required && (!strValue || strValue.trim() === "")) { + return rules.customMessage || "ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค."; + } - // ๊ฐ’์ด ๋น„์–ด์žˆ์œผ๋ฉด ๋‹ค๋ฅธ ๊ฒ€์‚ฌ ์Šคํ‚ต (required๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ) - if (!strValue || strValue.trim() === "") return null; + // ๊ฐ’์ด ๋น„์–ด์žˆ์œผ๋ฉด ๋‹ค๋ฅธ ๊ฒ€์‚ฌ ์Šคํ‚ต (required๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ) + if (!strValue || strValue.trim() === "") return null; - // ์ตœ์†Œ๊ฐ’ ๊ฒ€์‚ฌ - if (rules.min !== undefined && !isNaN(numValue) && numValue < rules.min) { - return rules.customMessage || `์ตœ์†Œ๊ฐ’์€ ${rules.min}์ž…๋‹ˆ๋‹ค.`; - } + // ์ตœ์†Œ๊ฐ’ ๊ฒ€์‚ฌ + if (rules.min !== undefined && !isNaN(numValue) && numValue < rules.min) { + return rules.customMessage || `์ตœ์†Œ๊ฐ’์€ ${rules.min}์ž…๋‹ˆ๋‹ค.`; + } - // ์ตœ๋Œ€๊ฐ’ ๊ฒ€์‚ฌ - if (rules.max !== undefined && !isNaN(numValue) && numValue > rules.max) { - return rules.customMessage || `์ตœ๋Œ€๊ฐ’์€ ${rules.max}์ž…๋‹ˆ๋‹ค.`; - } + // ์ตœ๋Œ€๊ฐ’ ๊ฒ€์‚ฌ + if (rules.max !== undefined && !isNaN(numValue) && numValue > rules.max) { + return rules.customMessage || `์ตœ๋Œ€๊ฐ’์€ ${rules.max}์ž…๋‹ˆ๋‹ค.`; + } - // ์ตœ์†Œ ๊ธธ์ด ๊ฒ€์‚ฌ - if (rules.minLength !== undefined && strValue.length < rules.minLength) { - return rules.customMessage || `์ตœ์†Œ ${rules.minLength}์ž ์ด์ƒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.`; - } + // ์ตœ์†Œ ๊ธธ์ด ๊ฒ€์‚ฌ + if (rules.minLength !== undefined && strValue.length < rules.minLength) { + return rules.customMessage || `์ตœ์†Œ ${rules.minLength}์ž ์ด์ƒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.`; + } - // ์ตœ๋Œ€ ๊ธธ์ด ๊ฒ€์‚ฌ - if (rules.maxLength !== undefined && strValue.length > rules.maxLength) { - return rules.customMessage || `์ตœ๋Œ€ ${rules.maxLength}์ž๊นŒ์ง€ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.`; - } + // ์ตœ๋Œ€ ๊ธธ์ด ๊ฒ€์‚ฌ + if (rules.maxLength !== undefined && strValue.length > rules.maxLength) { + return rules.customMessage || `์ตœ๋Œ€ ${rules.maxLength}์ž๊นŒ์ง€ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.`; + } - // ํŒจํ„ด ๊ฒ€์‚ฌ - if (rules.pattern && !rules.pattern.test(strValue)) { - return rules.customMessage || "์ž…๋ ฅ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; - } + // ํŒจํ„ด ๊ฒ€์‚ฌ + if (rules.pattern && !rules.pattern.test(strValue)) { + return rules.customMessage || "์ž…๋ ฅ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + } - // ์ปค์Šคํ…€ ๊ฒ€์ฆ - if (rules.validate) { - const customError = rules.validate(value, row); - if (customError) return customError; - } + // ์ปค์Šคํ…€ ๊ฒ€์ฆ + if (rules.validate) { + const customError = rules.validate(value, row); + if (customError) return customError; + } - return null; - }, [tableConfig]); + return null; + }, + [tableConfig], + ); // ๐Ÿ†• ์…€ ์œ ํšจ์„ฑ ์—๋Ÿฌ ์—ฌ๋ถ€ ํ™•์ธ - const getCellValidationError = useCallback((rowIndex: number, columnName: string): string | null => { - return validationErrors.get(`${rowIndex}-${columnName}`) || null; - }, [validationErrors]); + const getCellValidationError = useCallback( + (rowIndex: number, columnName: string): string | null => { + return validationErrors.get(`${rowIndex}-${columnName}`) || null; + }, + [validationErrors], + ); // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ ์„ค์ • const setCellValidationError = useCallback((rowIndex: number, columnName: string, error: string | null) => { @@ -2451,141 +2490,158 @@ export const TableListComponent: React.FC = ({ }, []); // ๐Ÿ†• Excel ๋‚ด๋ณด๋‚ด๊ธฐ ํ•จ์ˆ˜ - const exportToExcel = useCallback((exportAll: boolean = true) => { - try { - // ๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ์„ ํƒ (์„ ํƒ๋œ ํ–‰๋งŒ ๋˜๋Š” ์ „์ฒด) - let exportData: any[]; - if (exportAll) { - exportData = filteredData; - } else { - // ์„ ํƒ๋œ ํ–‰๋งŒ ๋‚ด๋ณด๋‚ด๊ธฐ - exportData = filteredData.filter((row, index) => { - const rowKey = getRowKey(row, index); - return selectedRows.has(rowKey); - }); - } + const exportToExcel = useCallback( + (exportAll: boolean = true) => { + try { + // ๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ์„ ํƒ (์„ ํƒ๋œ ํ–‰๋งŒ ๋˜๋Š” ์ „์ฒด) + let exportData: any[]; + if (exportAll) { + exportData = filteredData; + } else { + // ์„ ํƒ๋œ ํ–‰๋งŒ ๋‚ด๋ณด๋‚ด๊ธฐ + exportData = filteredData.filter((row, index) => { + const rowKey = getRowKey(row, index); + return selectedRows.has(rowKey); + }); + } - if (exportData.length === 0) { - toast.error(exportAll ? "๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." : "์„ ํƒ๋œ ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค."); - return; - } + if (exportData.length === 0) { + toast.error(exportAll ? "๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." : "์„ ํƒ๋œ ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค."); + return; + } - // ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ) - const exportColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__"); - - // ํ—ค๋” ํ–‰ ์ƒ์„ฑ - const headers = exportColumns.map((col) => columnLabels[col.columnName] || col.columnName); - - // ๋ฐ์ดํ„ฐ ํ–‰ ์ƒ์„ฑ - const rows = exportData.map((row) => { - return exportColumns.map((col) => { - const mappedColumnName = joinColumnMapping[col.columnName] || col.columnName; - const value = row[mappedColumnName]; - - // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘๋œ ๊ฐ’ ์ฒ˜๋ฆฌ - if (categoryMappings[col.columnName] && value !== null && value !== undefined) { - const mapping = categoryMappings[col.columnName][String(value)]; - if (mapping) { - return mapping.label; + // ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ) + const exportColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__"); + + // ํ—ค๋” ํ–‰ ์ƒ์„ฑ + const headers = exportColumns.map((col) => columnLabels[col.columnName] || col.columnName); + + // ๋ฐ์ดํ„ฐ ํ–‰ ์ƒ์„ฑ + const rows = exportData.map((row) => { + return exportColumns.map((col) => { + const mappedColumnName = joinColumnMapping[col.columnName] || col.columnName; + const value = row[mappedColumnName]; + + // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘๋œ ๊ฐ’ ์ฒ˜๋ฆฌ + if (categoryMappings[col.columnName] && value !== null && value !== undefined) { + const mapping = categoryMappings[col.columnName][String(value)]; + if (mapping) { + return mapping.label; + } } - } - - // null/undefined ์ฒ˜๋ฆฌ - if (value === null || value === undefined) { - return ""; - } - - return value; + + // null/undefined ์ฒ˜๋ฆฌ + if (value === null || value === undefined) { + return ""; + } + + return value; + }); }); - }); - // ์›Œํฌ์‹œํŠธ ์ƒ์„ฑ - const wsData = [headers, ...rows]; - const ws = XLSX.utils.aoa_to_sheet(wsData); + // ์›Œํฌ์‹œํŠธ ์ƒ์„ฑ + const wsData = [headers, ...rows]; + const ws = XLSX.utils.aoa_to_sheet(wsData); - // ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ž๋™ ์กฐ์ • - const colWidths = exportColumns.map((col, idx) => { - const headerLength = headers[idx]?.length || 10; - const maxDataLength = Math.max( - ...rows.map((row) => String(row[idx] ?? "").length) - ); - return { wch: Math.min(Math.max(headerLength, maxDataLength) + 2, 50) }; - }); - ws["!cols"] = colWidths; + // ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ž๋™ ์กฐ์ • + const colWidths = exportColumns.map((col, idx) => { + const headerLength = headers[idx]?.length || 10; + const maxDataLength = Math.max(...rows.map((row) => String(row[idx] ?? "").length)); + return { wch: Math.min(Math.max(headerLength, maxDataLength) + 2, 50) }; + }); + ws["!cols"] = colWidths; - // ์›Œํฌ๋ถ ์ƒ์„ฑ - const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, tableLabel || "๋ฐ์ดํ„ฐ"); + // ์›Œํฌ๋ถ ์ƒ์„ฑ + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, tableLabel || "๋ฐ์ดํ„ฐ"); - // ํŒŒ์ผ๋ช… ์ƒ์„ฑ - const fileName = `${tableLabel || tableConfig.selectedTable || "export"}_${new Date().toISOString().split("T")[0]}.xlsx`; + // ํŒŒ์ผ๋ช… ์ƒ์„ฑ + const fileName = `${tableLabel || tableConfig.selectedTable || "export"}_${new Date().toISOString().split("T")[0]}.xlsx`; - // ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ - XLSX.writeFile(wb, fileName); + // ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ + XLSX.writeFile(wb, fileName); - toast.success(`${exportData.length}๊ฐœ ํ–‰์ด Excel๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); - console.log("โœ… Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์™„๋ฃŒ:", fileName); - } catch (error) { - console.error("โŒ Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ:", error); - toast.error("Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); - } - }, [filteredData, selectedRows, visibleColumns, columnLabels, joinColumnMapping, categoryMappings, tableLabel, tableConfig.selectedTable, getRowKey]); + toast.success(`${exportData.length}๊ฐœ ํ–‰์ด Excel๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + console.log("โœ… Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์™„๋ฃŒ:", fileName); + } catch (error) { + console.error("โŒ Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ:", error); + toast.error("Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + }, + [ + filteredData, + selectedRows, + visibleColumns, + columnLabels, + joinColumnMapping, + categoryMappings, + tableLabel, + tableConfig.selectedTable, + getRowKey, + ], + ); // ๐Ÿ†• ํ–‰ ํ™•์žฅ/์ถ•์†Œ ํ† ๊ธ€ - const toggleRowExpand = useCallback(async (rowKey: string, row: any) => { - setExpandedRows((prev) => { - const newSet = new Set(prev); - if (newSet.has(rowKey)) { - newSet.delete(rowKey); - } else { - newSet.add(rowKey); - // ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ (์•„์ง ์—†๋Š” ๊ฒฝ์šฐ) - if (!detailData[rowKey] && (tableConfig as any).masterDetail?.detailTable) { - loadDetailData(rowKey, row); + const toggleRowExpand = useCallback( + async (rowKey: string, row: any) => { + setExpandedRows((prev) => { + const newSet = new Set(prev); + if (newSet.has(rowKey)) { + newSet.delete(rowKey); + } else { + newSet.add(rowKey); + // ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ (์•„์ง ์—†๋Š” ๊ฒฝ์šฐ) + if (!detailData[rowKey] && (tableConfig as any).masterDetail?.detailTable) { + loadDetailData(rowKey, row); + } } - } - return newSet; - }); - }, [detailData, tableConfig]); + return newSet; + }); + }, + [detailData, tableConfig], + ); // ๐Ÿ†• ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ - const loadDetailData = useCallback(async (rowKey: string, row: any) => { - const masterDetailConfig = (tableConfig as any).masterDetail; - if (!masterDetailConfig?.detailTable) return; + const loadDetailData = useCallback( + async (rowKey: string, row: any) => { + const masterDetailConfig = (tableConfig as any).masterDetail; + if (!masterDetailConfig?.detailTable) return; - try { - const { apiClient } = await import("@/lib/api/client"); - - // masterKey ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ - const masterKeyField = masterDetailConfig.masterKey || "id"; - const masterKeyValue = row[masterKeyField]; - - // ์ƒ์„ธ ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ - const response = await apiClient.post(`/table-management/tables/${masterDetailConfig.detailTable}/data`, { - page: 1, - size: 100, - search: { - [masterDetailConfig.detailKey || masterKeyField]: masterKeyValue, - }, - autoFilter: true, - }); + try { + const { apiClient } = await import("@/lib/api/client"); - const details = response.data?.data?.data || []; - - setDetailData((prev) => ({ - ...prev, - [rowKey]: details, - })); - - console.log("โœ… ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์™„๋ฃŒ:", { rowKey, count: details.length }); - } catch (error) { - console.error("โŒ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ:", error); - setDetailData((prev) => ({ - ...prev, - [rowKey]: [], - })); - } - }, [tableConfig]); + // masterKey ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + const masterKeyField = masterDetailConfig.masterKey || "id"; + const masterKeyValue = row[masterKeyField]; + + // ์ƒ์„ธ ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ + const response = await apiClient.post(`/table-management/tables/${masterDetailConfig.detailTable}/data`, { + page: 1, + size: 100, + search: { + [masterDetailConfig.detailKey || masterKeyField]: masterKeyValue, + }, + autoFilter: true, + }); + + const details = response.data?.data?.data || []; + + setDetailData((prev) => ({ + ...prev, + [rowKey]: details, + })); + + console.log("โœ… ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์™„๋ฃŒ:", { rowKey, count: details.length }); + } catch (error) { + console.error("โŒ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ:", error); + setDetailData((prev) => ({ + ...prev, + [rowKey]: [], + })); + } + }, + [tableConfig], + ); // ๐Ÿ†• ๋ชจ๋“  ํ–‰ ํ™•์žฅ/์ถ•์†Œ const expandAllRows = useCallback(() => { @@ -2605,22 +2661,22 @@ export const TableListComponent: React.FC = ({ if (!bands || bands.length === 0) return null; // ๊ฐ band์˜ ์‹œ์ž‘ ์ธ๋ฑ์Šค์™€ colspan ๊ณ„์‚ฐ - const bandInfo = bands.map((band) => { - const visibleBandColumns = band.columns.filter((colName) => - visibleColumns.some((vc) => vc.columnName === colName) - ); - - const startIndex = visibleColumns.findIndex( - (vc) => visibleBandColumns.includes(vc.columnName) - ); + const bandInfo = bands + .map((band) => { + const visibleBandColumns = band.columns.filter((colName) => + visibleColumns.some((vc) => vc.columnName === colName), + ); - return { - caption: band.caption, - columns: visibleBandColumns, - colSpan: visibleBandColumns.length, - startIndex, - }; - }).filter((b) => b.colSpan > 0); + const startIndex = visibleColumns.findIndex((vc) => visibleBandColumns.includes(vc.columnName)); + + return { + caption: band.caption, + columns: visibleBandColumns, + colSpan: visibleBandColumns.length, + startIndex, + }; + }) + .filter((b) => b.colSpan > 0); // Band์— ํฌํ•จ๋˜์ง€ ์•Š์€ ์ปฌ๋Ÿผ ์ฐพ๊ธฐ const bandedColumns = new Set(bands.flatMap((b) => b.columns)); @@ -2636,109 +2692,84 @@ export const TableListComponent: React.FC = ({ }, [tableConfig, visibleColumns]); // ๐Ÿ†• Cascading Lookups: ์—ฐ๊ณ„ ๋“œ๋กญ๋‹ค์šด ์˜ต์…˜ ๋กœ๋”ฉ - const loadCascadingOptions = useCallback(async ( - columnName: string, - parentColumnName: string, - parentValue: any - ) => { - const cascadingConfig = (tableConfig as any).cascadingLookups?.[columnName]; - if (!cascadingConfig) return; + const loadCascadingOptions = useCallback( + async (columnName: string, parentColumnName: string, parentValue: any) => { + const cascadingConfig = (tableConfig as any).cascadingLookups?.[columnName]; + if (!cascadingConfig) return; - const cacheKey = `${columnName}_${parentValue}`; - - // ์ด๋ฏธ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์Šคํ‚ต - if (loadingCascading[cacheKey]) return; - - // ์ด๋ฏธ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์Šคํ‚ต - if (cascadingOptions[cacheKey]) return; + const cacheKey = `${columnName}_${parentValue}`; - setLoadingCascading((prev) => ({ ...prev, [cacheKey]: true })); + // ์ด๋ฏธ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์Šคํ‚ต + if (loadingCascading[cacheKey]) return; - try { - const { apiClient } = await import("@/lib/api/client"); - - // API์—์„œ ์—ฐ๊ณ„ ์˜ต์…˜ ๋กœ๋”ฉ - const response = await apiClient.post(`/table-management/tables/${cascadingConfig.sourceTable}/data`, { - page: 1, - size: 1000, - search: { - [cascadingConfig.parentKeyField || parentColumnName]: parentValue, - }, - autoFilter: true, - }); + // ์ด๋ฏธ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์Šคํ‚ต + if (cascadingOptions[cacheKey]) return; - const items = response.data?.data?.data || []; - const options = items.map((item: any) => ({ - value: item[cascadingConfig.valueField || "id"], - label: item[cascadingConfig.labelField || "name"], - })); + setLoadingCascading((prev) => ({ ...prev, [cacheKey]: true })); - setCascadingOptions((prev) => ({ - ...prev, - [cacheKey]: options, - })); + try { + const { apiClient } = await import("@/lib/api/client"); - console.log("โœ… Cascading options ๋กœ๋”ฉ ์™„๋ฃŒ:", { columnName, parentValue, count: options.length }); - } catch (error) { - console.error("โŒ Cascading options ๋กœ๋”ฉ ์‹คํŒจ:", error); - setCascadingOptions((prev) => ({ - ...prev, - [cacheKey]: [], - })); - } finally { - setLoadingCascading((prev) => ({ ...prev, [cacheKey]: false })); - } - }, [tableConfig, cascadingOptions, loadingCascading]); + // API์—์„œ ์—ฐ๊ณ„ ์˜ต์…˜ ๋กœ๋”ฉ + const response = await apiClient.post(`/table-management/tables/${cascadingConfig.sourceTable}/data`, { + page: 1, + size: 1000, + search: { + [cascadingConfig.parentKeyField || parentColumnName]: parentValue, + }, + autoFilter: true, + }); + + const items = response.data?.data?.data || []; + const options = items.map((item: any) => ({ + value: item[cascadingConfig.valueField || "id"], + label: item[cascadingConfig.labelField || "name"], + })); + + setCascadingOptions((prev) => ({ + ...prev, + [cacheKey]: options, + })); + + console.log("โœ… Cascading options ๋กœ๋”ฉ ์™„๋ฃŒ:", { columnName, parentValue, count: options.length }); + } catch (error) { + console.error("โŒ Cascading options ๋กœ๋”ฉ ์‹คํŒจ:", error); + setCascadingOptions((prev) => ({ + ...prev, + [cacheKey]: [], + })); + } finally { + setLoadingCascading((prev) => ({ ...prev, [cacheKey]: false })); + } + }, + [tableConfig, cascadingOptions, loadingCascading], + ); // ๐Ÿ†• Cascading Lookups: ํŠน์ • ์ปฌ๋Ÿผ์˜ ์˜ต์…˜ ๊ฐ€์ ธ์˜ค๊ธฐ - const getCascadingOptions = useCallback((columnName: string, row: any): { value: string; label: string }[] => { - const cascadingConfig = (tableConfig as any).cascadingLookups?.[columnName]; - if (!cascadingConfig) return []; + const getCascadingOptions = useCallback( + (columnName: string, row: any): { value: string; label: string }[] => { + const cascadingConfig = (tableConfig as any).cascadingLookups?.[columnName]; + if (!cascadingConfig) return []; - const parentValue = row[cascadingConfig.parentColumn]; - if (parentValue === undefined || parentValue === null) return []; + const parentValue = row[cascadingConfig.parentColumn]; + if (parentValue === undefined || parentValue === null) return []; - const cacheKey = `${columnName}_${parentValue}`; - return cascadingOptions[cacheKey] || []; - }, [tableConfig, cascadingOptions]); + const cacheKey = `${columnName}_${parentValue}`; + return cascadingOptions[cacheKey] || []; + }, + [tableConfig, cascadingOptions], + ); - // ๐Ÿ†• Virtual Scrolling: ๋ณด์ด๋Š” ํ–‰ ๋ฒ”์œ„ ๊ณ„์‚ฐ - const virtualScrollInfo = useMemo(() => { - if (!isVirtualScrollEnabled || filteredData.length === 0) { - return { - startIndex: 0, - endIndex: filteredData.length, - visibleData: filteredData, - topSpacerHeight: 0, - bottomSpacerHeight: 0, - totalHeight: filteredData.length * ROW_HEIGHT, - }; - } - - const containerHeight = scrollContainerRef.current?.clientHeight || 600; - const totalRows = filteredData.length; - const totalHeight = totalRows * ROW_HEIGHT; - - // ํ˜„์žฌ ๋ณด์ด๋Š” ํ–‰ ๋ฒ”์œ„ ๊ณ„์‚ฐ - const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN); - const visibleRowCount = Math.ceil(containerHeight / ROW_HEIGHT) + OVERSCAN * 2; - const endIndex = Math.min(totalRows, startIndex + visibleRowCount); - - return { - startIndex, - endIndex, - visibleData: filteredData.slice(startIndex, endIndex), - topSpacerHeight: startIndex * ROW_HEIGHT, - bottomSpacerHeight: (totalRows - endIndex) * ROW_HEIGHT, - totalHeight, - }; - }, [isVirtualScrollEnabled, filteredData, scrollTop, ROW_HEIGHT, OVERSCAN]); + // ๐Ÿ†• Virtual Scrolling: virtualScrollInfo๋Š” displayData ์ •์˜ ์ดํ›„๋กœ ์ด๋™๋จ (์•„๋ž˜ ์ฐธ์กฐ) // ๐Ÿ†• Virtual Scrolling: ์Šคํฌ๋กค ํ•ธ๋“ค๋Ÿฌ - const handleVirtualScroll = useCallback((e: React.UIEvent) => { - if (!isVirtualScrollEnabled) return; - setScrollTop(e.currentTarget.scrollTop); - }, [isVirtualScrollEnabled]); + const handleVirtualScroll = useCallback( + (e: React.UIEvent) => { + if (!isVirtualScrollEnabled) return; + setScrollTop(e.currentTarget.scrollTop); + }, + [isVirtualScrollEnabled], + ); // ๐Ÿ†• State Persistence: ํ†ตํ•ฉ ์ƒํƒœ ์ €์žฅ const saveTableState = useCallback(() => { @@ -2753,7 +2784,7 @@ export const TableListComponent: React.FC = ({ frozenColumns, showGridLines, headerFilters: Object.fromEntries( - Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set)]) + Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set)]), ), pageSize: localPageSize, timestamp: Date.now(), @@ -2765,7 +2796,18 @@ export const TableListComponent: React.FC = ({ } catch (error) { console.error("โŒ ํ…Œ์ด๋ธ” ์ƒํƒœ ์ €์žฅ ์‹คํŒจ:", error); } - }, [tableStateKey, columnWidths, columnOrder, sortColumn, sortDirection, groupByColumns, frozenColumns, showGridLines, headerFilters, localPageSize]); + }, [ + tableStateKey, + columnWidths, + columnOrder, + sortColumn, + sortDirection, + groupByColumns, + frozenColumns, + showGridLines, + headerFilters, + localPageSize, + ]); // ๐Ÿ†• State Persistence: ํ†ตํ•ฉ ์ƒํƒœ ๋ณต์› const loadTableState = useCallback(() => { @@ -2776,7 +2818,7 @@ export const TableListComponent: React.FC = ({ if (!saved) return; const state = JSON.parse(saved); - + if (state.columnWidths) setColumnWidths(state.columnWidths); if (state.columnOrder) setColumnOrder(state.columnOrder); if (state.sortColumn !== undefined) setSortColumn(state.sortColumn); @@ -2828,7 +2870,8 @@ export const TableListComponent: React.FC = ({ const connectWebSocket = useCallback(() => { if (!isRealTimeEnabled || !tableConfig.selectedTable) return; - const wsUrl = (tableConfig as any).wsUrl || + const wsUrl = + (tableConfig as any).wsUrl || `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws/table/${tableConfig.selectedTable}`; try { @@ -2876,7 +2919,7 @@ export const TableListComponent: React.FC = ({ wsRef.current.onclose = () => { setWsConnectionStatus("disconnected"); console.log("๐Ÿ”Œ WebSocket ์—ฐ๊ฒฐ ์ข…๋ฃŒ"); - + // ์ž๋™ ์žฌ์—ฐ๊ฒฐ (5์ดˆ ํ›„) if (isRealTimeEnabled) { reconnectTimeoutRef.current = setTimeout(() => { @@ -2921,14 +2964,23 @@ export const TableListComponent: React.FC = ({ }, 1000); // 1์ดˆ ํ›„ ์ €์žฅ (๋””๋ฐ”์šด์Šค) return () => clearTimeout(timeoutId); - }, [columnWidths, columnOrder, sortColumn, sortDirection, groupByColumns, frozenColumns, showGridLines, headerFilters]); + }, [ + columnWidths, + columnOrder, + sortColumn, + sortDirection, + groupByColumns, + frozenColumns, + showGridLines, + headerFilters, + ]); // ๐Ÿ†• Clipboard: ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ const handleCopy = useCallback(async () => { try { // ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ let copyData: any[]; - + if (selectedRows.size > 0) { // ์„ ํƒ๋œ ํ–‰๋งŒ copyData = filteredData.filter((row, index) => { @@ -2955,15 +3007,17 @@ export const TableListComponent: React.FC = ({ const exportColumns = visibleColumns.filter((c) => c.columnName !== "__checkbox__"); const headers = exportColumns.map((c) => columnLabels[c.columnName] || c.columnName); const rows = copyData.map((row) => - exportColumns.map((c) => { - const value = row[c.columnName]; - return value !== null && value !== undefined ? String(value).replace(/\t/g, " ").replace(/\n/g, " ") : ""; - }).join("\t") + exportColumns + .map((c) => { + const value = row[c.columnName]; + return value !== null && value !== undefined ? String(value).replace(/\t/g, " ").replace(/\n/g, " ") : ""; + }) + .join("\t"), ); const tsvContent = [headers.join("\t"), ...rows].join("\n"); await navigator.clipboard.writeText(tsvContent); - + toast.success(`${copyData.length}ํ–‰ ๋ณต์‚ฌ๋จ`); console.log("โœ… ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ:", copyData.length, "ํ–‰"); } catch (error) { @@ -3013,39 +3067,42 @@ export const TableListComponent: React.FC = ({ }, [contextMenu, closeContextMenu]); // ๐Ÿ†• Search Panel: ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ์‹คํ–‰ - const executeGlobalSearch = useCallback((term: string) => { - if (!term.trim()) { - setSearchHighlights(new Set()); - return; - } + const executeGlobalSearch = useCallback( + (term: string) => { + if (!term.trim()) { + setSearchHighlights(new Set()); + return; + } - const lowerTerm = term.toLowerCase(); - const highlights = new Set(); + const lowerTerm = term.toLowerCase(); + const highlights = new Set(); - filteredData.forEach((row, rowIndex) => { - visibleColumns.forEach((col, colIndex) => { - const value = row[col.columnName]; - if (value !== null && value !== undefined) { - const strValue = String(value).toLowerCase(); - if (strValue.includes(lowerTerm)) { - highlights.add(`${rowIndex}-${colIndex}`); + filteredData.forEach((row, rowIndex) => { + visibleColumns.forEach((col, colIndex) => { + const value = row[col.columnName]; + if (value !== null && value !== undefined) { + const strValue = String(value).toLowerCase(); + if (strValue.includes(lowerTerm)) { + highlights.add(`${rowIndex}-${colIndex}`); + } } - } + }); }); - }); - setSearchHighlights(highlights); - - // ์ฒซ ๋ฒˆ์งธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋กœ ํฌ์ปค์Šค ์ด๋™ - if (highlights.size > 0) { - const firstHighlight = Array.from(highlights)[0]; - const [rowIdx, colIdx] = firstHighlight.split("-").map(Number); - setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx }); - toast.success(`${highlights.size}๊ฐœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ`); - } else { - toast.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); - } - }, [filteredData, visibleColumns]); + setSearchHighlights(highlights); + + // ์ฒซ ๋ฒˆ์งธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋กœ ํฌ์ปค์Šค ์ด๋™ + if (highlights.size > 0) { + const firstHighlight = Array.from(highlights)[0]; + const [rowIdx, colIdx] = firstHighlight.split("-").map(Number); + setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx }); + toast.success(`${highlights.size}๊ฐœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ`); + } else { + toast.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); + } + }, + [filteredData, visibleColumns], + ); // ๐Ÿ†• Search Panel: ๋‹ค์Œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋กœ ์ด๋™ const goToNextSearchResult = useCallback(() => { @@ -3120,8 +3177,8 @@ export const TableListComponent: React.FC = ({ }, ], } - : group - ) + : group, + ), ); }, []); @@ -3134,8 +3191,8 @@ export const TableListComponent: React.FC = ({ ...group, conditions: group.conditions.filter((c) => c.id !== conditionId), } - : group - ) + : group, + ), ); }, []); @@ -3147,15 +3204,13 @@ export const TableListComponent: React.FC = ({ group.id === groupId ? { ...group, - conditions: group.conditions.map((c) => - c.id === conditionId ? { ...c, [field]: value } : c - ), + conditions: group.conditions.map((c) => (c.id === conditionId ? { ...c, [field]: value } : c)), } - : group - ) + : group, + ), ); }, - [] + [], ); // ๐Ÿ†• Filter Builder: ๊ทธ๋ฃน ์ถ”๊ฐ€ @@ -3184,9 +3239,7 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• Filter Builder: ๊ทธ๋ฃน ๋กœ์ง ๋ณ€๊ฒฝ const updateGroupLogic = useCallback((groupId: string, logic: "AND" | "OR") => { - setFilterGroups((prev) => - prev.map((group) => (group.id === groupId ? { ...group, logic } : group)) - ); + setFilterGroups((prev) => prev.map((group) => (group.id === groupId ? { ...group, logic } : group))); }, []); // ๐Ÿ†• Filter Builder: ํ•„ํ„ฐ ์ ์šฉ @@ -3255,7 +3308,7 @@ export const TableListComponent: React.FC = ({ // ๋ชจ๋“  ๊ทธ๋ฃน์ด AND๋กœ ์—ฐ๊ฒฐ๋จ (๊ทธ๋ฃน ๊ฐ„) return filterGroups.every((group) => { const validConditions = group.conditions.filter( - (c) => c.column && (c.operator === "isEmpty" || c.operator === "isNotEmpty" || c.value) + (c) => c.column && (c.operator === "isEmpty" || c.operator === "isNotEmpty" || c.value), ); if (validConditions.length === 0) return true; @@ -3266,29 +3319,35 @@ export const TableListComponent: React.FC = ({ } }); }, - [filterGroups, evaluateCondition] + [filterGroups, evaluateCondition], ); // ๐Ÿ†• ์ปฌ๋Ÿผ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ - const handleColumnDragStart = useCallback((e: React.DragEvent, index: number) => { - if (!isColumnDragEnabled) return; - - setDraggedColumnIndex(index); - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/plain", `col-${index}`); - }, [isColumnDragEnabled]); + const handleColumnDragStart = useCallback( + (e: React.DragEvent, index: number) => { + if (!isColumnDragEnabled) return; + + setDraggedColumnIndex(index); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", `col-${index}`); + }, + [isColumnDragEnabled], + ); // ๐Ÿ†• ์ปฌ๋Ÿผ ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„ - const handleColumnDragOver = useCallback((e: React.DragEvent, index: number) => { - if (!isColumnDragEnabled || draggedColumnIndex === null) return; - - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - - if (index !== draggedColumnIndex) { - setDropTargetColumnIndex(index); - } - }, [isColumnDragEnabled, draggedColumnIndex]); + const handleColumnDragOver = useCallback( + (e: React.DragEvent, index: number) => { + if (!isColumnDragEnabled || draggedColumnIndex === null) return; + + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + + if (index !== draggedColumnIndex) { + setDropTargetColumnIndex(index); + } + }, + [isColumnDragEnabled, draggedColumnIndex], + ); // ๐Ÿ†• ์ปฌ๋Ÿผ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ const handleColumnDragEnd = useCallback(() => { @@ -3297,55 +3356,64 @@ export const TableListComponent: React.FC = ({ }, []); // ๐Ÿ†• ์ปฌ๋Ÿผ ๋“œ๋กญ - const handleColumnDrop = useCallback((e: React.DragEvent, targetIndex: number) => { - e.preventDefault(); - - if (!isColumnDragEnabled || draggedColumnIndex === null || draggedColumnIndex === targetIndex) { + const handleColumnDrop = useCallback( + (e: React.DragEvent, targetIndex: number) => { + e.preventDefault(); + + if (!isColumnDragEnabled || draggedColumnIndex === null || draggedColumnIndex === targetIndex) { + handleColumnDragEnd(); + return; + } + + // ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ + const newOrder = [...(columnOrder.length > 0 ? columnOrder : visibleColumns.map((c) => c.columnName))]; + const [movedColumn] = newOrder.splice(draggedColumnIndex, 1); + newOrder.splice(targetIndex, 0, movedColumn); + + setColumnOrder(newOrder); + toast.info("์ปฌ๋Ÿผ ์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + console.log("โœ… ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ:", { from: draggedColumnIndex, to: targetIndex }); + handleColumnDragEnd(); - return; - } - - // ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ - const newOrder = [...(columnOrder.length > 0 ? columnOrder : visibleColumns.map((c) => c.columnName))]; - const [movedColumn] = newOrder.splice(draggedColumnIndex, 1); - newOrder.splice(targetIndex, 0, movedColumn); - - setColumnOrder(newOrder); - toast.info("์ปฌ๋Ÿผ ์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); - console.log("โœ… ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ:", { from: draggedColumnIndex, to: targetIndex }); - - handleColumnDragEnd(); - }, [isColumnDragEnabled, draggedColumnIndex, columnOrder, visibleColumns, handleColumnDragEnd]); + }, + [isColumnDragEnabled, draggedColumnIndex, columnOrder, visibleColumns, handleColumnDragEnd], + ); // ๐Ÿ†• ํ–‰ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ - const handleRowDragStart = useCallback((e: React.DragEvent, index: number) => { - if (!isDragEnabled) return; - - setDraggedRowIndex(index); - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/plain", String(index)); - - // ๋“œ๋ž˜๊ทธ ์ด๋ฏธ์ง€ ์„ค์ • (๋ฐ˜ํˆฌ๋ช…) - const dragImage = e.currentTarget.cloneNode(true) as HTMLElement; - dragImage.style.opacity = "0.5"; - dragImage.style.position = "absolute"; - dragImage.style.top = "-1000px"; - document.body.appendChild(dragImage); - e.dataTransfer.setDragImage(dragImage, 0, 0); - setTimeout(() => document.body.removeChild(dragImage), 0); - }, [isDragEnabled]); + const handleRowDragStart = useCallback( + (e: React.DragEvent, index: number) => { + if (!isDragEnabled) return; + + setDraggedRowIndex(index); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", String(index)); + + // ๋“œ๋ž˜๊ทธ ์ด๋ฏธ์ง€ ์„ค์ • (๋ฐ˜ํˆฌ๋ช…) + const dragImage = e.currentTarget.cloneNode(true) as HTMLElement; + dragImage.style.opacity = "0.5"; + dragImage.style.position = "absolute"; + dragImage.style.top = "-1000px"; + document.body.appendChild(dragImage); + e.dataTransfer.setDragImage(dragImage, 0, 0); + setTimeout(() => document.body.removeChild(dragImage), 0); + }, + [isDragEnabled], + ); // ๐Ÿ†• ํ–‰ ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„ - const handleRowDragOver = useCallback((e: React.DragEvent, index: number) => { - if (!isDragEnabled || draggedRowIndex === null) return; - - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - - if (index !== draggedRowIndex) { - setDropTargetIndex(index); - } - }, [isDragEnabled, draggedRowIndex]); + const handleRowDragOver = useCallback( + (e: React.DragEvent, index: number) => { + if (!isDragEnabled || draggedRowIndex === null) return; + + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + + if (index !== draggedRowIndex) { + setDropTargetIndex(index); + } + }, + [isDragEnabled, draggedRowIndex], + ); // ๐Ÿ†• ํ–‰ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ const handleRowDragEnd = useCallback(() => { @@ -3354,84 +3422,84 @@ export const TableListComponent: React.FC = ({ }, []); // ๐Ÿ†• ํ–‰ ๋“œ๋กญ - const handleRowDrop = useCallback(async (e: React.DragEvent, targetIndex: number) => { - e.preventDefault(); - - if (!isDragEnabled || draggedRowIndex === null || draggedRowIndex === targetIndex) { - handleRowDragEnd(); - return; - } + const handleRowDrop = useCallback( + async (e: React.DragEvent, targetIndex: number) => { + e.preventDefault(); - try { - // ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์žฌ์ •๋ ฌ - const newData = [...filteredData]; - const [movedRow] = newData.splice(draggedRowIndex, 1); - newData.splice(targetIndex, 0, movedRow); - - // ์„œ๋ฒ„์— ์ˆœ์„œ ์ €์žฅ (order_index ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) - const orderField = (tableConfig as any).orderField || "order_index"; - const hasOrderField = newData[0] && orderField in newData[0]; - - if (hasOrderField && tableConfig.selectedTable) { - const { apiClient } = await import("@/lib/api/client"); - const primaryKeyField = tableConfig.primaryKey || "id"; - - // ์˜ํ–ฅ๋ฐ›๋Š” ํ–‰๋“ค์˜ ์ˆœ์„œ ์—…๋ฐ์ดํŠธ - const updates = newData.map((row, idx) => ({ - tableName: tableConfig.selectedTable, - keyField: primaryKeyField, - keyValue: row[primaryKeyField], - updateField: orderField, - updateValue: idx + 1, - })); - - // ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ - await Promise.all( - updates.map((update) => - apiClient.put(`/dynamic-form/update-field`, update) - ) - ); - - toast.success("์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); - setRefreshTrigger((prev) => prev + 1); - } else { - // ๋กœ์ปฌ์—์„œ๋งŒ ์ˆœ์„œ ๋ณ€๊ฒฝ (์ €์žฅ ์•ˆํ•จ) - toast.info("์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (๋กœ์ปฌ๋งŒ)"); - } - - console.log("โœ… ํ–‰ ์ˆœ์„œ ๋ณ€๊ฒฝ:", { from: draggedRowIndex, to: targetIndex }); - } catch (error) { - console.error("โŒ ํ–‰ ์ˆœ์„œ ๋ณ€๊ฒฝ ์‹คํŒจ:", error); - toast.error("์ˆœ์„œ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); - } - - handleRowDragEnd(); - }, [isDragEnabled, draggedRowIndex, filteredData, tableConfig, handleRowDragEnd]); - - // ๐Ÿ†• PDF ๋‚ด๋ณด๋‚ด๊ธฐ (์ธ์‡„์šฉ HTML ์ƒ์„ฑ) - const exportToPdf = useCallback((exportAll: boolean = true) => { - try { - // ๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ์„ ํƒ - let exportData: any[]; - if (exportAll) { - exportData = filteredData; - } else { - exportData = filteredData.filter((row, index) => { - const rowKey = getRowKey(row, index); - return selectedRows.has(rowKey); - }); - } - - if (exportData.length === 0) { - toast.error(exportAll ? "๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." : "์„ ํƒ๋œ ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค."); + if (!isDragEnabled || draggedRowIndex === null || draggedRowIndex === targetIndex) { + handleRowDragEnd(); return; } - // ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ) - const exportColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__"); - - // ์ธ์‡„์šฉ HTML ์ƒ์„ฑ - const printContent = ` + try { + // ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์žฌ์ •๋ ฌ + const newData = [...filteredData]; + const [movedRow] = newData.splice(draggedRowIndex, 1); + newData.splice(targetIndex, 0, movedRow); + + // ์„œ๋ฒ„์— ์ˆœ์„œ ์ €์žฅ (order_index ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) + const orderField = (tableConfig as any).orderField || "order_index"; + const hasOrderField = newData[0] && orderField in newData[0]; + + if (hasOrderField && tableConfig.selectedTable) { + const { apiClient } = await import("@/lib/api/client"); + const primaryKeyField = tableConfig.primaryKey || "id"; + + // ์˜ํ–ฅ๋ฐ›๋Š” ํ–‰๋“ค์˜ ์ˆœ์„œ ์—…๋ฐ์ดํŠธ + const updates = newData.map((row, idx) => ({ + tableName: tableConfig.selectedTable, + keyField: primaryKeyField, + keyValue: row[primaryKeyField], + updateField: orderField, + updateValue: idx + 1, + })); + + // ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ + await Promise.all(updates.map((update) => apiClient.put(`/dynamic-form/update-field`, update))); + + toast.success("์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + setRefreshTrigger((prev) => prev + 1); + } else { + // ๋กœ์ปฌ์—์„œ๋งŒ ์ˆœ์„œ ๋ณ€๊ฒฝ (์ €์žฅ ์•ˆํ•จ) + toast.info("์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (๋กœ์ปฌ๋งŒ)"); + } + + console.log("โœ… ํ–‰ ์ˆœ์„œ ๋ณ€๊ฒฝ:", { from: draggedRowIndex, to: targetIndex }); + } catch (error) { + console.error("โŒ ํ–‰ ์ˆœ์„œ ๋ณ€๊ฒฝ ์‹คํŒจ:", error); + toast.error("์ˆœ์„œ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + + handleRowDragEnd(); + }, + [isDragEnabled, draggedRowIndex, filteredData, tableConfig, handleRowDragEnd], + ); + + // ๐Ÿ†• PDF ๋‚ด๋ณด๋‚ด๊ธฐ (์ธ์‡„์šฉ HTML ์ƒ์„ฑ) + const exportToPdf = useCallback( + (exportAll: boolean = true) => { + try { + // ๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ์„ ํƒ + let exportData: any[]; + if (exportAll) { + exportData = filteredData; + } else { + exportData = filteredData.filter((row, index) => { + const rowKey = getRowKey(row, index); + return selectedRows.has(rowKey); + }); + } + + if (exportData.length === 0) { + toast.error(exportAll ? "๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." : "์„ ํƒ๋œ ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + + // ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ) + const exportColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__"); + + // ์ธ์‡„์šฉ HTML ์ƒ์„ฑ + const printContent = ` @@ -3467,68 +3535,90 @@ export const TableListComponent: React.FC = ({ - ${exportData.map((row) => ` + ${exportData + .map( + (row) => ` - ${exportColumns.map((col) => { - const mappedColumnName = joinColumnMapping[col.columnName] || col.columnName; - let value = row[mappedColumnName]; - - // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ - if (categoryMappings[col.columnName] && value !== null && value !== undefined) { - const mapping = categoryMappings[col.columnName][String(value)]; - if (mapping) value = mapping.label; - } - - const meta = columnMeta[col.columnName]; - const inputType = meta?.inputType || (col as any).inputType; - const isNumeric = inputType === "number" || inputType === "decimal"; - - return `${value ?? ""}`; - }).join("")} + ${exportColumns + .map((col) => { + const mappedColumnName = joinColumnMapping[col.columnName] || col.columnName; + let value = row[mappedColumnName]; + + // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ + if (categoryMappings[col.columnName] && value !== null && value !== undefined) { + const mapping = categoryMappings[col.columnName][String(value)]; + if (mapping) value = mapping.label; + } + + const meta = columnMeta[col.columnName]; + const inputType = meta?.inputType || (col as any).inputType; + const isNumeric = inputType === "number" || inputType === "decimal"; + + return `${value ?? ""}`; + }) + .join("")} - `).join("")} + `, + ) + .join("")} `; - // ์ƒˆ ์ฐฝ์—์„œ ์ธ์‡„ - const printWindow = window.open("", "_blank"); - if (printWindow) { - printWindow.document.write(printContent); - printWindow.document.close(); - printWindow.onload = () => { - printWindow.print(); - }; - toast.success("์ธ์‡„ ์ฐฝ์ด ์—ด๋ ธ์Šต๋‹ˆ๋‹ค."); - } else { - toast.error("ํŒ์—…์ด ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํŒ์—…์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”."); + // ์ƒˆ ์ฐฝ์—์„œ ์ธ์‡„ + const printWindow = window.open("", "_blank"); + if (printWindow) { + printWindow.document.write(printContent); + printWindow.document.close(); + printWindow.onload = () => { + printWindow.print(); + }; + toast.success("์ธ์‡„ ์ฐฝ์ด ์—ด๋ ธ์Šต๋‹ˆ๋‹ค."); + } else { + toast.error("ํŒ์—…์ด ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํŒ์—…์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”."); + } + } catch (error) { + console.error("โŒ PDF ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ:", error); + toast.error("PDF ๋‚ด๋ณด๋‚ด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } - } catch (error) { - console.error("โŒ PDF ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ:", error); - toast.error("PDF ๋‚ด๋ณด๋‚ด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); - } - }, [filteredData, selectedRows, visibleColumns, columnLabels, joinColumnMapping, categoryMappings, columnMeta, tableLabel, tableConfig.selectedTable, getRowKey]); + }, + [ + filteredData, + selectedRows, + visibleColumns, + columnLabels, + joinColumnMapping, + categoryMappings, + columnMeta, + tableLabel, + tableConfig.selectedTable, + getRowKey, + ], + ); // ๐Ÿ†• ํŽธ์ง‘ ์ค‘ ํ‚ค๋ณด๋“œ ํ•ธ๋“ค๋Ÿฌ (๊ฐ„๋‹จ ๋ฒ„์ „ - Tab ์ด๋™์€ visibleColumns ์ •์˜ ํ›„ ์ฒ˜๋ฆฌ) - const handleEditKeyDown = useCallback((e: React.KeyboardEvent) => { - switch (e.key) { - case "Enter": - e.preventDefault(); - saveEditing(); - break; - case "Escape": - e.preventDefault(); - cancelEditing(); - break; - case "Tab": - e.preventDefault(); - saveEditing(); - // Tab ์ด๋™์€ ํŽธ์ง‘ ์ €์žฅ ํ›„ ํ…Œ์ด๋ธ” ํ‚ค๋ณด๋“œ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌ - break; - } - }, [saveEditing, cancelEditing]); + const handleEditKeyDown = useCallback( + (e: React.KeyboardEvent) => { + switch (e.key) { + case "Enter": + e.preventDefault(); + saveEditing(); + break; + case "Escape": + e.preventDefault(); + cancelEditing(); + break; + case "Tab": + e.preventDefault(); + saveEditing(); + // Tab ์ด๋™์€ ํŽธ์ง‘ ์ €์žฅ ํ›„ ํ…Œ์ด๋ธ” ํ‚ค๋ณด๋“œ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌ + break; + } + }, + [saveEditing, cancelEditing], + ); // ๐Ÿ†• ํŽธ์ง‘ ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ๋‚˜ํƒ€๋‚˜๋ฉด ์ž๋™ ํฌ์ปค์Šค useEffect(() => { @@ -3545,9 +3635,9 @@ export const TableListComponent: React.FC = ({ useEffect(() => { if (focusedCell && tableContainerRef.current) { const focusedCellElement = tableContainerRef.current.querySelector( - `[data-row="${focusedCell.rowIndex}"][data-col="${focusedCell.colIndex}"]` + `[data-row="${focusedCell.rowIndex}"][data-col="${focusedCell.colIndex}"]`, ) as HTMLElement; - + if (focusedCellElement) { focusedCellElement.scrollIntoView({ block: "nearest", inline: "nearest" }); } @@ -3636,239 +3726,242 @@ export const TableListComponent: React.FC = ({ }, [visibleColumns.length, visibleColumns.map((c) => c.columnName).join(",")]); // ์˜์กด์„ฑ ๋‹จ์ˆœํ™” // ๐Ÿ†• ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ํ•ธ๋“ค๋Ÿฌ (visibleColumns ์ •์˜ ํ›„์— ๋ฐฐ์น˜) - const handleTableKeyDown = useCallback((e: React.KeyboardEvent) => { - // ํŽธ์ง‘ ์ค‘์ผ ๋•Œ๋Š” ํ…Œ์ด๋ธ” ํ‚ค๋ณด๋“œ ํ•ธ๋“ค๋Ÿฌ ๋ฌด์‹œ (ํŽธ์ง‘ ์ž…๋ ฅ์—์„œ ์ฒ˜๋ฆฌ) - if (editingCell) return; - - if (!focusedCell || data.length === 0) return; + const handleTableKeyDown = useCallback( + (e: React.KeyboardEvent) => { + // ํŽธ์ง‘ ์ค‘์ผ ๋•Œ๋Š” ํ…Œ์ด๋ธ” ํ‚ค๋ณด๋“œ ํ•ธ๋“ค๋Ÿฌ ๋ฌด์‹œ (ํŽธ์ง‘ ์ž…๋ ฅ์—์„œ ์ฒ˜๋ฆฌ) + if (editingCell) return; - const { rowIndex, colIndex } = focusedCell; - const maxRowIndex = data.length - 1; - const maxColIndex = visibleColumns.length - 1; + if (!focusedCell || data.length === 0) return; - switch (e.key) { - case "ArrowUp": - e.preventDefault(); - if (rowIndex > 0) { - setFocusedCell({ rowIndex: rowIndex - 1, colIndex }); - } - break; - case "ArrowDown": - e.preventDefault(); - if (rowIndex < maxRowIndex) { - setFocusedCell({ rowIndex: rowIndex + 1, colIndex }); - } - break; - case "ArrowLeft": - e.preventDefault(); - if (colIndex > 0) { - setFocusedCell({ rowIndex, colIndex: colIndex - 1 }); - } - break; - case "ArrowRight": - e.preventDefault(); - if (colIndex < maxColIndex) { - setFocusedCell({ rowIndex, colIndex: colIndex + 1 }); - } - break; - case "Enter": - e.preventDefault(); - // ํ˜„์žฌ ํ–‰ ์„ ํƒ/ํ•ด์ œ - const enterRow = data[rowIndex]; - if (enterRow) { - const rowKey = getRowKey(enterRow, rowIndex); - const isCurrentlySelected = selectedRows.has(rowKey); - handleRowSelection(rowKey, !isCurrentlySelected); - } - break; - case " ": // Space - e.preventDefault(); - // ์ฒดํฌ๋ฐ•์Šค ํ† ๊ธ€ - const spaceRow = data[rowIndex]; - if (spaceRow) { - const currentRowKey = getRowKey(spaceRow, rowIndex); - const isChecked = selectedRows.has(currentRowKey); - handleRowSelection(currentRowKey, !isChecked); - } - break; - case "F2": - // ๐Ÿ†• F2: ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… - e.preventDefault(); - { - const col = visibleColumns[colIndex]; - if (col && col.columnName !== "__checkbox__") { - // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ - if (col.editable === false) { - toast.warning(`'${col.displayName || col.columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); - break; - } - const row = data[rowIndex]; - const mappedCol = joinColumnMapping[col.columnName] || col.columnName; - const val = row?.[mappedCol]; - setEditingCell({ - rowIndex, - colIndex, - columnName: col.columnName, - originalValue: val - }); - setEditingValue(val !== null && val !== undefined ? String(val) : ""); + const { rowIndex, colIndex } = focusedCell; + const maxRowIndex = data.length - 1; + const maxColIndex = visibleColumns.length - 1; + + switch (e.key) { + case "ArrowUp": + e.preventDefault(); + if (rowIndex > 0) { + setFocusedCell({ rowIndex: rowIndex - 1, colIndex }); } - } - break; - case "b": - case "B": - // ๐Ÿ†• Ctrl+B: ๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ํ† ๊ธ€ - if (e.ctrlKey) { + break; + case "ArrowDown": e.preventDefault(); - setEditMode((prev) => { - const newMode = prev === "immediate" ? "batch" : "immediate"; - if (newMode === "immediate" && pendingChanges.size > 0) { - // ์ฆ‰์‹œ ๋ชจ๋“œ๋กœ ์ „ํ™˜ ์‹œ ์ €์žฅ๋˜์ง€ ์•Š์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ ๊ฒฝ๊ณ  - const confirmDiscard = window.confirm( - `์ €์žฅ๋˜์ง€ ์•Š์€ ${pendingChanges.size}๊ฐœ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ทจ์†Œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?` - ); - if (confirmDiscard) { - setPendingChanges(new Map()); - setLocalEditedData({}); - toast.info("๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ์ข…๋ฃŒ"); - return "immediate"; - } - return "batch"; - } - toast.info(newMode === "batch" ? "๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ์‹œ์ž‘ (Ctrl+B๋กœ ์ข…๋ฃŒ)" : "์ฆ‰์‹œ ์ €์žฅ ๋ชจ๋“œ"); - return newMode; - }); - } - break; - case "s": - case "S": - // ๐Ÿ†• Ctrl+S: ๋ฐฐ์น˜ ์ €์žฅ - if (e.ctrlKey && editMode === "batch") { + if (rowIndex < maxRowIndex) { + setFocusedCell({ rowIndex: rowIndex + 1, colIndex }); + } + break; + case "ArrowLeft": e.preventDefault(); - saveBatchChanges(); - } - break; - case "c": - case "C": - // ๐Ÿ†• Ctrl+C: ์„ ํƒ๋œ ํ–‰/์…€ ๋ณต์‚ฌ - if (e.ctrlKey) { - e.preventDefault(); - handleCopy(); - } - break; - case "v": - case "V": - // ๐Ÿ†• Ctrl+V: ๋ถ™์—ฌ๋„ฃ๊ธฐ (ํŽธ์ง‘ ์ค‘์ธ ๊ฒฝ์šฐ๋งŒ) - if (e.ctrlKey && editingCell) { - // ๊ธฐ๋ณธ ๋™์ž‘ ํ—ˆ์šฉ (input์—์„œ ์ฒ˜๋ฆฌ) - } - break; - case "a": - case "A": - // ๐Ÿ†• Ctrl+A: ์ „์ฒด ์„ ํƒ - if (e.ctrlKey) { - e.preventDefault(); - handleSelectAllRows(); - } - break; - case "f": - case "F": - // ๐Ÿ†• Ctrl+F: ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ํŒจ๋„ ์—ด๊ธฐ - if (e.ctrlKey) { - e.preventDefault(); - setIsSearchPanelOpen(true); - } - break; - case "F3": - // ๐Ÿ†• F3: ๋‹ค์Œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ / Shift+F3: ์ด์ „ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ - e.preventDefault(); - if (e.shiftKey) { - goToPrevSearchResult(); - } else { - goToNextSearchResult(); - } - break; - case "Home": - e.preventDefault(); - if (e.ctrlKey) { - // Ctrl+Home: ์ฒซ ๋ฒˆ์งธ ์…€๋กœ - setFocusedCell({ rowIndex: 0, colIndex: 0 }); - } else { - // Home: ํ˜„์žฌ ํ–‰์˜ ์ฒซ ๋ฒˆ์งธ ์…€๋กœ - setFocusedCell({ rowIndex, colIndex: 0 }); - } - break; - case "End": - e.preventDefault(); - if (e.ctrlKey) { - // Ctrl+End: ๋งˆ์ง€๋ง‰ ์…€๋กœ - setFocusedCell({ rowIndex: maxRowIndex, colIndex: maxColIndex }); - } else { - // End: ํ˜„์žฌ ํ–‰์˜ ๋งˆ์ง€๋ง‰ ์…€๋กœ - setFocusedCell({ rowIndex, colIndex: maxColIndex }); - } - break; - case "PageUp": - e.preventDefault(); - // 10ํ–‰ ์œ„๋กœ - setFocusedCell({ rowIndex: Math.max(0, rowIndex - 10), colIndex }); - break; - case "PageDown": - e.preventDefault(); - // 10ํ–‰ ์•„๋ž˜๋กœ - setFocusedCell({ rowIndex: Math.min(maxRowIndex, rowIndex + 10), colIndex }); - break; - case "Escape": - e.preventDefault(); - // ํฌ์ปค์Šค ํ•ด์ œ - setFocusedCell(null); - break; - case "Tab": - e.preventDefault(); - if (e.shiftKey) { - // Shift+Tab: ์ด์ „ ์…€ if (colIndex > 0) { setFocusedCell({ rowIndex, colIndex: colIndex - 1 }); - } else if (rowIndex > 0) { - setFocusedCell({ rowIndex: rowIndex - 1, colIndex: maxColIndex }); } - } else { - // Tab: ๋‹ค์Œ ์…€ + break; + case "ArrowRight": + e.preventDefault(); if (colIndex < maxColIndex) { setFocusedCell({ rowIndex, colIndex: colIndex + 1 }); - } else if (rowIndex < maxRowIndex) { - setFocusedCell({ rowIndex: rowIndex + 1, colIndex: 0 }); } - } - break; - default: - // ๐Ÿ†• ์ง์ ‘ ํƒ€์ดํ•‘์œผ๋กœ ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… (์˜๋ฌธ์ž, ์ˆซ์ž, ํ•œ๊ธ€ ๋“ฑ) - if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { - const column = visibleColumns[colIndex]; - if (column && column.columnName !== "__checkbox__") { - // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ - if (column.editable === false) { - toast.warning(`'${column.displayName || column.columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); - break; + break; + case "Enter": + e.preventDefault(); + // ํ˜„์žฌ ํ–‰ ์„ ํƒ/ํ•ด์ œ + const enterRow = data[rowIndex]; + if (enterRow) { + const rowKey = getRowKey(enterRow, rowIndex); + const isCurrentlySelected = selectedRows.has(rowKey); + handleRowSelection(rowKey, !isCurrentlySelected); + } + break; + case " ": // Space + e.preventDefault(); + // ์ฒดํฌ๋ฐ•์Šค ํ† ๊ธ€ + const spaceRow = data[rowIndex]; + if (spaceRow) { + const currentRowKey = getRowKey(spaceRow, rowIndex); + const isChecked = selectedRows.has(currentRowKey); + handleRowSelection(currentRowKey, !isChecked); + } + break; + case "F2": + // ๐Ÿ†• F2: ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… + e.preventDefault(); + { + const col = visibleColumns[colIndex]; + if (col && col.columnName !== "__checkbox__") { + // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ + if (col.editable === false) { + toast.warning(`'${col.displayName || col.columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); + break; + } + const row = data[rowIndex]; + const mappedCol = joinColumnMapping[col.columnName] || col.columnName; + const val = row?.[mappedCol]; + setEditingCell({ + rowIndex, + colIndex, + columnName: col.columnName, + originalValue: val, + }); + setEditingValue(val !== null && val !== undefined ? String(val) : ""); } - e.preventDefault(); - // ํŽธ์ง‘ ์‹œ์ž‘ (ํ˜„์žฌ ํ‚ค๋ฅผ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ) - const row = data[rowIndex]; - const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; - const value = row?.[mappedColumnName]; - - setEditingCell({ - rowIndex, - colIndex, - columnName: column.columnName, - originalValue: value - }); - setEditingValue(e.key); // ์ž…๋ ฅํ•œ ํ‚ค๋กœ ์‹œ์ž‘ } - } - break; - } - }, [editingCell, focusedCell, data, visibleColumns, joinColumnMapping, selectedRows, getRowKey, handleRowSelection]); + break; + case "b": + case "B": + // ๐Ÿ†• Ctrl+B: ๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ํ† ๊ธ€ + if (e.ctrlKey) { + e.preventDefault(); + setEditMode((prev) => { + const newMode = prev === "immediate" ? "batch" : "immediate"; + if (newMode === "immediate" && pendingChanges.size > 0) { + // ์ฆ‰์‹œ ๋ชจ๋“œ๋กœ ์ „ํ™˜ ์‹œ ์ €์žฅ๋˜์ง€ ์•Š์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ ๊ฒฝ๊ณ  + const confirmDiscard = window.confirm( + `์ €์žฅ๋˜์ง€ ์•Š์€ ${pendingChanges.size}๊ฐœ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ทจ์†Œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`, + ); + if (confirmDiscard) { + setPendingChanges(new Map()); + setLocalEditedData({}); + toast.info("๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ์ข…๋ฃŒ"); + return "immediate"; + } + return "batch"; + } + toast.info(newMode === "batch" ? "๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ์‹œ์ž‘ (Ctrl+B๋กœ ์ข…๋ฃŒ)" : "์ฆ‰์‹œ ์ €์žฅ ๋ชจ๋“œ"); + return newMode; + }); + } + break; + case "s": + case "S": + // ๐Ÿ†• Ctrl+S: ๋ฐฐ์น˜ ์ €์žฅ + if (e.ctrlKey && editMode === "batch") { + e.preventDefault(); + saveBatchChanges(); + } + break; + case "c": + case "C": + // ๐Ÿ†• Ctrl+C: ์„ ํƒ๋œ ํ–‰/์…€ ๋ณต์‚ฌ + if (e.ctrlKey) { + e.preventDefault(); + handleCopy(); + } + break; + case "v": + case "V": + // ๐Ÿ†• Ctrl+V: ๋ถ™์—ฌ๋„ฃ๊ธฐ (ํŽธ์ง‘ ์ค‘์ธ ๊ฒฝ์šฐ๋งŒ) + if (e.ctrlKey && editingCell) { + // ๊ธฐ๋ณธ ๋™์ž‘ ํ—ˆ์šฉ (input์—์„œ ์ฒ˜๋ฆฌ) + } + break; + case "a": + case "A": + // ๐Ÿ†• Ctrl+A: ์ „์ฒด ์„ ํƒ + if (e.ctrlKey) { + e.preventDefault(); + handleSelectAllRows(); + } + break; + case "f": + case "F": + // ๐Ÿ†• Ctrl+F: ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ํŒจ๋„ ์—ด๊ธฐ + if (e.ctrlKey) { + e.preventDefault(); + setIsSearchPanelOpen(true); + } + break; + case "F3": + // ๐Ÿ†• F3: ๋‹ค์Œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ / Shift+F3: ์ด์ „ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ + e.preventDefault(); + if (e.shiftKey) { + goToPrevSearchResult(); + } else { + goToNextSearchResult(); + } + break; + case "Home": + e.preventDefault(); + if (e.ctrlKey) { + // Ctrl+Home: ์ฒซ ๋ฒˆ์งธ ์…€๋กœ + setFocusedCell({ rowIndex: 0, colIndex: 0 }); + } else { + // Home: ํ˜„์žฌ ํ–‰์˜ ์ฒซ ๋ฒˆ์งธ ์…€๋กœ + setFocusedCell({ rowIndex, colIndex: 0 }); + } + break; + case "End": + e.preventDefault(); + if (e.ctrlKey) { + // Ctrl+End: ๋งˆ์ง€๋ง‰ ์…€๋กœ + setFocusedCell({ rowIndex: maxRowIndex, colIndex: maxColIndex }); + } else { + // End: ํ˜„์žฌ ํ–‰์˜ ๋งˆ์ง€๋ง‰ ์…€๋กœ + setFocusedCell({ rowIndex, colIndex: maxColIndex }); + } + break; + case "PageUp": + e.preventDefault(); + // 10ํ–‰ ์œ„๋กœ + setFocusedCell({ rowIndex: Math.max(0, rowIndex - 10), colIndex }); + break; + case "PageDown": + e.preventDefault(); + // 10ํ–‰ ์•„๋ž˜๋กœ + setFocusedCell({ rowIndex: Math.min(maxRowIndex, rowIndex + 10), colIndex }); + break; + case "Escape": + e.preventDefault(); + // ํฌ์ปค์Šค ํ•ด์ œ + setFocusedCell(null); + break; + case "Tab": + e.preventDefault(); + if (e.shiftKey) { + // Shift+Tab: ์ด์ „ ์…€ + if (colIndex > 0) { + setFocusedCell({ rowIndex, colIndex: colIndex - 1 }); + } else if (rowIndex > 0) { + setFocusedCell({ rowIndex: rowIndex - 1, colIndex: maxColIndex }); + } + } else { + // Tab: ๋‹ค์Œ ์…€ + if (colIndex < maxColIndex) { + setFocusedCell({ rowIndex, colIndex: colIndex + 1 }); + } else if (rowIndex < maxRowIndex) { + setFocusedCell({ rowIndex: rowIndex + 1, colIndex: 0 }); + } + } + break; + default: + // ๐Ÿ†• ์ง์ ‘ ํƒ€์ดํ•‘์œผ๋กœ ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… (์˜๋ฌธ์ž, ์ˆซ์ž, ํ•œ๊ธ€ ๋“ฑ) + if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { + const column = visibleColumns[colIndex]; + if (column && column.columnName !== "__checkbox__") { + // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ + if (column.editable === false) { + toast.warning(`'${column.displayName || column.columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); + break; + } + e.preventDefault(); + // ํŽธ์ง‘ ์‹œ์ž‘ (ํ˜„์žฌ ํ‚ค๋ฅผ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ) + const row = data[rowIndex]; + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + const value = row?.[mappedColumnName]; + + setEditingCell({ + rowIndex, + colIndex, + columnName: column.columnName, + originalValue: value, + }); + setEditingValue(e.key); // ์ž…๋ ฅํ•œ ํ‚ค๋กœ ์‹œ์ž‘ + } + } + break; + } + }, + [editingCell, focusedCell, data, visibleColumns, joinColumnMapping, selectedRows, getRowKey, handleRowSelection], + ); const getColumnWidth = (column: ColumnConfig) => { if (column.columnName === "__checkbox__") return 50; @@ -3911,7 +4004,8 @@ export const TableListComponent: React.FC = ({ // ๐ŸŽฏ ์—”ํ‹ฐํ‹ฐ ์ปฌ๋Ÿผ ํ‘œ์‹œ ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ - value๊ฐ€ null์ด์–ด๋„ rowData์—์„œ ์กฐํ•ฉ ๊ฐ€๋Šฅ // ์ด ์ฒดํฌ๋ฅผ ๊ฐ€์žฅ ๋จผ์ € ์ˆ˜ํ–‰ (null ์ฒดํฌ๋ณด๋‹ค ์•ž์—) if (column.entityDisplayConfig && rowData) { - const displayColumns = column.entityDisplayConfig.displayColumns || (column.entityDisplayConfig as any).selectedColumns; + const displayColumns = + column.entityDisplayConfig.displayColumns || (column.entityDisplayConfig as any).selectedColumns; const separator = column.entityDisplayConfig.separator; if (displayColumns && displayColumns.length > 0) { @@ -3920,13 +4014,13 @@ export const TableListComponent: React.FC = ({ .map((colName: string) => { // 1. ๋จผ์ € ์ง์ ‘ ์ปฌ๋Ÿผ๋ช…์œผ๋กœ ์‹œ๋„ (๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ) let cellValue = rowData[colName]; - + // 2. ์—†์œผ๋ฉด ${sourceColumn}_${colName} ํ˜•์‹์œผ๋กœ ์‹œ๋„ (์กฐ์ธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ) if (cellValue === null || cellValue === undefined) { const joinedKey = `${column.columnName}_${colName}`; cellValue = rowData[joinedKey]; } - + if (cellValue === null || cellValue === undefined) return ""; return String(cellValue); }) @@ -4007,20 +4101,23 @@ export const TableListComponent: React.FC = ({ // 1. ์›๋ž˜ ์ปฌ๋Ÿผ๋ช… (item_info.material) // 2. ์ (.) ๋’ค์˜ ์ปฌ๋Ÿผ๋ช…๋งŒ (material) let mapping = categoryMappings[column.columnName]; - + if (!mapping && column.columnName.includes(".")) { const simpleColumnName = column.columnName.split(".").pop(); if (simpleColumnName) { mapping = categoryMappings[simpleColumnName]; } } - + const { Badge } = require("@/components/ui/badge"); // ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ: ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๊ฐ’๋“ค์„ ๋ถ„๋ฆฌ const valueStr = String(value); - const values = valueStr.includes(",") - ? valueStr.split(",").map(v => v.trim()).filter(v => v) + const values = valueStr.includes(",") + ? valueStr + .split(",") + .map((v) => v.trim()) + .filter((v) => v) : [valueStr]; // ๋‹จ์ผ ๊ฐ’์ธ ๊ฒฝ์šฐ (๊ธฐ์กด ๋กœ์ง) @@ -4102,8 +4199,8 @@ export const TableListComponent: React.FC = ({ try { const date = new Date(value); const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } catch { return String(value); @@ -4145,8 +4242,8 @@ export const TableListComponent: React.FC = ({ try { const date = new Date(value); const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } catch { return value; @@ -4178,7 +4275,7 @@ export const TableListComponent: React.FC = ({ // ํ•„ํ„ฐ ์„ค์ • localStorage ํ‚ค ์ƒ์„ฑ (ํ™”๋ฉด๋ณ„๋กœ ๋…๋ฆฝ์ ) const filterSettingKey = useMemo(() => { if (!tableConfig.selectedTable) return null; - return screenId + return screenId ? `tableList_filterSettings_${tableConfig.selectedTable}_screen_${screenId}` : `tableList_filterSettings_${tableConfig.selectedTable}`; }, [tableConfig.selectedTable, screenId]); @@ -4186,7 +4283,7 @@ export const TableListComponent: React.FC = ({ // ๊ทธ๋ฃน ์„ค์ • localStorage ํ‚ค ์ƒ์„ฑ (ํ™”๋ฉด๋ณ„๋กœ ๋…๋ฆฝ์ ) const groupSettingKey = useMemo(() => { if (!tableConfig.selectedTable) return null; - return screenId + return screenId ? `tableList_groupSettings_${tableConfig.selectedTable}_screen_${screenId}` : `tableList_groupSettings_${tableConfig.selectedTable}`; }, [tableConfig.selectedTable, screenId]); @@ -4367,15 +4464,15 @@ export const TableListComponent: React.FC = ({ // ์นดํ…Œ๊ณ ๋ฆฌ/์—”ํ‹ฐํ‹ฐ ํƒ€์ž…์ธ ๊ฒฝ์šฐ _name ํ•„๋“œ ์‚ฌ์šฉ const inputType = columnMeta?.[col]?.inputType; let displayValue = item[col]; - - if (inputType === 'category' || inputType === 'entity' || inputType === 'code') { + + if (inputType === "category" || inputType === "entity" || inputType === "code") { // _name ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ (์˜ˆ: division_name, writer_name) const nameField = `${col}_name`; if (item[nameField] !== undefined && item[nameField] !== null) { displayValue = item[nameField]; } } - + const label = columnLabels[col] || col; return `${label}:${displayValue !== null && displayValue !== undefined ? displayValue : "-"}`; }); @@ -4395,20 +4492,18 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ์†Œ๊ณ„ ๊ณ„์‚ฐ const groupSummary: Record = {}; - + // ์ˆซ์žํ˜• ์ปฌ๋Ÿผ์— ๋Œ€ํ•ด ์†Œ๊ณ„ ๊ณ„์‚ฐ (tableConfig.columns || []).forEach((col: { columnName: string }) => { if (col.columnName === "__checkbox__") return; - + const colMeta = columnMeta?.[col.columnName]; const inputType = colMeta?.inputType; const isNumeric = inputType === "number" || inputType === "decimal"; - + if (isNumeric) { - const values = items - .map((item) => parseFloat(item[col.columnName])) - .filter((v) => !isNaN(v)); - + const values = items.map((item) => parseFloat(item[col.columnName])).filter((v) => !isNaN(v)); + if (values.length > 0) { const sum = values.reduce((a, b) => a + b, 0); groupSummary[col.columnName] = { @@ -4430,6 +4525,114 @@ export const TableListComponent: React.FC = ({ }); }, [data, groupByColumns, columnLabels, columnMeta, tableConfig.columns]); + // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ๋œ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ (FilterPanel์—์„œ ์„ค์ •ํ•œ ๊ฒฝ์šฐ) + const summedData = useMemo(() => { + // ๊ทธ๋ฃนํ•‘์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ๊ฑฐ๋‚˜ ๊ทธ๋ฃน ๊ธฐ์ค€ ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + if (!groupSumConfig?.enabled || !groupSumConfig?.groupByColumn) { + return filteredData; + } + + console.log("๐Ÿ” [ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ] ๊ทธ๋ฃนํ•ฉ์‚ฐ ์ ์šฉ:", groupSumConfig); + + const groupByColumn = groupSumConfig.groupByColumn; + const groupMap = new Map(); + + // ์กฐ์ธ ์ปฌ๋Ÿผ์ธ์ง€ ํ™•์ธํ•˜๊ณ  ์‹ค์ œ ํ‚ค ์ถ”๋ก  + const getActualKey = (columnName: string, item: any): string => { + if (columnName.includes(".")) { + const [refTable, fieldName] = columnName.split("."); + const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); + const exactKey = `${inferredSourceColumn}_${fieldName}`; + if (item[exactKey] !== undefined) return exactKey; + if (fieldName === "item_name" || fieldName === "name") { + const aliasKey = `${inferredSourceColumn}_name`; + if (item[aliasKey] !== undefined) return aliasKey; + } + } + return columnName; + }; + + // ์ˆซ์ž ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜ + const isNumericValue = (value: any): boolean => { + if (value === null || value === undefined || value === "") return false; + const num = parseFloat(String(value)); + return !isNaN(num) && isFinite(num); + }; + + // ๊ทธ๋ฃนํ•‘ ์ˆ˜ํ–‰ + filteredData.forEach((item) => { + const actualKey = getActualKey(groupByColumn, item); + const groupValue = String(item[actualKey] || item[groupByColumn] || ""); + + if (!groupMap.has(groupValue)) { + // ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์„ ๊ธฐ์ค€์œผ๋กœ ์ดˆ๊ธฐํ™” + groupMap.set(groupValue, { ...item, _groupCount: 1 }); + } else { + const existing = groupMap.get(groupValue); + existing._groupCount += 1; + + // ๋ชจ๋“  ํ‚ค์— ๋Œ€ํ•ด ์ˆซ์ž๋ฉด ํ•ฉ์‚ฐ + Object.keys(item).forEach((key) => { + const value = item[key]; + if (isNumericValue(value) && key !== groupByColumn && !key.endsWith("_id") && !key.includes("code")) { + const numValue = parseFloat(String(value)); + const existingValue = parseFloat(String(existing[key] || 0)); + existing[key] = existingValue + numValue; + } + }); + + groupMap.set(groupValue, existing); + } + }); + + const result = Array.from(groupMap.values()); + console.log("๐Ÿ”— [ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ] ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ๊ฒฐ๊ณผ:", { + ์›๋ณธ๊ฐœ์ˆ˜: filteredData.length, + ๊ทธ๋ฃน๊ฐœ์ˆ˜: result.length, + ๊ทธ๋ฃน๊ธฐ์ค€: groupByColumn, + }); + + return result; + }, [filteredData, groupSumConfig]); + + // ๐Ÿ†• ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ: ํ•ฉ์‚ฐ ๋ชจ๋“œ๋ฉด summedData, ์•„๋‹ˆ๋ฉด filteredData + const displayData = useMemo(() => { + return groupSumConfig?.enabled ? summedData : filteredData; + }, [groupSumConfig?.enabled, summedData, filteredData]); + + // ๐Ÿ†• Virtual Scrolling: ๋ณด์ด๋Š” ํ–‰ ๋ฒ”์œ„ ๊ณ„์‚ฐ (displayData ์ •์˜ ์ดํ›„์— ์œ„์น˜) + const virtualScrollInfo = useMemo(() => { + const dataSource = displayData; + if (!isVirtualScrollEnabled || dataSource.length === 0) { + return { + startIndex: 0, + endIndex: dataSource.length, + visibleData: dataSource, + topSpacerHeight: 0, + bottomSpacerHeight: 0, + totalHeight: dataSource.length * ROW_HEIGHT, + }; + } + + const containerHeight = scrollContainerRef.current?.clientHeight || 600; + const totalRows = dataSource.length; + const totalHeight = totalRows * ROW_HEIGHT; + + // ํ˜„์žฌ ๋ณด์ด๋Š” ํ–‰ ๋ฒ”์œ„ ๊ณ„์‚ฐ + const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN); + const visibleRowCount = Math.ceil(containerHeight / ROW_HEIGHT) + OVERSCAN * 2; + const endIndex = Math.min(totalRows, startIndex + visibleRowCount); + + return { + startIndex, + endIndex, + visibleData: dataSource.slice(startIndex, endIndex), + topSpacerHeight: startIndex * ROW_HEIGHT, + bottomSpacerHeight: (totalRows - endIndex) * ROW_HEIGHT, + totalHeight, + }; + }, [isVirtualScrollEnabled, displayData, scrollTop, ROW_HEIGHT, OVERSCAN]); + // ์ €์žฅ๋œ ๊ทธ๋ฃน ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ useEffect(() => { if (!groupSettingKey || visibleColumns.length === 0) return; @@ -4458,7 +4661,7 @@ export const TableListComponent: React.FC = ({ // sortColumn, // sortDirection, // }); - + if (!isDesignMode && tableConfig.selectedTable) { fetchTableDataDebounced(); } @@ -4504,43 +4707,45 @@ export const TableListComponent: React.FC = ({ }, [tableConfig.selectedTable, isDesignMode]); // ๐ŸŽฏ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ž๋™ ๊ณ„์‚ฐ (๋‚ด์šฉ ๊ธฐ๋ฐ˜) - const calculateOptimalColumnWidth = useCallback((columnName: string, displayName: string): number => { - // ๊ธฐ๋ณธ ๋„ˆ๋น„ ์„ค์ • - const MIN_WIDTH = 100; - const MAX_WIDTH = 400; - const PADDING = 48; // ์ขŒ์šฐ ํŒจ๋”ฉ + ์—ฌ์œ  ๊ณต๊ฐ„ - const HEADER_PADDING = 60; // ํ—ค๋” ์ถ”๊ฐ€ ์—ฌ์œ  (์ •๋ ฌ ์•„์ด์ฝ˜ ๋“ฑ) + const calculateOptimalColumnWidth = useCallback( + (columnName: string, displayName: string): number => { + // ๊ธฐ๋ณธ ๋„ˆ๋น„ ์„ค์ • + const MIN_WIDTH = 100; + const MAX_WIDTH = 400; + const PADDING = 48; // ์ขŒ์šฐ ํŒจ๋”ฉ + ์—ฌ์œ  ๊ณต๊ฐ„ + const HEADER_PADDING = 60; // ํ—ค๋” ์ถ”๊ฐ€ ์—ฌ์œ  (์ •๋ ฌ ์•„์ด์ฝ˜ ๋“ฑ) - // ํ—ค๋” ํ…์ŠคํŠธ ๋„ˆ๋น„ ๊ณ„์‚ฐ (๋Œ€๋žต 8px per character) - const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING; + // ํ—ค๋” ํ…์ŠคํŠธ ๋„ˆ๋น„ ๊ณ„์‚ฐ (๋Œ€๋žต 8px per character) + const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING; - // ๋ฐ์ดํ„ฐ ์…€ ๋„ˆ๋น„ ๊ณ„์‚ฐ (์ƒ์œ„ 50๊ฐœ ์ƒ˜ํ”Œ๋ง) - const sampleSize = Math.min(50, data.length); - let maxDataWidth = headerWidth; + // ๋ฐ์ดํ„ฐ ์…€ ๋„ˆ๋น„ ๊ณ„์‚ฐ (์ƒ์œ„ 50๊ฐœ ์ƒ˜ํ”Œ๋ง) + const sampleSize = Math.min(50, data.length); + let maxDataWidth = headerWidth; - for (let i = 0; i < sampleSize; i++) { - const cellValue = data[i]?.[columnName]; - if (cellValue !== null && cellValue !== undefined) { - const cellText = String(cellValue); - // ์ˆซ์ž๋Š” ์ข๊ฒŒ, ํ…์ŠคํŠธ๋Š” ๋„“๊ฒŒ ๊ณ„์‚ฐ - const isNumber = !isNaN(Number(cellValue)) && cellValue !== ""; - const charWidth = isNumber ? 8 : 9; - const cellWidth = cellText.length * charWidth + PADDING; - maxDataWidth = Math.max(maxDataWidth, cellWidth); + for (let i = 0; i < sampleSize; i++) { + const cellValue = data[i]?.[columnName]; + if (cellValue !== null && cellValue !== undefined) { + const cellText = String(cellValue); + // ์ˆซ์ž๋Š” ์ข๊ฒŒ, ํ…์ŠคํŠธ๋Š” ๋„“๊ฒŒ ๊ณ„์‚ฐ + const isNumber = !isNaN(Number(cellValue)) && cellValue !== ""; + const charWidth = isNumber ? 8 : 9; + const cellWidth = cellText.length * charWidth + PADDING; + maxDataWidth = Math.max(maxDataWidth, cellWidth); + } } - } - // ์ตœ์†Œ/์ตœ๋Œ€ ๋ฒ”์œ„ ๋‚ด๋กœ ์ œํ•œ - return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth))); - }, [data]); + // ์ตœ์†Œ/์ตœ๋Œ€ ๋ฒ”์œ„ ๋‚ด๋กœ ์ œํ•œ + return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth))); + }, + [data], + ); // ๐ŸŽฏ localStorage์—์„œ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๋ฐ ์ดˆ๊ธฐ ๊ณ„์‚ฐ useEffect(() => { if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) { const timer = setTimeout(() => { - const storageKey = tableConfig.selectedTable && userId - ? `table_column_widths_${tableConfig.selectedTable}_${userId}` - : null; + const storageKey = + tableConfig.selectedTable && userId ? `table_column_widths_${tableConfig.selectedTable}_${userId}` : null; // 1. localStorage์—์„œ ์ €์žฅ๋œ ๋„ˆ๋น„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ let savedWidths: Record = {}; @@ -4570,8 +4775,8 @@ export const TableListComponent: React.FC = ({ } else { // ์ €์žฅ๋œ ๋„ˆ๋น„๊ฐ€ ์—†์œผ๋ฉด ์ž๋™ ๊ณ„์‚ฐ const optimalWidth = calculateOptimalColumnWidth( - column.columnName, - columnLabels[column.columnName] || column.displayName + column.columnName, + columnLabels[column.columnName] || column.displayName, ); newWidths[column.columnName] = optimalWidth; hasAnyWidth = true; @@ -4647,24 +4852,14 @@ export const TableListComponent: React.FC = ({ {/* ๐Ÿ†• ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ (Excel/PDF) */} -
Excel
- @@ -4680,12 +4875,7 @@ export const TableListComponent: React.FC = ({
PDF/์ธ์‡„
- @@ -4718,7 +4908,18 @@ export const TableListComponent: React.FC = ({
); - }, [tableConfig.pagination, tableConfig.toolbar?.showPaginationRefresh, isDesignMode, currentPage, totalPages, totalItems, loading, selectedRows.size, exportToExcel, exportToPdf]); + }, [ + tableConfig.pagination, + tableConfig.toolbar?.showPaginationRefresh, + isDesignMode, + currentPage, + totalPages, + totalItems, + loading, + selectedRows.size, + exportToExcel, + exportToPdf, + ]); // ======================================== // ๋ Œ๋”๋ง @@ -4822,7 +5023,7 @@ export const TableListComponent: React.FC = ({
{/* ํŽธ์ง‘ ๋ชจ๋“œ ํ† ๊ธ€ */} {(tableConfig.toolbar?.showEditMode ?? true) && ( -
+
{activeFilterCount > 0 && ( @@ -5030,15 +5227,13 @@ export const TableListComponent: React.FC = ({ {/* ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘ ํˆด๋ฐ” */} {(editMode === "batch" || pendingChanges.size > 0) && ( -
+
- + ๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ {pendingChanges.size > 0 && ( - - {pendingChanges.size}๊ฐœ ๋ณ€๊ฒฝ์‚ฌํ•ญ - + {pendingChanges.size}๊ฐœ ๋ณ€๊ฒฝ์‚ฌํ•ญ )}
@@ -5094,7 +5289,7 @@ export const TableListComponent: React.FC = ({
= ({
= ({ {visibleColumns.map((column, colIdx) => { // ์ด ์ปฌ๋Ÿผ์ด ์†ํ•œ band ์ฐพ๊ธฐ const band = columnBandsInfo.bands.find( - (b) => b.columns.includes(column.columnName) && b.startIndex === colIdx + (b) => b.columns.includes(column.columnName) && b.startIndex === colIdx, ); - + // band์˜ ์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ์—๋งŒ ๋ Œ๋”๋ง if (band) { return ( {band.caption} ); } - + // band์— ์†ํ•˜์ง€ ์•Š์€ ์ปฌ๋Ÿผ (๊ฐœ๋ณ„ ํ‘œ์‹œ) - const isInAnyBand = columnBandsInfo.bands.some( - (b) => b.columns.includes(column.columnName) - ); + const isInAnyBand = columnBandsInfo.bands.some((b) => b.columns.includes(column.columnName)); if (!isInAnyBand) { return ( {columnLabels[column.columnName] || column.columnName} ); } - + // band์˜ ์ค‘๊ฐ„ ์ปฌ๋Ÿผ์€ ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ return null; })} @@ -5205,7 +5398,7 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• Column Reordering ์ƒํƒœ const isColumnDragging = draggedColumnIndex === columnIndex; const isColumnDropTarget = dropTargetColumnIndex === columnIndex; - + return ( = ({ "hover:bg-muted/70 cursor-pointer transition-colors", isFrozen && "sticky z-60 shadow-[2px_0_4px_rgba(0,0,0,0.1)]", // ๐Ÿ†• Column Reordering ์Šคํƒ€์ผ - isColumnDragEnabled && column.columnName !== "__checkbox__" && "cursor-grab active:cursor-grabbing", - isColumnDragging && "opacity-50 bg-primary/20", - isColumnDropTarget && "border-l-4 border-l-primary", + isColumnDragEnabled && + column.columnName !== "__checkbox__" && + "cursor-grab active:cursor-grabbing", + isColumnDragging && "bg-primary/20 opacity-50", + isColumnDropTarget && "border-l-primary border-l-4", )} style={{ textAlign: column.columnName === "__checkbox__" ? "center" : "center", @@ -5256,7 +5451,7 @@ export const TableListComponent: React.FC = ({ {/* ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ํ‘œ์‹œ */} {column.editable === false && ( - + )} {columnLabels[column.columnName] || column.displayName} @@ -5264,75 +5459,82 @@ export const TableListComponent: React.FC = ({ {sortDirection === "asc" ? "โ†‘" : "โ†“"} )} {/* ๐Ÿ†• ํ—ค๋” ํ•„ํ„ฐ ๋ฒ„ํŠผ */} - {tableConfig.headerFilter !== false && columnUniqueValues[column.columnName]?.length > 0 && ( - setOpenFilterColumn(open ? column.columnName : null)} - > - - - - e.stopPropagation()} + {tableConfig.headerFilter !== false && + columnUniqueValues[column.columnName]?.length > 0 && ( + setOpenFilterColumn(open ? column.columnName : null)} > -
-
- ํ•„ํ„ฐ: {columnLabels[column.columnName] || column.displayName} - {headerFilters[column.columnName]?.size > 0 && ( - - )} -
-
- {columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => { - const isSelected = headerFilters[column.columnName]?.has(val); - return ( -
toggleHeaderFilter(column.columnName, val)} - > -
- {isSelected && } -
- {val || "(๋นˆ ๊ฐ’)"} -
+ + + + e.stopPropagation()} + > +
+
+ + ํ•„ํ„ฐ: {columnLabels[column.columnName] || column.displayName} + + {headerFilters[column.columnName]?.size > 0 && ( + + )} +
+
+ {columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => { + const isSelected = headerFilters[column.columnName]?.has(val); + return ( +
toggleHeaderFilter(column.columnName, val)} + > +
+ {isSelected && } +
+ {val || "(๋นˆ ๊ฐ’)"} +
+ ); + })} + {(columnUniqueValues[column.columnName]?.length || 0) > 50 && ( +
+ ...์™ธ {(columnUniqueValues[column.columnName]?.length || 0) - 50}๊ฐœ +
+ )} +
-
- - - )} + + + )}
)} {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ) */} @@ -5375,7 +5577,7 @@ export const TableListComponent: React.FC = ({ const finalWidth = Math.max(80, thElement.offsetWidth); setColumnWidths((prev) => { const newWidths = { ...prev, [column.columnName]: finalWidth }; - + // ๐ŸŽฏ localStorage์— ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ €์žฅ (์‚ฌ์šฉ์ž๋ณ„) if (tableConfig.selectedTable && userId) { const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`; @@ -5385,7 +5587,7 @@ export const TableListComponent: React.FC = ({ console.error("์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ €์žฅ ์‹คํŒจ:", error); } } - + return newWidths; }); } @@ -5508,8 +5710,10 @@ export const TableListComponent: React.FC = ({ = ({ ))} {/* ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ์†Œ๊ณ„ ํ–‰ */} {!isCollapsed && group.summary && Object.keys(group.summary).length > 0 && ( - + {visibleColumns.map((column, colIndex) => { const summary = group.summary?.[column.columnName]; const meta = columnMeta[column.columnName]; const inputType = meta?.inputType || (column as any).inputType; const isNumeric = inputType === "number" || inputType === "decimal"; - + if (colIndex === 0 && column.columnName === "__checkbox__") { return ( @@ -5552,15 +5756,17 @@ export const TableListComponent: React.FC = ({ ); } - + if (colIndex === 0 && column.columnName !== "__checkbox__") { return ( - ์†Œ๊ณ„ ({group.count}๊ฑด) + + ์†Œ๊ณ„ ({group.count}๊ฑด) + ); } - + if (summary) { return ( = ({ ); } - + return ; })} @@ -5589,193 +5795,208 @@ export const TableListComponent: React.FC = ({ )} - {/* ๋ฐ์ดํ„ฐ ํ–‰ ๋ Œ๋”๋ง */} - {(isVirtualScrollEnabled ? virtualScrollInfo.visibleData : filteredData).map((row, idx) => { + {/* ๋ฐ์ดํ„ฐ ํ–‰ ๋ Œ๋”๋ง - ๐Ÿ†• ํ•ฉ์‚ฐ ๋ชจ๋“œ๋ฉด displayData ์‚ฌ์šฉ */} + {(isVirtualScrollEnabled ? virtualScrollInfo.visibleData : displayData).map((row, idx) => { // Virtual Scrolling์—์„œ๋Š” ์‹ค์ œ ์ธ๋ฑ์Šค ๊ณ„์‚ฐ const index = isVirtualScrollEnabled ? virtualScrollInfo.startIndex + idx : idx; const rowKey = getRowKey(row, index); const isRowSelected = selectedRows.has(rowKey); const isRowFocused = focusedCell?.rowIndex === index; - - // ๐Ÿ†• Drag & Drop ์ƒํƒœ - const isDragging = draggedRowIndex === index; - const isDropTarget = dropTargetIndex === index; - - return ( - handleRowClick(row, index, e)} - role="row" - aria-selected={isRowSelected} - // ๐Ÿ†• Drag & Drop ์ด๋ฒคํŠธ - draggable={isDragEnabled} - onDragStart={(e) => handleRowDragStart(e, index)} - onDragOver={(e) => handleRowDragOver(e, index)} - onDragEnd={handleRowDragEnd} - onDrop={(e) => handleRowDrop(e, index)} - > - {visibleColumns.map((column, colIndex) => { - const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; - // ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘: ๋กœ์ปฌ ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์šฐ์„  ํ‘œ์‹œ - const cellValue = editMode === "batch" - ? getDisplayValue(row, index, mappedColumnName) - : row[mappedColumnName]; - const meta = columnMeta[column.columnName]; - const inputType = meta?.inputType || column.inputType; - const isNumeric = inputType === "number" || inputType === "decimal"; + // ๐Ÿ†• Drag & Drop ์ƒํƒœ + const isDragging = draggedRowIndex === index; + const isDropTarget = dropTargetIndex === index; - const isFrozen = frozenColumns.includes(column.columnName); - const frozenIndex = frozenColumns.indexOf(column.columnName); - - // ์…€ ํฌ์ปค์Šค ์ƒํƒœ - const isCellFocused = focusedCell?.rowIndex === index && focusedCell?.colIndex === colIndex; - - // ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘: ์ˆ˜์ •๋œ ์…€ ์—ฌ๋ถ€ - const isModified = isCellModified(index, mappedColumnName); - - // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ - const cellValidationError = getCellValidationError(index, mappedColumnName); - - // ๐Ÿ†• ๊ฒ€์ƒ‰ ํ•˜์ด๋ผ์ดํŠธ ์—ฌ๋ถ€ - const isSearchHighlighted = searchHighlights.has(`${index}-${colIndex}`); + return ( + handleRowClick(row, index, e)} + role="row" + aria-selected={isRowSelected} + // ๐Ÿ†• Drag & Drop ์ด๋ฒคํŠธ + draggable={isDragEnabled} + onDragStart={(e) => handleRowDragStart(e, index)} + onDragOver={(e) => handleRowDragOver(e, index)} + onDragEnd={handleRowDragEnd} + onDrop={(e) => handleRowDrop(e, index)} + > + {visibleColumns.map((column, colIndex) => { + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + // ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘: ๋กœ์ปฌ ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์šฐ์„  ํ‘œ์‹œ + const cellValue = + editMode === "batch" + ? getDisplayValue(row, index, mappedColumnName) + : row[mappedColumnName]; - // ํ‹€๊ณ ์ •๋œ ์ปฌ๋Ÿผ์˜ left ์œ„์น˜ ๊ณ„์‚ฐ - let leftPosition = 0; - if (isFrozen && frozenIndex > 0) { - for (let i = 0; i < frozenIndex; i++) { - const frozenCol = frozenColumns[i]; - const frozenColWidth = columnWidths[frozenCol] || 150; - leftPosition += frozenColWidth; + const meta = columnMeta[column.columnName]; + const inputType = meta?.inputType || column.inputType; + const isNumeric = inputType === "number" || inputType === "decimal"; + + const isFrozen = frozenColumns.includes(column.columnName); + const frozenIndex = frozenColumns.indexOf(column.columnName); + + // ์…€ ํฌ์ปค์Šค ์ƒํƒœ + const isCellFocused = focusedCell?.rowIndex === index && focusedCell?.colIndex === colIndex; + + // ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘: ์ˆ˜์ •๋œ ์…€ ์—ฌ๋ถ€ + const isModified = isCellModified(index, mappedColumnName); + + // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ + const cellValidationError = getCellValidationError(index, mappedColumnName); + + // ๐Ÿ†• ๊ฒ€์ƒ‰ ํ•˜์ด๋ผ์ดํŠธ ์—ฌ๋ถ€ + const isSearchHighlighted = searchHighlights.has(`${index}-${colIndex}`); + + // ํ‹€๊ณ ์ •๋œ ์ปฌ๋Ÿผ์˜ left ์œ„์น˜ ๊ณ„์‚ฐ + let leftPosition = 0; + if (isFrozen && frozenIndex > 0) { + for (let i = 0; i < frozenIndex; i++) { + const frozenCol = frozenColumns[i]; + const frozenColWidth = columnWidths[frozenCol] || 150; + leftPosition += frozenColWidth; + } } - } - return ( - handleCellClick(index, colIndex, e)} - onDoubleClick={() => handleCellDoubleClick(index, colIndex, column.columnName, cellValue)} - onContextMenu={(e) => handleContextMenu(e, index, colIndex, row)} - role="gridcell" - tabIndex={isCellFocused ? 0 : -1} - > - {/* ๐Ÿ†• ์ธ๋ผ์ธ ํŽธ์ง‘ ๋ชจ๋“œ */} - {editingCell?.rowIndex === index && editingCell?.colIndex === colIndex ? ( - // ๐Ÿ†• Cascading Lookups: ๋“œ๋กญ๋‹ค์šด ๋˜๋Š” ์ผ๋ฐ˜ ์ž…๋ ฅ - (() => { - const cascadingConfig = (tableConfig as any).cascadingLookups?.[column.columnName]; - const options = cascadingConfig ? getCascadingOptions(column.columnName, row) : []; - - // ๋ถ€๋ชจ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์˜ต์…˜ ๋กœ๋”ฉ - if (cascadingConfig && options.length === 0) { - const parentValue = row[cascadingConfig.parentColumn]; - if (parentValue !== undefined && parentValue !== null) { - loadCascadingOptions(column.columnName, cascadingConfig.parentColumn, parentValue); - } - } - - // ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ํƒ€์ž…์ด๊ฑฐ๋‚˜ Cascading Lookup์ธ ๊ฒฝ์šฐ ๋“œ๋กญ๋‹ค์šด - const colMeta = columnMeta[column.columnName]; - const isCategoryType = colMeta?.inputType === "category" || colMeta?.inputType === "code"; - const hasCategoryOptions = categoryMappings[column.columnName] && Object.keys(categoryMappings[column.columnName]).length > 0; - - if (cascadingConfig || (isCategoryType && hasCategoryOptions)) { - const selectOptions = cascadingConfig - ? options - : Object.entries(categoryMappings[column.columnName] || {}).map(([value, info]) => ({ - value, - label: info.label, - })); - - return ( - - ); - } - - // ์ผ๋ฐ˜ ์ž…๋ ฅ ํ•„๋“œ - return ( - setEditingValue(e.target.value)} - onKeyDown={handleEditKeyDown} - onBlur={saveEditing} - className="w-full h-full px-2 py-1 sm:px-4 sm:py-1.5 text-xs sm:text-sm border-2 border-primary bg-background focus:outline-none" - style={{ - textAlign: isNumeric ? "right" : column.align || "left", - }} - /> - ); - })() - ) : column.columnName === "__checkbox__" ? ( - renderCheckboxCell(row, index) - ) : ( - formatCellValue(cellValue, column, row) - )} - - ); - })} - - ); - })} + return ( + handleCellClick(index, colIndex, e)} + onDoubleClick={() => + handleCellDoubleClick(index, colIndex, column.columnName, cellValue) + } + onContextMenu={(e) => handleContextMenu(e, index, colIndex, row)} + role="gridcell" + tabIndex={isCellFocused ? 0 : -1} + > + {/* ๐Ÿ†• ์ธ๋ผ์ธ ํŽธ์ง‘ ๋ชจ๋“œ */} + {editingCell?.rowIndex === index && editingCell?.colIndex === colIndex + ? // ๐Ÿ†• Cascading Lookups: ๋“œ๋กญ๋‹ค์šด ๋˜๋Š” ์ผ๋ฐ˜ ์ž…๋ ฅ + (() => { + const cascadingConfig = (tableConfig as any).cascadingLookups?.[ + column.columnName + ]; + const options = cascadingConfig + ? getCascadingOptions(column.columnName, row) + : []; + + // ๋ถ€๋ชจ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์˜ต์…˜ ๋กœ๋”ฉ + if (cascadingConfig && options.length === 0) { + const parentValue = row[cascadingConfig.parentColumn]; + if (parentValue !== undefined && parentValue !== null) { + loadCascadingOptions( + column.columnName, + cascadingConfig.parentColumn, + parentValue, + ); + } + } + + // ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ํƒ€์ž…์ด๊ฑฐ๋‚˜ Cascading Lookup์ธ ๊ฒฝ์šฐ ๋“œ๋กญ๋‹ค์šด + const colMeta = columnMeta[column.columnName]; + const isCategoryType = + colMeta?.inputType === "category" || colMeta?.inputType === "code"; + const hasCategoryOptions = + categoryMappings[column.columnName] && + Object.keys(categoryMappings[column.columnName]).length > 0; + + if (cascadingConfig || (isCategoryType && hasCategoryOptions)) { + const selectOptions = cascadingConfig + ? options + : Object.entries(categoryMappings[column.columnName] || {}).map( + ([value, info]) => ({ + value, + label: info.label, + }), + ); + + return ( + + ); + } + + // ์ผ๋ฐ˜ ์ž…๋ ฅ ํ•„๋“œ + return ( + setEditingValue(e.target.value)} + onKeyDown={handleEditKeyDown} + onBlur={saveEditing} + className="border-primary bg-background h-full w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm" + style={{ + textAlign: isNumeric ? "right" : column.align || "left", + }} + /> + ); + })() + : column.columnName === "__checkbox__" + ? renderCheckboxCell(row, index) + : formatCellValue(cellValue, column, row)} + + ); + })} + + ); + })} {/* ๐Ÿ†• Virtual Scrolling: Bottom Spacer */} {isVirtualScrollEnabled && virtualScrollInfo.bottomSpacerHeight > 0 && ( @@ -5788,7 +6009,7 @@ export const TableListComponent: React.FC = ({ {/* ๐Ÿ†• ๋ฐ์ดํ„ฐ ์š”์•ฝ (Total Summaries) */} {summaryData && Object.keys(summaryData).length > 0 && ( - + {visibleColumns.map((column, colIndex) => { const summary = summaryData[column.columnName]; @@ -5961,24 +6182,20 @@ export const TableListComponent: React.FC = ({ closeContextMenu(); }} > - - ์…€ ๋ณต์‚ฌ + ์…€ ๋ณต์‚ฌ {/* ํ–‰ ๋ณต์‚ฌ */}
@@ -5990,9 +6207,7 @@ export const TableListComponent: React.FC = ({ return ( ); })()} @@ -6070,8 +6284,7 @@ export const TableListComponent: React.FC = ({ closeContextMenu(); }} > - - ํ–‰ ์‚ญ์ œ + ํ–‰ ์‚ญ์ œ
@@ -6219,10 +6432,7 @@ export const TableListComponent: React.FC = ({ > ์ทจ์†Œ - diff --git a/frontend/types/table-options.ts b/frontend/types/table-options.ts index c9971710..5685f127 100644 --- a/frontend/types/table-options.ts +++ b/frontend/types/table-options.ts @@ -7,21 +7,21 @@ */ export interface TableFilter { columnName: string; - operator: - | "equals" - | "contains" - | "startsWith" - | "endsWith" - | "gt" - | "lt" - | "gte" - | "lte" - | "notEquals"; + operator: "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "lt" | "gte" | "lte" | "notEquals"; value: string | number | boolean; filterType?: "text" | "number" | "date" | "select"; // ํ•„ํ„ฐ ์ž…๋ ฅ ํƒ€์ž… width?: number; // ํ•„ํ„ฐ ์ž…๋ ฅ ํ•„๋“œ ๋„ˆ๋น„ (px) } +/** + * ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • + */ +export interface GroupSumConfig { + enabled: boolean; // ๊ทธ๋ฃนํ•‘ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + groupByColumn: string; // ๊ทธ๋ฃน ๊ธฐ์ค€ ์ปฌ๋Ÿผ + groupByColumnLabel?: string; // ๊ทธ๋ฃน ๊ธฐ์ค€ ์ปฌ๋Ÿผ ๋ผ๋ฒจ (UI ํ‘œ์‹œ์šฉ) +} + /** * ์ปฌ๋Ÿผ ํ‘œ์‹œ ์„ค์ • */ @@ -60,7 +60,8 @@ export interface TableRegistration { onFilterChange: (filters: TableFilter[]) => void; onGroupChange: (groups: string[]) => void; onColumnVisibilityChange: (columns: ColumnVisibility[]) => void; - + onGroupSumChange?: (config: GroupSumConfig | null) => void; // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • ๋ณ€๊ฒฝ + // ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ•จ์ˆ˜ (์„ ํƒ ํƒ€์ž… ํ•„ํ„ฐ์šฉ) getColumnUniqueValues?: (columnName: string) => Promise>; } @@ -77,4 +78,3 @@ export interface TableOptionsContextValue { selectedTableId: string | null; setSelectedTableId: (tableId: string | null) => void; } -