From 3188bc051388e0c25b4f8bb84b1202e043e04a43 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 10 Dec 2025 16:06:47 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=85=EA=B3=A0=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EB=82=A0=EC=A7=9C=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=97=90=EB=9F=AC=E3=85=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cascadingRelationController.ts | 87 ++-- backend-node/src/services/dataService.ts | 423 +++++++++++------- .../src/services/dynamicFormService.ts | 242 ++++++---- frontend/lib/services/enhancedFormService.ts | 7 +- 4 files changed, 481 insertions(+), 278 deletions(-) diff --git a/backend-node/src/controllers/cascadingRelationController.ts b/backend-node/src/controllers/cascadingRelationController.ts index 5fb4e9d1..3f7b5cb6 100644 --- a/backend-node/src/controllers/cascadingRelationController.ts +++ b/backend-node/src/controllers/cascadingRelationController.ts @@ -45,8 +45,10 @@ export const getCascadingRelations = async (req: Request, res: Response) => { let paramIndex = 1; // 멀티테넌시 필터링 + // - 최고 관리자(company_code = "*"): 모든 데이터 조회 가능 + // - 일반 회사: 자기 회사 데이터만 조회 (공통 데이터는 조회 불가) if (companyCode !== "*") { - query += ` AND (company_code = $${paramIndex} OR company_code = '*')`; + query += ` AND company_code = $${paramIndex}`; params.push(companyCode); paramIndex++; } @@ -120,9 +122,9 @@ export const getCascadingRelationById = async (req: Request, res: Response) => { const params: any[] = [id]; - // 멀티테넌시 필터링 + // 멀티테넌시 필터링 (company_code = "*"는 최고 관리자 전용) if (companyCode !== "*") { - query += ` AND (company_code = $2 OR company_code = '*')`; + query += ` AND company_code = $2`; params.push(companyCode); } @@ -152,7 +154,10 @@ export const getCascadingRelationById = async (req: Request, res: Response) => { /** * 연쇄 관계 코드로 조회 */ -export const getCascadingRelationByCode = async (req: Request, res: Response) => { +export const getCascadingRelationByCode = async ( + req: Request, + res: Response +) => { try { const { code } = req.params; const companyCode = req.user?.companyCode || "*"; @@ -185,14 +190,12 @@ export const getCascadingRelationByCode = async (req: Request, res: Response) => const params: any[] = [code]; - // 멀티테넌시 필터링 (회사 전용 관계 우선, 없으면 공통 관계) + // 멀티테넌시 필터링 (company_code = "*"는 최고 관리자 전용) if (companyCode !== "*") { - query += ` AND (company_code = $2 OR company_code = '*')`; + query += ` AND company_code = $2`; params.push(companyCode); - query += ` ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END LIMIT 1`; - } else { - query += ` LIMIT 1`; } + query += ` LIMIT 1`; const result = await pool.query(query, params); @@ -245,8 +248,16 @@ export const createCascadingRelation = async (req: Request, res: Response) => { } = req.body; // 필수 필드 검증 - if (!relationCode || !relationName || !parentTable || !parentValueColumn || - !childTable || !childFilterColumn || !childValueColumn || !childLabelColumn) { + if ( + !relationCode || + !relationName || + !parentTable || + !parentValueColumn || + !childTable || + !childFilterColumn || + !childValueColumn || + !childLabelColumn + ) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다.", @@ -379,7 +390,11 @@ export const updateCascadingRelation = async (req: Request, res: Response) => { // 다른 회사의 데이터는 수정 불가 (최고 관리자 제외) const existingCompanyCode = existingCheck.rows[0].company_code; - if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") { + if ( + companyCode !== "*" && + existingCompanyCode !== companyCode && + existingCompanyCode !== "*" + ) { return res.status(403).json({ success: false, message: "수정 권한이 없습니다.", @@ -425,7 +440,11 @@ export const updateCascadingRelation = async (req: Request, res: Response) => { emptyParentMessage, noOptionsMessage, loadingMessage, - clearOnParentChange !== undefined ? (clearOnParentChange ? "Y" : "N") : null, + clearOnParentChange !== undefined + ? clearOnParentChange + ? "Y" + : "N" + : null, isActive !== undefined ? (isActive ? "Y" : "N") : null, userId, id, @@ -476,7 +495,11 @@ export const deleteCascadingRelation = async (req: Request, res: Response) => { // 다른 회사의 데이터는 삭제 불가 (최고 관리자 제외) const existingCompanyCode = existingCheck.rows[0].company_code; - if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") { + if ( + companyCode !== "*" && + existingCompanyCode !== companyCode && + existingCompanyCode !== "*" + ) { return res.status(403).json({ success: false, message: "삭제 권한이 없습니다.", @@ -531,13 +554,12 @@ export const getParentOptions = async (req: Request, res: Response) => { const relationParams: any[] = [code]; + // 멀티테넌시 필터링 (company_code = "*"는 최고 관리자 전용) if (companyCode !== "*") { - relationQuery += ` AND (company_code = $2 OR company_code = '*')`; + relationQuery += ` AND company_code = $2`; relationParams.push(companyCode); - relationQuery += ` ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END LIMIT 1`; - } else { - relationQuery += ` LIMIT 1`; } + relationQuery += ` LIMIT 1`; const relationResult = await pool.query(relationQuery, relationParams); @@ -551,7 +573,8 @@ export const getParentOptions = async (req: Request, res: Response) => { const relation = relationResult.rows[0]; // 라벨 컬럼이 없으면 값 컬럼 사용 - const labelColumn = relation.parent_label_column || relation.parent_value_column; + const labelColumn = + relation.parent_label_column || relation.parent_value_column; // 부모 옵션 조회 let optionsQuery = ` @@ -571,8 +594,13 @@ export const getParentOptions = async (req: Request, res: Response) => { const optionsParams: any[] = []; - if (tableInfoResult.rowCount && tableInfoResult.rowCount > 0 && companyCode !== "*") { - optionsQuery += ` AND (company_code = $1 OR company_code = '*')`; + // company_code = "*"는 최고 관리자 전용이므로 일반 회사는 자기 회사 데이터만 + if ( + tableInfoResult.rowCount && + tableInfoResult.rowCount > 0 && + companyCode !== "*" + ) { + optionsQuery += ` AND company_code = $1`; optionsParams.push(companyCode); } @@ -646,13 +674,12 @@ export const getCascadingOptions = async (req: Request, res: Response) => { const relationParams: any[] = [code]; + // 멀티테넌시 필터링 (company_code = "*"는 최고 관리자 전용) if (companyCode !== "*") { - relationQuery += ` AND (company_code = $2 OR company_code = '*')`; + relationQuery += ` AND company_code = $2`; relationParams.push(companyCode); - relationQuery += ` ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END LIMIT 1`; - } else { - relationQuery += ` LIMIT 1`; } + relationQuery += ` LIMIT 1`; const relationResult = await pool.query(relationQuery, relationParams); @@ -683,8 +710,13 @@ export const getCascadingOptions = async (req: Request, res: Response) => { const optionsParams: any[] = [parentValue]; - if (tableInfoResult.rowCount && tableInfoResult.rowCount > 0 && companyCode !== "*") { - optionsQuery += ` AND (company_code = $2 OR company_code = '*')`; + // company_code = "*"는 최고 관리자 전용이므로 일반 회사는 자기 회사 데이터만 + if ( + tableInfoResult.rowCount && + tableInfoResult.rowCount > 0 && + companyCode !== "*" + ) { + optionsQuery += ` AND company_code = $2`; optionsParams.push(companyCode); } @@ -716,4 +748,3 @@ export const getCascadingOptions = async (req: Request, res: Response) => { }); } }; - diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index a278eb97..a1a494f2 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -1,12 +1,12 @@ /** * 동적 데이터 서비스 - * + * * 주요 특징: * 1. 화이트리스트 제거 - 모든 테이블에 동적으로 접근 가능 * 2. 블랙리스트 방식 - 시스템 중요 테이블만 접근 금지 * 3. 자동 회사별 필터링 - company_code 컬럼 자동 감지 및 필터 적용 * 4. SQL 인젝션 방지 - 정규식 기반 테이블명/컬럼명 검증 - * + * * 보안: * - 테이블명은 영문, 숫자, 언더스코어만 허용 * - 시스템 테이블(pg_*, information_schema 등) 접근 금지 @@ -70,11 +70,11 @@ class DataService { // 그룹별로 데이터 분류 const groups: Record = {}; - + for (const row of data) { const groupKey = row[config.groupByColumn]; if (groupKey === undefined || groupKey === null) continue; - + if (!groups[groupKey]) { groups[groupKey] = []; } @@ -83,12 +83,12 @@ class DataService { // 각 그룹에서 하나의 행만 선택 const result: any[] = []; - + for (const [groupKey, rows] of Object.entries(groups)) { if (rows.length === 0) continue; - + let selectedRow: any; - + switch (config.keepStrategy) { case "latest": // 정렬 컬럼 기준 최신 (가장 큰 값) @@ -103,7 +103,7 @@ class DataService { } selectedRow = rows[0]; break; - + case "earliest": // 정렬 컬럼 기준 최초 (가장 작은 값) if (config.sortColumn) { @@ -117,38 +117,41 @@ class DataService { } selectedRow = rows[0]; break; - + case "base_price": // base_price = true인 행 찾기 - selectedRow = rows.find(row => row.base_price === true) || rows[0]; + selectedRow = rows.find((row) => row.base_price === true) || rows[0]; break; - + case "current_date": // start_date <= CURRENT_DATE <= end_date 조건에 맞는 행 const today = new Date(); today.setHours(0, 0, 0, 0); // 시간 제거 - - selectedRow = rows.find(row => { - const startDate = row.start_date ? new Date(row.start_date) : null; - const endDate = row.end_date ? new Date(row.end_date) : null; - - if (startDate) startDate.setHours(0, 0, 0, 0); - if (endDate) endDate.setHours(0, 0, 0, 0); - - const afterStart = !startDate || today >= startDate; - const beforeEnd = !endDate || today <= endDate; - - return afterStart && beforeEnd; - }) || rows[0]; // 조건에 맞는 행이 없으면 첫 번째 행 + + selectedRow = + rows.find((row) => { + const startDate = row.start_date + ? new Date(row.start_date) + : null; + const endDate = row.end_date ? new Date(row.end_date) : null; + + if (startDate) startDate.setHours(0, 0, 0, 0); + if (endDate) endDate.setHours(0, 0, 0, 0); + + const afterStart = !startDate || today >= startDate; + const beforeEnd = !endDate || today <= endDate; + + return afterStart && beforeEnd; + }) || rows[0]; // 조건에 맞는 행이 없으면 첫 번째 행 break; - + default: selectedRow = rows[0]; } - + result.push(selectedRow); } - + return result; } @@ -230,12 +233,17 @@ class DataService { // 4. 회사별 필터링 자동 적용 (company_code 컬럼이 있는 경우) if (userCompany && userCompany !== "*") { - const hasCompanyCode = await this.checkColumnExists(tableName, "company_code"); + const hasCompanyCode = await this.checkColumnExists( + tableName, + "company_code" + ); if (hasCompanyCode) { whereConditions.push(`company_code = $${paramIndex}`); queryParams.push(userCompany); paramIndex++; - console.log(`🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}`); + console.log( + `🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}` + ); } } @@ -508,7 +516,8 @@ class DataService { const entityJoinService = new EntityJoinService(); // Entity Join 구성 감지 - const joinConfigs = await entityJoinService.detectEntityJoins(tableName); + const joinConfigs = + await entityJoinService.detectEntityJoins(tableName); if (joinConfigs.length > 0) { console.log(`✅ Entity Join 감지: ${joinConfigs.length}개`); @@ -518,7 +527,7 @@ class DataService { tableName, joinConfigs, ["*"], - `main."${pkColumn}" = $1` // 🔧 main. 접두사 추가하여 모호성 해결 + `main."${pkColumn}" = $1` // 🔧 main. 접두사 추가하여 모호성 해결 ); const result = await pool.query(joinQuery, [id]); @@ -533,14 +542,14 @@ class DataService { // 🔧 날짜 타입 타임존 문제 해결: Date 객체를 YYYY-MM-DD 문자열로 변환 const normalizeDates = (rows: any[]) => { - return rows.map(row => { + return rows.map((row) => { const normalized: any = {}; for (const [key, value] of Object.entries(row)) { if (value instanceof Date) { // Date 객체를 YYYY-MM-DD 형식으로 변환 (타임존 무시) const year = value.getFullYear(); - const month = String(value.getMonth() + 1).padStart(2, '0'); - const day = String(value.getDate()).padStart(2, '0'); + const month = String(value.getMonth() + 1).padStart(2, "0"); + const day = String(value.getDate()).padStart(2, "0"); normalized[key] = `${year}-${month}-${day}`; } else { normalized[key] = value; @@ -551,17 +560,20 @@ class DataService { }; const normalizedRows = normalizeDates(result.rows); - console.log(`✅ Entity Join 데이터 조회 성공 (날짜 정규화됨):`, normalizedRows[0]); + console.log( + `✅ Entity Join 데이터 조회 성공 (날짜 정규화됨):`, + normalizedRows[0] + ); // 🆕 groupByColumns가 있으면 그룹핑 기반 다중 레코드 조회 if (groupByColumns.length > 0) { const baseRecord = result.rows[0]; - + // 그룹핑 컬럼들의 값 추출 const groupConditions: string[] = []; const groupValues: any[] = []; let paramIndex = 1; - + for (const col of groupByColumns) { const value = normalizedRows[0][col]; if (value !== undefined && value !== null) { @@ -570,12 +582,15 @@ class DataService { paramIndex++; } } - + if (groupConditions.length > 0) { const groupWhereClause = groupConditions.join(" AND "); - - console.log(`🔍 그룹핑 조회: ${groupByColumns.join(", ")}`, groupValues); - + + console.log( + `🔍 그룹핑 조회: ${groupByColumns.join(", ")}`, + groupValues + ); + // 그룹핑 기준으로 모든 레코드 조회 const { query: groupQuery } = entityJoinService.buildJoinQuery( tableName, @@ -583,12 +598,14 @@ class DataService { ["*"], groupWhereClause ); - + const groupResult = await pool.query(groupQuery, groupValues); - + const normalizedGroupRows = normalizeDates(groupResult.rows); - console.log(`✅ 그룹 레코드 조회 성공: ${normalizedGroupRows.length}개`); - + console.log( + `✅ 그룹 레코드 조회 성공: ${normalizedGroupRows.length}개` + ); + return { success: true, data: normalizedGroupRows, // 🔧 배열로 반환! @@ -642,7 +659,8 @@ class DataService { dataFilter?: any, // 🆕 데이터 필터 enableEntityJoin?: boolean, // 🆕 Entity 조인 활성화 displayColumns?: Array<{ name: string; label?: string }>, // 🆕 표시 컬럼 (item_info.item_name 등) - deduplication?: { // 🆕 중복 제거 설정 + deduplication?: { + // 🆕 중복 제거 설정 enabled: boolean; groupByColumn: string; keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; @@ -666,36 +684,41 @@ class DataService { if (enableEntityJoin) { try { const { entityJoinService } = await import("./entityJoinService"); - const joinConfigs = await entityJoinService.detectEntityJoins(rightTable); + const joinConfigs = + await entityJoinService.detectEntityJoins(rightTable); // 🆕 displayColumns에서 추가 조인 필요한 컬럼 감지 (item_info.item_name 등) if (displayColumns && Array.isArray(displayColumns)) { // 테이블별로 요청된 컬럼들을 그룹핑 const tableColumns: Record> = {}; - + for (const col of displayColumns) { - if (col.name && col.name.includes('.')) { - const [refTable, refColumn] = col.name.split('.'); + if (col.name && col.name.includes(".")) { + const [refTable, refColumn] = col.name.split("."); if (!tableColumns[refTable]) { tableColumns[refTable] = new Set(); } tableColumns[refTable].add(refColumn); } } - + // 각 테이블별로 처리 for (const [refTable, refColumns] of Object.entries(tableColumns)) { // 이미 조인 설정에 있는지 확인 - const existingJoins = joinConfigs.filter(jc => jc.referenceTable === refTable); - + const existingJoins = joinConfigs.filter( + (jc) => jc.referenceTable === refTable + ); + if (existingJoins.length > 0) { // 기존 조인이 있으면, 각 컬럼을 개별 조인으로 분리 for (const refColumn of refColumns) { // 이미 해당 컬럼을 표시하는 조인이 있는지 확인 const existingJoin = existingJoins.find( - jc => jc.displayColumns.length === 1 && jc.displayColumns[0] === refColumn + (jc) => + jc.displayColumns.length === 1 && + jc.displayColumns[0] === refColumn ); - + if (!existingJoin) { // 없으면 새 조인 설정 복제하여 추가 const baseJoin = existingJoins[0]; @@ -708,7 +731,9 @@ class DataService { referenceColumn: baseJoin.referenceColumn, // item_number 등 }; joinConfigs.push(newJoin); - console.log(`📌 추가 표시 컬럼: ${refTable}.${refColumn} (새 조인 생성, alias: ${newJoin.aliasColumn})`); + console.log( + `📌 추가 표시 컬럼: ${refTable}.${refColumn} (새 조인 생성, alias: ${newJoin.aliasColumn})` + ); } } } else { @@ -718,7 +743,9 @@ class DataService { } if (joinConfigs.length > 0) { - console.log(`🔗 조인 모드에서 Entity 조인 적용: ${joinConfigs.length}개 설정`); + console.log( + `🔗 조인 모드에서 Entity 조인 적용: ${joinConfigs.length}개 설정` + ); // WHERE 조건 생성 const whereConditions: string[] = []; @@ -735,7 +762,10 @@ class DataService { // 회사별 필터링 if (userCompany && userCompany !== "*") { - const hasCompanyCode = await this.checkColumnExists(rightTable, "company_code"); + const hasCompanyCode = await this.checkColumnExists( + rightTable, + "company_code" + ); if (hasCompanyCode) { whereConditions.push(`main.company_code = $${paramIndex}`); values.push(userCompany); @@ -744,48 +774,64 @@ class DataService { } // 데이터 필터 적용 (buildDataFilterWhereClause 사용) - if (dataFilter && dataFilter.enabled && dataFilter.filters && dataFilter.filters.length > 0) { - const { buildDataFilterWhereClause } = await import("../utils/dataFilterUtil"); - const filterResult = buildDataFilterWhereClause(dataFilter, "main", paramIndex); + if ( + dataFilter && + dataFilter.enabled && + dataFilter.filters && + dataFilter.filters.length > 0 + ) { + const { buildDataFilterWhereClause } = await import( + "../utils/dataFilterUtil" + ); + const filterResult = buildDataFilterWhereClause( + dataFilter, + "main", + paramIndex + ); if (filterResult.whereClause) { whereConditions.push(filterResult.whereClause); values.push(...filterResult.params); paramIndex += filterResult.params.length; - console.log(`🔍 Entity 조인에 데이터 필터 적용 (${rightTable}):`, filterResult.whereClause); + console.log( + `🔍 Entity 조인에 데이터 필터 적용 (${rightTable}):`, + filterResult.whereClause + ); console.log(`📊 필터 파라미터:`, filterResult.params); } } - const whereClause = whereConditions.length > 0 ? whereConditions.join(" AND ") : ""; + const whereClause = + whereConditions.length > 0 ? whereConditions.join(" AND ") : ""; // Entity 조인 쿼리 빌드 // buildJoinQuery가 자동으로 main.* 처리하므로 ["*"]만 전달 const selectColumns = ["*"]; - const { query: finalQuery, aliasMap } = entityJoinService.buildJoinQuery( - rightTable, - joinConfigs, - selectColumns, - whereClause, - "", - undefined, - undefined - ); + const { query: finalQuery, aliasMap } = + entityJoinService.buildJoinQuery( + rightTable, + joinConfigs, + selectColumns, + whereClause, + "", + undefined, + undefined + ); console.log(`🔍 Entity 조인 쿼리 실행 (전체):`, finalQuery); console.log(`🔍 파라미터:`, values); const result = await pool.query(finalQuery, values); - + // 🔧 날짜 타입 타임존 문제 해결 const normalizeDates = (rows: any[]) => { - return rows.map(row => { + return rows.map((row) => { const normalized: any = {}; for (const [key, value] of Object.entries(row)) { if (value instanceof Date) { const year = value.getFullYear(); - const month = String(value.getMonth() + 1).padStart(2, '0'); - const day = String(value.getDate()).padStart(2, '0'); + const month = String(value.getMonth() + 1).padStart(2, "0"); + const day = String(value.getDate()).padStart(2, "0"); normalized[key] = `${year}-${month}-${day}`; } else { normalized[key] = value; @@ -794,18 +840,24 @@ class DataService { return normalized; }); }; - + const normalizedRows = normalizeDates(result.rows); - console.log(`✅ Entity 조인 성공! 반환된 데이터 개수: ${normalizedRows.length}개 (날짜 정규화됨)`); - + console.log( + `✅ Entity 조인 성공! 반환된 데이터 개수: ${normalizedRows.length}개 (날짜 정규화됨)` + ); + // 🆕 중복 제거 처리 let finalData = normalizedRows; if (deduplication?.enabled && deduplication.groupByColumn) { - console.log(`🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}`); + console.log( + `🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}` + ); finalData = this.deduplicateData(normalizedRows, deduplication); - console.log(`✅ 중복 제거 완료: ${normalizedRows.length}개 → ${finalData.length}개`); + console.log( + `✅ 중복 제거 완료: ${normalizedRows.length}개 → ${finalData.length}개` + ); } - + return { success: true, data: finalData, @@ -838,23 +890,40 @@ class DataService { // 우측 테이블 회사별 필터링 (company_code 컬럼이 있는 경우) if (userCompany && userCompany !== "*") { - const hasCompanyCode = await this.checkColumnExists(rightTable, "company_code"); + const hasCompanyCode = await this.checkColumnExists( + rightTable, + "company_code" + ); if (hasCompanyCode) { whereConditions.push(`r.company_code = $${paramIndex}`); values.push(userCompany); paramIndex++; - console.log(`🏢 우측 패널 회사별 필터링 적용: ${rightTable}.company_code = ${userCompany}`); + console.log( + `🏢 우측 패널 회사별 필터링 적용: ${rightTable}.company_code = ${userCompany}` + ); } } // 🆕 데이터 필터 적용 (우측 패널에 대해, 테이블 별칭 "r" 사용) - if (dataFilter && dataFilter.enabled && dataFilter.filters && dataFilter.filters.length > 0) { - const filterResult = buildDataFilterWhereClause(dataFilter, "r", paramIndex); + if ( + dataFilter && + dataFilter.enabled && + dataFilter.filters && + dataFilter.filters.length > 0 + ) { + const filterResult = buildDataFilterWhereClause( + dataFilter, + "r", + paramIndex + ); if (filterResult.whereClause) { whereConditions.push(filterResult.whereClause); values.push(...filterResult.params); paramIndex += filterResult.params.length; - console.log(`🔍 데이터 필터 적용 (${rightTable}):`, filterResult.whereClause); + console.log( + `🔍 데이터 필터 적용 (${rightTable}):`, + filterResult.whereClause + ); } } @@ -871,9 +940,13 @@ class DataService { // 🆕 중복 제거 처리 let finalData = result; if (deduplication?.enabled && deduplication.groupByColumn) { - console.log(`🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}`); + console.log( + `🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}` + ); finalData = this.deduplicateData(result, deduplication); - console.log(`✅ 중복 제거 완료: ${result.length}개 → ${finalData.length}개`); + console.log( + `✅ 중복 제거 완료: ${result.length}개 → ${finalData.length}개` + ); } return { @@ -909,8 +982,10 @@ class DataService { // 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼 제외) const tableColumns = await this.getTableColumnsSimple(tableName); - const validColumnNames = new Set(tableColumns.map((col: any) => col.column_name)); - + const validColumnNames = new Set( + tableColumns.map((col: any) => col.column_name) + ); + const invalidColumns: string[] = []; const filteredData = Object.fromEntries( Object.entries(data).filter(([key]) => { @@ -921,9 +996,11 @@ class DataService { return false; }) ); - + if (invalidColumns.length > 0) { - console.log(`⚠️ [createRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}`); + console.log( + `⚠️ [createRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}` + ); } const columns = Object.keys(filteredData); @@ -975,8 +1052,10 @@ class DataService { // 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼 제외) const tableColumns = await this.getTableColumnsSimple(tableName); - const validColumnNames = new Set(tableColumns.map((col: any) => col.column_name)); - + const validColumnNames = new Set( + tableColumns.map((col: any) => col.column_name) + ); + const invalidColumns: string[] = []; cleanData = Object.fromEntries( Object.entries(cleanData).filter(([key]) => { @@ -987,9 +1066,11 @@ class DataService { return false; }) ); - + if (invalidColumns.length > 0) { - console.log(`⚠️ [updateRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}`); + console.log( + `⚠️ [updateRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}` + ); } // Primary Key 컬럼 찾기 @@ -1031,8 +1112,14 @@ class DataService { } // 🔗 조인 관계가 있는 경우, 연결된 테이블의 FK도 업데이트 - if (relationInfo && relationInfo.rightTable && relationInfo.leftColumn && relationInfo.rightColumn) { - const { rightTable, leftColumn, rightColumn, oldLeftValue } = relationInfo; + if ( + relationInfo && + relationInfo.rightTable && + relationInfo.leftColumn && + relationInfo.rightColumn + ) { + const { rightTable, leftColumn, rightColumn, oldLeftValue } = + relationInfo; const newLeftValue = cleanData[leftColumn]; // leftColumn 값이 변경된 경우에만 우측 테이블 업데이트 @@ -1050,8 +1137,13 @@ class DataService { SET "${rightColumn}" = $1 WHERE "${rightColumn}" = $2 `; - const updateResult = await query(updateRelatedQuery, [newLeftValue, oldLeftValue]); - console.log(`✅ 연결된 ${rightTable} 테이블의 ${updateResult.length}개 레코드 업데이트 완료`); + const updateResult = await query(updateRelatedQuery, [ + newLeftValue, + oldLeftValue, + ]); + console.log( + `✅ 연결된 ${rightTable} 테이블의 ${updateResult.length}개 레코드 업데이트 완료` + ); } catch (relError) { console.error("❌ 연결된 테이블 업데이트 실패:", relError); // 연결된 테이블 업데이트 실패 시 롤백은 하지 않고 경고만 로그 @@ -1102,9 +1194,11 @@ class DataService { if (pkResult.length > 1) { // 복합키인 경우: id가 객체여야 함 - console.log(`🔑 복합키 테이블: ${tableName}, PK: [${pkResult.map(r => r.attname).join(', ')}]`); - - if (typeof id === 'object' && !Array.isArray(id)) { + console.log( + `🔑 복합키 테이블: ${tableName}, PK: [${pkResult.map((r) => r.attname).join(", ")}]` + ); + + if (typeof id === "object" && !Array.isArray(id)) { // id가 객체인 경우: { user_id: 'xxx', dept_code: 'yyy' } pkResult.forEach((pk, index) => { whereClauses.push(`"${pk.attname}" = $${index + 1}`); @@ -1119,15 +1213,17 @@ class DataService { // 단일키인 경우 const pkColumn = pkResult.length > 0 ? pkResult[0].attname : "id"; whereClauses.push(`"${pkColumn}" = $1`); - params.push(typeof id === 'object' ? id[pkColumn] : id); + params.push(typeof id === "object" ? id[pkColumn] : id); } - const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(' AND ')}`; + const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(" AND ")}`; console.log(`🗑️ 삭제 쿼리:`, queryText, params); - + const result = await query(queryText, params); - - console.log(`✅ 레코드 삭제 완료: ${tableName}, 영향받은 행: ${result.length}`); + + console.log( + `✅ 레코드 삭제 완료: ${tableName}, 영향받은 행: ${result.length}` + ); return { success: true, @@ -1166,7 +1262,11 @@ class DataService { } if (whereConditions.length === 0) { - return { success: false, message: "삭제 조건이 없습니다.", error: "NO_CONDITIONS" }; + return { + success: false, + message: "삭제 조건이 없습니다.", + error: "NO_CONDITIONS", + }; } const whereClause = whereConditions.join(" AND "); @@ -1201,7 +1301,9 @@ class DataService { records: Array>, userCompany?: string, userId?: string - ): Promise> { + ): Promise< + ServiceResponse<{ inserted: number; updated: number; deleted: number }> + > { try { // 테이블 접근 권한 검증 const validation = await this.validateTableAccess(tableName); @@ -1239,11 +1341,14 @@ class DataService { const whereClause = whereConditions.join(" AND "); const selectQuery = `SELECT * FROM "${tableName}" WHERE ${whereClause}`; - - console.log(`📋 기존 레코드 조회:`, { query: selectQuery, values: whereValues }); - + + console.log(`📋 기존 레코드 조회:`, { + query: selectQuery, + values: whereValues, + }); + const existingRecords = await pool.query(selectQuery, whereValues); - + console.log(`✅ 기존 레코드: ${existingRecords.rows.length}개`); // 2. 새 레코드와 기존 레코드 비교 @@ -1254,50 +1359,53 @@ class DataService { // 날짜 필드를 YYYY-MM-DD 형식으로 변환하는 헬퍼 함수 const normalizeDateValue = (value: any): any => { if (value == null) return value; - + // ISO 날짜 문자열 감지 (YYYY-MM-DDTHH:mm:ss.sssZ) - if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) { - return value.split('T')[0]; // YYYY-MM-DD 만 추출 + if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) { + return value.split("T")[0]; // YYYY-MM-DD 만 추출 } - + return value; }; // 새 레코드 처리 (INSERT or UPDATE) for (const newRecord of records) { console.log(`🔍 처리할 새 레코드:`, newRecord); - + // 날짜 필드 정규화 const normalizedRecord: Record = {}; for (const [key, value] of Object.entries(newRecord)) { normalizedRecord[key] = normalizeDateValue(value); } - + console.log(`🔄 정규화된 레코드:`, normalizedRecord); - + // 전체 레코드 데이터 (parentKeys + normalizedRecord) const fullRecord = { ...parentKeys, ...normalizedRecord }; // 고유 키: parentKeys 제외한 나머지 필드들 const uniqueFields = Object.keys(normalizedRecord); - + console.log(`🔑 고유 필드들:`, uniqueFields); - + // 기존 레코드에서 일치하는 것 찾기 const existingRecord = existingRecords.rows.find((existing) => { return uniqueFields.every((field) => { const existingValue = existing[field]; const newValue = normalizedRecord[field]; - + // null/undefined 처리 if (existingValue == null && newValue == null) return true; if (existingValue == null || newValue == null) return false; - + // Date 타입 처리 - if (existingValue instanceof Date && typeof newValue === 'string') { - return existingValue.toISOString().split('T')[0] === newValue.split('T')[0]; + if (existingValue instanceof Date && typeof newValue === "string") { + return ( + existingValue.toISOString().split("T")[0] === + newValue.split("T")[0] + ); } - + // 문자열 비교 return String(existingValue) === String(newValue); }); @@ -1310,7 +1418,8 @@ class DataService { let updateParamIndex = 1; for (const [key, value] of Object.entries(fullRecord)) { - if (key !== pkColumn) { // Primary Key는 업데이트하지 않음 + if (key !== pkColumn) { + // Primary Key는 업데이트하지 않음 updateFields.push(`"${key}" = $${updateParamIndex}`); updateValues.push(value); updateParamIndex++; @@ -1326,36 +1435,42 @@ class DataService { await pool.query(updateQuery, updateValues); updated++; - + console.log(`✏️ UPDATE: ${pkColumn} = ${existingRecord[pkColumn]}`); } else { // INSERT: 기존 레코드가 없으면 삽입 - + // 🆕 자동 필드 추가 (company_code, writer, created_date, updated_date, id) + // created_date는 프론트엔드에서 전달된 값 무시하고 항상 현재 시간 설정 + const { created_date: _, ...recordWithoutCreatedDate } = fullRecord; const recordWithMeta: Record = { - ...fullRecord, + ...recordWithoutCreatedDate, id: uuidv4(), // 새 ID 생성 created_date: "NOW()", updated_date: "NOW()", }; - + // company_code가 없으면 userCompany 사용 (단, userCompany가 "*"가 아닐 때만) - if (!recordWithMeta.company_code && userCompany && userCompany !== "*") { + if ( + !recordWithMeta.company_code && + userCompany && + userCompany !== "*" + ) { recordWithMeta.company_code = userCompany; } - + // writer가 없으면 userId 사용 if (!recordWithMeta.writer && userId) { recordWithMeta.writer = userId; } - - const insertFields = Object.keys(recordWithMeta).filter(key => - recordWithMeta[key] !== "NOW()" + + const insertFields = Object.keys(recordWithMeta).filter( + (key) => recordWithMeta[key] !== "NOW()" ); const insertPlaceholders: string[] = []; const insertValues: any[] = []; let insertParamIndex = 1; - + for (const field of Object.keys(recordWithMeta)) { if (recordWithMeta[field] === "NOW()") { insertPlaceholders.push("NOW()"); @@ -1367,15 +1482,20 @@ class DataService { } const insertQuery = ` - INSERT INTO "${tableName}" (${Object.keys(recordWithMeta).map(f => `"${f}"`).join(", ")}) + INSERT INTO "${tableName}" (${Object.keys(recordWithMeta) + .map((f) => `"${f}"`) + .join(", ")}) VALUES (${insertPlaceholders.join(", ")}) `; - console.log(`➕ INSERT 쿼리:`, { query: insertQuery, values: insertValues }); + console.log(`➕ INSERT 쿼리:`, { + query: insertQuery, + values: insertValues, + }); await pool.query(insertQuery, insertValues); inserted++; - + console.log(`➕ INSERT: 새 레코드`); } } @@ -1383,19 +1503,22 @@ class DataService { // 3. 삭제할 레코드 찾기 (기존 레코드 중 새 레코드에 없는 것) for (const existingRecord of existingRecords.rows) { const uniqueFields = Object.keys(records[0] || {}); - + const stillExists = records.some((newRecord) => { return uniqueFields.every((field) => { const existingValue = existingRecord[field]; const newValue = newRecord[field]; - + if (existingValue == null && newValue == null) return true; if (existingValue == null || newValue == null) return false; - - if (existingValue instanceof Date && typeof newValue === 'string') { - return existingValue.toISOString().split('T')[0] === newValue.split('T')[0]; + + if (existingValue instanceof Date && typeof newValue === "string") { + return ( + existingValue.toISOString().split("T")[0] === + newValue.split("T")[0] + ); } - + return String(existingValue) === String(newValue); }); }); @@ -1405,7 +1528,7 @@ class DataService { const deleteQuery = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`; await pool.query(deleteQuery, [existingRecord[pkColumn]]); deleted++; - + console.log(`🗑️ DELETE: ${pkColumn} = ${existingRecord[pkColumn]}`); } } diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 99d6257c..be87f930 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -103,12 +103,16 @@ export class DynamicFormService { if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { // DATE 타입이면 문자열 그대로 유지 if (lowerDataType === "date") { - console.log(`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`); + console.log( + `📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)` + ); return value; // 문자열 그대로 반환 } // TIMESTAMP 타입이면 Date 객체로 변환 else { - console.log(`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`); + console.log( + `📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)` + ); return new Date(value + "T00:00:00"); } } @@ -250,7 +254,8 @@ export class DynamicFormService { if (tableColumns.includes("regdate") && !dataToInsert.regdate) { dataToInsert.regdate = new Date(); } - if (tableColumns.includes("created_date") && !dataToInsert.created_date) { + // created_date는 항상 현재 시간으로 설정 (기존 값 무시) + if (tableColumns.includes("created_date")) { dataToInsert.created_date = new Date(); } if (tableColumns.includes("updated_date") && !dataToInsert.updated_date) { @@ -313,7 +318,9 @@ export class DynamicFormService { } // YYYY-MM-DD 형태의 문자열은 그대로 유지 (DATE 타입으로 저장) else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) { - console.log(`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`); + console.log( + `📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)` + ); // dataToInsert[key] = value; // 문자열 그대로 유지 (이미 올바른 형식) } } @@ -346,35 +353,37 @@ export class DynamicFormService { ) { try { parsedArray = JSON.parse(value); - console.log( + console.log( `🔄 JSON 문자열 Repeater 데이터 감지: ${key}, ${parsedArray?.length || 0}개 항목` - ); + ); } catch (parseError) { console.log(`⚠️ JSON 파싱 실패: ${key}`); } } // 파싱된 배열이 있으면 처리 - if (parsedArray && Array.isArray(parsedArray) && parsedArray.length > 0) { - // 컴포넌트 설정에서 targetTable 추출 (컴포넌트 ID를 통해) - // 프론트엔드에서 { data: [...], targetTable: "..." } 형식으로 전달될 수 있음 - let targetTable: string | undefined; - let actualData = parsedArray; + if ( + parsedArray && + Array.isArray(parsedArray) && + parsedArray.length > 0 + ) { + // 컴포넌트 설정에서 targetTable 추출 (컴포넌트 ID를 통해) + // 프론트엔드에서 { data: [...], targetTable: "..." } 형식으로 전달될 수 있음 + let targetTable: string | undefined; + let actualData = parsedArray; - // 첫 번째 항목에 _targetTable이 있는지 확인 (프론트엔드에서 메타데이터 전달) - if (parsedArray[0] && parsedArray[0]._targetTable) { - targetTable = parsedArray[0]._targetTable; - actualData = parsedArray.map( - ({ _targetTable, ...item }) => item - ); - } + // 첫 번째 항목에 _targetTable이 있는지 확인 (프론트엔드에서 메타데이터 전달) + if (parsedArray[0] && parsedArray[0]._targetTable) { + targetTable = parsedArray[0]._targetTable; + actualData = parsedArray.map(({ _targetTable, ...item }) => item); + } - repeaterData.push({ - data: actualData, - targetTable, - componentId: key, - }); - delete dataToInsert[key]; // 원본 배열 데이터는 제거 + repeaterData.push({ + data: actualData, + targetTable, + componentId: key, + }); + delete dataToInsert[key]; // 원본 배열 데이터는 제거 console.log(`✅ Repeater 데이터 추가: ${key}`, { targetTable: targetTable || "없음 (화면 설계에서 설정 필요)", @@ -387,8 +396,8 @@ export class DynamicFormService { // 🔥 Repeater targetTable이 메인 테이블과 같으면 분리해서 저장 const separateRepeaterData: typeof repeaterData = []; const mergedRepeaterData: typeof repeaterData = []; - - repeaterData.forEach(repeater => { + + repeaterData.forEach((repeater) => { if (repeater.targetTable && repeater.targetTable !== tableName) { // 다른 테이블: 나중에 별도 저장 separateRepeaterData.push(repeater); @@ -397,10 +406,10 @@ export class DynamicFormService { mergedRepeaterData.push(repeater); } }); - + console.log(`🔄 Repeater 데이터 분류:`, { separate: separateRepeaterData.length, // 별도 테이블 - merged: mergedRepeaterData.length, // 메인 테이블과 병합 + merged: mergedRepeaterData.length, // 메인 테이블과 병합 }); // 존재하지 않는 컬럼 제거 @@ -494,23 +503,30 @@ export class DynamicFormService { const clientIp = ipAddress || "unknown"; let result: any[]; - + // 🔥 메인 테이블과 병합할 Repeater가 있으면 각 품목별로 INSERT if (mergedRepeaterData.length > 0) { - console.log(`🔄 메인 테이블 병합 모드: ${mergedRepeaterData.length}개 Repeater를 개별 레코드로 저장`); - + console.log( + `🔄 메인 테이블 병합 모드: ${mergedRepeaterData.length}개 Repeater를 개별 레코드로 저장` + ); + result = []; - + for (const repeater of mergedRepeaterData) { for (const item of repeater.data) { // 헤더 + 품목을 병합 - const rawMergedData = { ...dataToInsert, ...item }; - + // item에서 created_date 제거 (dataToInsert의 현재 시간 유지) + const { created_date: _, ...itemWithoutCreatedDate } = item; + const rawMergedData = { + ...dataToInsert, + ...itemWithoutCreatedDate, + }; + // 🆕 새 레코드 저장 시 id 제거하여 새 UUID 생성되도록 함 // _existingRecord가 명시적으로 true인 경우에만 기존 레코드로 처리 (UPDATE) // 그 외의 경우는 모두 새 레코드로 처리 (INSERT) const isExistingRecord = rawMergedData._existingRecord === true; - + if (!isExistingRecord) { // 새 레코드: id 제거하여 새 UUID 자동 생성 const oldId = rawMergedData.id; @@ -519,37 +535,43 @@ export class DynamicFormService { } else { console.log(`📝 기존 레코드 수정 (id 유지: ${rawMergedData.id})`); } - + // 메타 플래그 제거 delete rawMergedData._isNewItem; delete rawMergedData._existingRecord; - + // 🆕 실제 테이블 컬럼만 필터링 (조인/계산 컬럼 제외) const validColumnNames = columnInfo.map((col) => col.column_name); const mergedData: Record = {}; - + Object.keys(rawMergedData).forEach((columnName) => { // 실제 테이블 컬럼인지 확인 if (validColumnNames.includes(columnName)) { - const column = columnInfo.find((col) => col.column_name === columnName); - if (column) { - // 타입 변환 - mergedData[columnName] = this.convertValueForPostgreSQL( - rawMergedData[columnName], - column.data_type + const column = columnInfo.find( + (col) => col.column_name === columnName ); + if (column) { + // 타입 변환 + mergedData[columnName] = this.convertValueForPostgreSQL( + rawMergedData[columnName], + column.data_type + ); } else { mergedData[columnName] = rawMergedData[columnName]; } } else { - console.log(`⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})`); + console.log( + `⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})` + ); } }); - + const mergedColumns = Object.keys(mergedData); const mergedValues: any[] = Object.values(mergedData); - const mergedPlaceholders = mergedValues.map((_, index) => `$${index + 1}`).join(", "); - + const mergedPlaceholders = mergedValues + .map((_, index) => `$${index + 1}`) + .join(", "); + let mergedUpsertQuery: string; if (primaryKeys.length > 0) { const conflictColumns = primaryKeys.join(", "); @@ -557,7 +579,7 @@ export class DynamicFormService { .filter((col) => !primaryKeys.includes(col)) .map((col) => `${col} = EXCLUDED.${col}`) .join(", "); - + mergedUpsertQuery = updateSet ? `INSERT INTO ${tableName} (${mergedColumns.join(", ")}) VALUES (${mergedPlaceholders}) @@ -574,20 +596,20 @@ export class DynamicFormService { VALUES (${mergedPlaceholders}) RETURNING *`; } - + console.log(`📝 병합 INSERT:`, { mergedData }); - + const itemResult = await transaction(async (client) => { await client.query(`SET LOCAL app.user_id = '${userId}'`); await client.query(`SET LOCAL app.ip_address = '${clientIp}'`); const res = await client.query(mergedUpsertQuery, mergedValues); return res.rows[0]; }); - + result.push(itemResult); } } - + console.log(`✅ 병합 저장 완료: ${result.length}개 레코드`); } else { // 일반 모드: 헤더만 저장 @@ -597,7 +619,7 @@ export class DynamicFormService { const res = await client.query(upsertQuery, values); return res.rows; }); - + console.log("✅ 서비스: 실제 테이블 저장 성공:", result); } @@ -843,10 +865,10 @@ export class DynamicFormService { FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' `; - const columnTypesResult = await query<{ column_name: string; data_type: string }>( - columnTypesQuery, - [tableName] - ); + const columnTypesResult = await query<{ + column_name: string; + data_type: string; + }>(columnTypesQuery, [tableName]); const columnTypes: Record = {}; columnTypesResult.forEach((row) => { columnTypes[row.column_name] = row.data_type; @@ -859,11 +881,20 @@ export class DynamicFormService { .map((key, index) => { const dataType = columnTypes[key]; // 숫자 타입인 경우 명시적 캐스팅 - if (dataType === 'integer' || dataType === 'bigint' || dataType === 'smallint') { + if ( + dataType === "integer" || + dataType === "bigint" || + dataType === "smallint" + ) { return `${key} = $${index + 1}::integer`; - } else if (dataType === 'numeric' || dataType === 'decimal' || dataType === 'real' || dataType === 'double precision') { + } else if ( + dataType === "numeric" || + dataType === "decimal" || + dataType === "real" || + dataType === "double precision" + ) { return `${key} = $${index + 1}::numeric`; - } else if (dataType === 'boolean') { + } else if (dataType === "boolean") { return `${key} = $${index + 1}::boolean`; } else { // 문자열 타입은 캐스팅 불필요 @@ -877,13 +908,17 @@ export class DynamicFormService { // 🔑 Primary Key 타입에 맞게 캐스팅 const pkDataType = columnTypes[primaryKeyColumn]; - let pkCast = ''; - if (pkDataType === 'integer' || pkDataType === 'bigint' || pkDataType === 'smallint') { - pkCast = '::integer'; - } else if (pkDataType === 'numeric' || pkDataType === 'decimal') { - pkCast = '::numeric'; - } else if (pkDataType === 'uuid') { - pkCast = '::uuid'; + let pkCast = ""; + if ( + pkDataType === "integer" || + pkDataType === "bigint" || + pkDataType === "smallint" + ) { + pkCast = "::integer"; + } else if (pkDataType === "numeric" || pkDataType === "decimal") { + pkCast = "::numeric"; + } else if (pkDataType === "uuid") { + pkCast = "::uuid"; } // text, varchar 등은 캐스팅 불필요 @@ -1556,9 +1591,11 @@ export class DynamicFormService { componentId: layout.component_id, componentType: properties?.componentType, actionType: properties?.componentConfig?.action?.type, - enableDataflowControl: properties?.webTypeConfig?.enableDataflowControl, + enableDataflowControl: + properties?.webTypeConfig?.enableDataflowControl, hasDataflowConfig: !!properties?.webTypeConfig?.dataflowConfig, - hasDiagramId: !!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId, + hasDiagramId: + !!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId, }); // 버튼 컴포넌트이고 저장 액션이며 제어관리가 활성화된 경우 @@ -1583,21 +1620,26 @@ export class DynamicFormService { // 노드 플로우 실행 (relationshipId가 없는 경우 노드 플로우로 간주) let controlResult: any; - + if (!relationshipId) { // 노드 플로우 실행 console.log(`🚀 노드 플로우 실행 (flowId: ${diagramId})`); - const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService"); - - const executionResult = await NodeFlowExecutionService.executeFlow(diagramId, { - sourceData: [savedData], - dataSourceType: "formData", - buttonId: "save-button", - screenId: screenId, - userId: userId, - formData: savedData, - }); - + const { NodeFlowExecutionService } = await import( + "./nodeFlowExecutionService" + ); + + const executionResult = await NodeFlowExecutionService.executeFlow( + diagramId, + { + sourceData: [savedData], + dataSourceType: "formData", + buttonId: "save-button", + screenId: screenId, + userId: userId, + formData: savedData, + } + ); + controlResult = { success: executionResult.success, message: executionResult.message, @@ -1612,15 +1654,18 @@ export class DynamicFormService { }; } else { // 관계 기반 제어관리 실행 - console.log(`🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})`); - controlResult = await this.dataflowControlService.executeDataflowControl( - diagramId, - relationshipId, - triggerType, - savedData, - tableName, - userId + console.log( + `🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})` ); + controlResult = + await this.dataflowControlService.executeDataflowControl( + diagramId, + relationshipId, + triggerType, + savedData, + tableName, + userId + ); } console.log(`🎯 제어관리 실행 결과:`, controlResult); @@ -1677,7 +1722,7 @@ export class DynamicFormService { ): Promise<{ affectedRows: number }> { const pool = getPool(); const client = await pool.connect(); - + try { console.log("🔄 [updateFieldValue] 업데이트 실행:", { tableName, @@ -1695,11 +1740,13 @@ export class DynamicFormService { WHERE table_name = $1 AND column_name IN ('updated_by', 'updated_at', 'company_code') `; const columnResult = await client.query(columnQuery, [tableName]); - const existingColumns = columnResult.rows.map((row: any) => row.column_name); - - const hasUpdatedBy = existingColumns.includes('updated_by'); - const hasUpdatedAt = existingColumns.includes('updated_at'); - const hasCompanyCode = existingColumns.includes('company_code'); + const existingColumns = columnResult.rows.map( + (row: any) => row.column_name + ); + + const hasUpdatedBy = existingColumns.includes("updated_by"); + const hasUpdatedAt = existingColumns.includes("updated_at"); + const hasCompanyCode = existingColumns.includes("company_code"); console.log("🔍 [updateFieldValue] 테이블 컬럼 확인:", { hasUpdatedBy, @@ -1896,7 +1943,8 @@ export class DynamicFormService { paramIndex++; } - const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const limitClause = params.limit ? `LIMIT ${params.limit}` : "LIMIT 1000"; const sqlQuery = ` diff --git a/frontend/lib/services/enhancedFormService.ts b/frontend/lib/services/enhancedFormService.ts index 36768dac..70bcc106 100644 --- a/frontend/lib/services/enhancedFormService.ts +++ b/frontend/lib/services/enhancedFormService.ts @@ -292,9 +292,10 @@ export class EnhancedFormService { } // 시스템 필드 자동 추가 - const now = new Date().toISOString(); - if (!transformed.created_date && tableColumns.some((col) => col.columnName === "created_date")) { - transformed.created_date = now; + // created_date는 백엔드에서 처리하도록 프론트엔드에서 제거 + // (기존 데이터 조회 시 포함된 created_date가 그대로 전송되는 문제 방지) + if (tableColumns.some((col) => col.columnName === "created_date")) { + delete transformed.created_date; } if (!transformed.updated_date && tableColumns.some((col) => col.columnName === "updated_date")) { transformed.updated_date = now;