입고테이블 생성날짜 저장에러ㅏ 수정

This commit is contained in:
kjs 2025-12-10 16:06:47 +09:00
parent 08575c296e
commit 3188bc0513
4 changed files with 481 additions and 278 deletions

View File

@ -45,8 +45,10 @@ export const getCascadingRelations = async (req: Request, res: Response) => {
let paramIndex = 1; let paramIndex = 1;
// 멀티테넌시 필터링 // 멀티테넌시 필터링
// - 최고 관리자(company_code = "*"): 모든 데이터 조회 가능
// - 일반 회사: 자기 회사 데이터만 조회 (공통 데이터는 조회 불가)
if (companyCode !== "*") { if (companyCode !== "*") {
query += ` AND (company_code = $${paramIndex} OR company_code = '*')`; query += ` AND company_code = $${paramIndex}`;
params.push(companyCode); params.push(companyCode);
paramIndex++; paramIndex++;
} }
@ -120,9 +122,9 @@ export const getCascadingRelationById = async (req: Request, res: Response) => {
const params: any[] = [id]; const params: any[] = [id];
// 멀티테넌시 필터링 // 멀티테넌시 필터링 (company_code = "*"는 최고 관리자 전용)
if (companyCode !== "*") { if (companyCode !== "*") {
query += ` AND (company_code = $2 OR company_code = '*')`; query += ` AND company_code = $2`;
params.push(companyCode); 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 { try {
const { code } = req.params; const { code } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -185,14 +190,12 @@ export const getCascadingRelationByCode = async (req: Request, res: Response) =>
const params: any[] = [code]; const params: any[] = [code];
// 멀티테넌시 필터링 (회사 전용 관계 우선, 없으면 공통 관계) // 멀티테넌시 필터링 (company_code = "*"는 최고 관리자 전용)
if (companyCode !== "*") { if (companyCode !== "*") {
query += ` AND (company_code = $2 OR company_code = '*')`; query += ` AND company_code = $2`;
params.push(companyCode); 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); const result = await pool.query(query, params);
@ -245,8 +248,16 @@ export const createCascadingRelation = async (req: Request, res: Response) => {
} = req.body; } = req.body;
// 필수 필드 검증 // 필수 필드 검증
if (!relationCode || !relationName || !parentTable || !parentValueColumn || if (
!childTable || !childFilterColumn || !childValueColumn || !childLabelColumn) { !relationCode ||
!relationName ||
!parentTable ||
!parentValueColumn ||
!childTable ||
!childFilterColumn ||
!childValueColumn ||
!childLabelColumn
) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "필수 필드가 누락되었습니다.", message: "필수 필드가 누락되었습니다.",
@ -379,7 +390,11 @@ export const updateCascadingRelation = async (req: Request, res: Response) => {
// 다른 회사의 데이터는 수정 불가 (최고 관리자 제외) // 다른 회사의 데이터는 수정 불가 (최고 관리자 제외)
const existingCompanyCode = existingCheck.rows[0].company_code; const existingCompanyCode = existingCheck.rows[0].company_code;
if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") { if (
companyCode !== "*" &&
existingCompanyCode !== companyCode &&
existingCompanyCode !== "*"
) {
return res.status(403).json({ return res.status(403).json({
success: false, success: false,
message: "수정 권한이 없습니다.", message: "수정 권한이 없습니다.",
@ -425,7 +440,11 @@ export const updateCascadingRelation = async (req: Request, res: Response) => {
emptyParentMessage, emptyParentMessage,
noOptionsMessage, noOptionsMessage,
loadingMessage, loadingMessage,
clearOnParentChange !== undefined ? (clearOnParentChange ? "Y" : "N") : null, clearOnParentChange !== undefined
? clearOnParentChange
? "Y"
: "N"
: null,
isActive !== undefined ? (isActive ? "Y" : "N") : null, isActive !== undefined ? (isActive ? "Y" : "N") : null,
userId, userId,
id, id,
@ -476,7 +495,11 @@ export const deleteCascadingRelation = async (req: Request, res: Response) => {
// 다른 회사의 데이터는 삭제 불가 (최고 관리자 제외) // 다른 회사의 데이터는 삭제 불가 (최고 관리자 제외)
const existingCompanyCode = existingCheck.rows[0].company_code; const existingCompanyCode = existingCheck.rows[0].company_code;
if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") { if (
companyCode !== "*" &&
existingCompanyCode !== companyCode &&
existingCompanyCode !== "*"
) {
return res.status(403).json({ return res.status(403).json({
success: false, success: false,
message: "삭제 권한이 없습니다.", message: "삭제 권한이 없습니다.",
@ -531,13 +554,12 @@ export const getParentOptions = async (req: Request, res: Response) => {
const relationParams: any[] = [code]; const relationParams: any[] = [code];
// 멀티테넌시 필터링 (company_code = "*"는 최고 관리자 전용)
if (companyCode !== "*") { if (companyCode !== "*") {
relationQuery += ` AND (company_code = $2 OR company_code = '*')`; relationQuery += ` AND company_code = $2`;
relationParams.push(companyCode); 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); 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 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 = ` let optionsQuery = `
@ -571,8 +594,13 @@ export const getParentOptions = async (req: Request, res: Response) => {
const optionsParams: any[] = []; const optionsParams: any[] = [];
if (tableInfoResult.rowCount && tableInfoResult.rowCount > 0 && companyCode !== "*") { // company_code = "*"는 최고 관리자 전용이므로 일반 회사는 자기 회사 데이터만
optionsQuery += ` AND (company_code = $1 OR company_code = '*')`; if (
tableInfoResult.rowCount &&
tableInfoResult.rowCount > 0 &&
companyCode !== "*"
) {
optionsQuery += ` AND company_code = $1`;
optionsParams.push(companyCode); optionsParams.push(companyCode);
} }
@ -646,13 +674,12 @@ export const getCascadingOptions = async (req: Request, res: Response) => {
const relationParams: any[] = [code]; const relationParams: any[] = [code];
// 멀티테넌시 필터링 (company_code = "*"는 최고 관리자 전용)
if (companyCode !== "*") { if (companyCode !== "*") {
relationQuery += ` AND (company_code = $2 OR company_code = '*')`; relationQuery += ` AND company_code = $2`;
relationParams.push(companyCode); 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); const relationResult = await pool.query(relationQuery, relationParams);
@ -683,8 +710,13 @@ export const getCascadingOptions = async (req: Request, res: Response) => {
const optionsParams: any[] = [parentValue]; const optionsParams: any[] = [parentValue];
if (tableInfoResult.rowCount && tableInfoResult.rowCount > 0 && companyCode !== "*") { // company_code = "*"는 최고 관리자 전용이므로 일반 회사는 자기 회사 데이터만
optionsQuery += ` AND (company_code = $2 OR company_code = '*')`; if (
tableInfoResult.rowCount &&
tableInfoResult.rowCount > 0 &&
companyCode !== "*"
) {
optionsQuery += ` AND company_code = $2`;
optionsParams.push(companyCode); optionsParams.push(companyCode);
} }
@ -716,4 +748,3 @@ export const getCascadingOptions = async (req: Request, res: Response) => {
}); });
} }
}; };

View File

@ -1,12 +1,12 @@
/** /**
* *
* *
* : * :
* 1. - * 1. -
* 2. - * 2. -
* 3. - company_code * 3. - company_code
* 4. SQL - / * 4. SQL - /
* *
* : * :
* - , , * - , ,
* - (pg_*, information_schema ) * - (pg_*, information_schema )
@ -70,11 +70,11 @@ class DataService {
// 그룹별로 데이터 분류 // 그룹별로 데이터 분류
const groups: Record<string, any[]> = {}; const groups: Record<string, any[]> = {};
for (const row of data) { for (const row of data) {
const groupKey = row[config.groupByColumn]; const groupKey = row[config.groupByColumn];
if (groupKey === undefined || groupKey === null) continue; if (groupKey === undefined || groupKey === null) continue;
if (!groups[groupKey]) { if (!groups[groupKey]) {
groups[groupKey] = []; groups[groupKey] = [];
} }
@ -83,12 +83,12 @@ class DataService {
// 각 그룹에서 하나의 행만 선택 // 각 그룹에서 하나의 행만 선택
const result: any[] = []; const result: any[] = [];
for (const [groupKey, rows] of Object.entries(groups)) { for (const [groupKey, rows] of Object.entries(groups)) {
if (rows.length === 0) continue; if (rows.length === 0) continue;
let selectedRow: any; let selectedRow: any;
switch (config.keepStrategy) { switch (config.keepStrategy) {
case "latest": case "latest":
// 정렬 컬럼 기준 최신 (가장 큰 값) // 정렬 컬럼 기준 최신 (가장 큰 값)
@ -103,7 +103,7 @@ class DataService {
} }
selectedRow = rows[0]; selectedRow = rows[0];
break; break;
case "earliest": case "earliest":
// 정렬 컬럼 기준 최초 (가장 작은 값) // 정렬 컬럼 기준 최초 (가장 작은 값)
if (config.sortColumn) { if (config.sortColumn) {
@ -117,38 +117,41 @@ class DataService {
} }
selectedRow = rows[0]; selectedRow = rows[0];
break; break;
case "base_price": case "base_price":
// base_price = true인 행 찾기 // base_price = true인 행 찾기
selectedRow = rows.find(row => row.base_price === true) || rows[0]; selectedRow = rows.find((row) => row.base_price === true) || rows[0];
break; break;
case "current_date": case "current_date":
// start_date <= CURRENT_DATE <= end_date 조건에 맞는 행 // start_date <= CURRENT_DATE <= end_date 조건에 맞는 행
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); // 시간 제거 today.setHours(0, 0, 0, 0); // 시간 제거
selectedRow = rows.find(row => { selectedRow =
const startDate = row.start_date ? new Date(row.start_date) : null; rows.find((row) => {
const endDate = row.end_date ? new Date(row.end_date) : null; const startDate = row.start_date
? new Date(row.start_date)
if (startDate) startDate.setHours(0, 0, 0, 0); : null;
if (endDate) endDate.setHours(0, 0, 0, 0); const endDate = row.end_date ? new Date(row.end_date) : null;
const afterStart = !startDate || today >= startDate; if (startDate) startDate.setHours(0, 0, 0, 0);
const beforeEnd = !endDate || today <= endDate; if (endDate) endDate.setHours(0, 0, 0, 0);
return afterStart && beforeEnd; const afterStart = !startDate || today >= startDate;
}) || rows[0]; // 조건에 맞는 행이 없으면 첫 번째 행 const beforeEnd = !endDate || today <= endDate;
return afterStart && beforeEnd;
}) || rows[0]; // 조건에 맞는 행이 없으면 첫 번째 행
break; break;
default: default:
selectedRow = rows[0]; selectedRow = rows[0];
} }
result.push(selectedRow); result.push(selectedRow);
} }
return result; return result;
} }
@ -230,12 +233,17 @@ class DataService {
// 4. 회사별 필터링 자동 적용 (company_code 컬럼이 있는 경우) // 4. 회사별 필터링 자동 적용 (company_code 컬럼이 있는 경우)
if (userCompany && userCompany !== "*") { if (userCompany && userCompany !== "*") {
const hasCompanyCode = await this.checkColumnExists(tableName, "company_code"); const hasCompanyCode = await this.checkColumnExists(
tableName,
"company_code"
);
if (hasCompanyCode) { if (hasCompanyCode) {
whereConditions.push(`company_code = $${paramIndex}`); whereConditions.push(`company_code = $${paramIndex}`);
queryParams.push(userCompany); queryParams.push(userCompany);
paramIndex++; paramIndex++;
console.log(`🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}`); console.log(
`🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}`
);
} }
} }
@ -508,7 +516,8 @@ class DataService {
const entityJoinService = new EntityJoinService(); const entityJoinService = new EntityJoinService();
// Entity Join 구성 감지 // Entity Join 구성 감지
const joinConfigs = await entityJoinService.detectEntityJoins(tableName); const joinConfigs =
await entityJoinService.detectEntityJoins(tableName);
if (joinConfigs.length > 0) { if (joinConfigs.length > 0) {
console.log(`✅ Entity Join 감지: ${joinConfigs.length}`); console.log(`✅ Entity Join 감지: ${joinConfigs.length}`);
@ -518,7 +527,7 @@ class DataService {
tableName, tableName,
joinConfigs, joinConfigs,
["*"], ["*"],
`main."${pkColumn}" = $1` // 🔧 main. 접두사 추가하여 모호성 해결 `main."${pkColumn}" = $1` // 🔧 main. 접두사 추가하여 모호성 해결
); );
const result = await pool.query(joinQuery, [id]); const result = await pool.query(joinQuery, [id]);
@ -533,14 +542,14 @@ class DataService {
// 🔧 날짜 타입 타임존 문제 해결: Date 객체를 YYYY-MM-DD 문자열로 변환 // 🔧 날짜 타입 타임존 문제 해결: Date 객체를 YYYY-MM-DD 문자열로 변환
const normalizeDates = (rows: any[]) => { const normalizeDates = (rows: any[]) => {
return rows.map(row => { return rows.map((row) => {
const normalized: any = {}; const normalized: any = {};
for (const [key, value] of Object.entries(row)) { for (const [key, value] of Object.entries(row)) {
if (value instanceof Date) { if (value instanceof Date) {
// Date 객체를 YYYY-MM-DD 형식으로 변환 (타임존 무시) // Date 객체를 YYYY-MM-DD 형식으로 변환 (타임존 무시)
const year = value.getFullYear(); const year = value.getFullYear();
const month = String(value.getMonth() + 1).padStart(2, '0'); const month = String(value.getMonth() + 1).padStart(2, "0");
const day = String(value.getDate()).padStart(2, '0'); const day = String(value.getDate()).padStart(2, "0");
normalized[key] = `${year}-${month}-${day}`; normalized[key] = `${year}-${month}-${day}`;
} else { } else {
normalized[key] = value; normalized[key] = value;
@ -551,17 +560,20 @@ class DataService {
}; };
const normalizedRows = normalizeDates(result.rows); const normalizedRows = normalizeDates(result.rows);
console.log(`✅ Entity Join 데이터 조회 성공 (날짜 정규화됨):`, normalizedRows[0]); console.log(
`✅ Entity Join 데이터 조회 성공 (날짜 정규화됨):`,
normalizedRows[0]
);
// 🆕 groupByColumns가 있으면 그룹핑 기반 다중 레코드 조회 // 🆕 groupByColumns가 있으면 그룹핑 기반 다중 레코드 조회
if (groupByColumns.length > 0) { if (groupByColumns.length > 0) {
const baseRecord = result.rows[0]; const baseRecord = result.rows[0];
// 그룹핑 컬럼들의 값 추출 // 그룹핑 컬럼들의 값 추출
const groupConditions: string[] = []; const groupConditions: string[] = [];
const groupValues: any[] = []; const groupValues: any[] = [];
let paramIndex = 1; let paramIndex = 1;
for (const col of groupByColumns) { for (const col of groupByColumns) {
const value = normalizedRows[0][col]; const value = normalizedRows[0][col];
if (value !== undefined && value !== null) { if (value !== undefined && value !== null) {
@ -570,12 +582,15 @@ class DataService {
paramIndex++; paramIndex++;
} }
} }
if (groupConditions.length > 0) { if (groupConditions.length > 0) {
const groupWhereClause = groupConditions.join(" AND "); const groupWhereClause = groupConditions.join(" AND ");
console.log(`🔍 그룹핑 조회: ${groupByColumns.join(", ")}`, groupValues); console.log(
`🔍 그룹핑 조회: ${groupByColumns.join(", ")}`,
groupValues
);
// 그룹핑 기준으로 모든 레코드 조회 // 그룹핑 기준으로 모든 레코드 조회
const { query: groupQuery } = entityJoinService.buildJoinQuery( const { query: groupQuery } = entityJoinService.buildJoinQuery(
tableName, tableName,
@ -583,12 +598,14 @@ class DataService {
["*"], ["*"],
groupWhereClause groupWhereClause
); );
const groupResult = await pool.query(groupQuery, groupValues); const groupResult = await pool.query(groupQuery, groupValues);
const normalizedGroupRows = normalizeDates(groupResult.rows); const normalizedGroupRows = normalizeDates(groupResult.rows);
console.log(`✅ 그룹 레코드 조회 성공: ${normalizedGroupRows.length}`); console.log(
`✅ 그룹 레코드 조회 성공: ${normalizedGroupRows.length}`
);
return { return {
success: true, success: true,
data: normalizedGroupRows, // 🔧 배열로 반환! data: normalizedGroupRows, // 🔧 배열로 반환!
@ -642,7 +659,8 @@ class DataService {
dataFilter?: any, // 🆕 데이터 필터 dataFilter?: any, // 🆕 데이터 필터
enableEntityJoin?: boolean, // 🆕 Entity 조인 활성화 enableEntityJoin?: boolean, // 🆕 Entity 조인 활성화
displayColumns?: Array<{ name: string; label?: string }>, // 🆕 표시 컬럼 (item_info.item_name 등) displayColumns?: Array<{ name: string; label?: string }>, // 🆕 표시 컬럼 (item_info.item_name 등)
deduplication?: { // 🆕 중복 제거 설정 deduplication?: {
// 🆕 중복 제거 설정
enabled: boolean; enabled: boolean;
groupByColumn: string; groupByColumn: string;
keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; keepStrategy: "latest" | "earliest" | "base_price" | "current_date";
@ -666,36 +684,41 @@ class DataService {
if (enableEntityJoin) { if (enableEntityJoin) {
try { try {
const { entityJoinService } = await import("./entityJoinService"); const { entityJoinService } = await import("./entityJoinService");
const joinConfigs = await entityJoinService.detectEntityJoins(rightTable); const joinConfigs =
await entityJoinService.detectEntityJoins(rightTable);
// 🆕 displayColumns에서 추가 조인 필요한 컬럼 감지 (item_info.item_name 등) // 🆕 displayColumns에서 추가 조인 필요한 컬럼 감지 (item_info.item_name 등)
if (displayColumns && Array.isArray(displayColumns)) { if (displayColumns && Array.isArray(displayColumns)) {
// 테이블별로 요청된 컬럼들을 그룹핑 // 테이블별로 요청된 컬럼들을 그룹핑
const tableColumns: Record<string, Set<string>> = {}; const tableColumns: Record<string, Set<string>> = {};
for (const col of displayColumns) { for (const col of displayColumns) {
if (col.name && col.name.includes('.')) { if (col.name && col.name.includes(".")) {
const [refTable, refColumn] = col.name.split('.'); const [refTable, refColumn] = col.name.split(".");
if (!tableColumns[refTable]) { if (!tableColumns[refTable]) {
tableColumns[refTable] = new Set(); tableColumns[refTable] = new Set();
} }
tableColumns[refTable].add(refColumn); tableColumns[refTable].add(refColumn);
} }
} }
// 각 테이블별로 처리 // 각 테이블별로 처리
for (const [refTable, refColumns] of Object.entries(tableColumns)) { 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) { if (existingJoins.length > 0) {
// 기존 조인이 있으면, 각 컬럼을 개별 조인으로 분리 // 기존 조인이 있으면, 각 컬럼을 개별 조인으로 분리
for (const refColumn of refColumns) { for (const refColumn of refColumns) {
// 이미 해당 컬럼을 표시하는 조인이 있는지 확인 // 이미 해당 컬럼을 표시하는 조인이 있는지 확인
const existingJoin = existingJoins.find( const existingJoin = existingJoins.find(
jc => jc.displayColumns.length === 1 && jc.displayColumns[0] === refColumn (jc) =>
jc.displayColumns.length === 1 &&
jc.displayColumns[0] === refColumn
); );
if (!existingJoin) { if (!existingJoin) {
// 없으면 새 조인 설정 복제하여 추가 // 없으면 새 조인 설정 복제하여 추가
const baseJoin = existingJoins[0]; const baseJoin = existingJoins[0];
@ -708,7 +731,9 @@ class DataService {
referenceColumn: baseJoin.referenceColumn, // item_number 등 referenceColumn: baseJoin.referenceColumn, // item_number 등
}; };
joinConfigs.push(newJoin); joinConfigs.push(newJoin);
console.log(`📌 추가 표시 컬럼: ${refTable}.${refColumn} (새 조인 생성, alias: ${newJoin.aliasColumn})`); console.log(
`📌 추가 표시 컬럼: ${refTable}.${refColumn} (새 조인 생성, alias: ${newJoin.aliasColumn})`
);
} }
} }
} else { } else {
@ -718,7 +743,9 @@ class DataService {
} }
if (joinConfigs.length > 0) { if (joinConfigs.length > 0) {
console.log(`🔗 조인 모드에서 Entity 조인 적용: ${joinConfigs.length}개 설정`); console.log(
`🔗 조인 모드에서 Entity 조인 적용: ${joinConfigs.length}개 설정`
);
// WHERE 조건 생성 // WHERE 조건 생성
const whereConditions: string[] = []; const whereConditions: string[] = [];
@ -735,7 +762,10 @@ class DataService {
// 회사별 필터링 // 회사별 필터링
if (userCompany && userCompany !== "*") { if (userCompany && userCompany !== "*") {
const hasCompanyCode = await this.checkColumnExists(rightTable, "company_code"); const hasCompanyCode = await this.checkColumnExists(
rightTable,
"company_code"
);
if (hasCompanyCode) { if (hasCompanyCode) {
whereConditions.push(`main.company_code = $${paramIndex}`); whereConditions.push(`main.company_code = $${paramIndex}`);
values.push(userCompany); values.push(userCompany);
@ -744,48 +774,64 @@ class DataService {
} }
// 데이터 필터 적용 (buildDataFilterWhereClause 사용) // 데이터 필터 적용 (buildDataFilterWhereClause 사용)
if (dataFilter && dataFilter.enabled && dataFilter.filters && dataFilter.filters.length > 0) { if (
const { buildDataFilterWhereClause } = await import("../utils/dataFilterUtil"); dataFilter &&
const filterResult = buildDataFilterWhereClause(dataFilter, "main", paramIndex); dataFilter.enabled &&
dataFilter.filters &&
dataFilter.filters.length > 0
) {
const { buildDataFilterWhereClause } = await import(
"../utils/dataFilterUtil"
);
const filterResult = buildDataFilterWhereClause(
dataFilter,
"main",
paramIndex
);
if (filterResult.whereClause) { if (filterResult.whereClause) {
whereConditions.push(filterResult.whereClause); whereConditions.push(filterResult.whereClause);
values.push(...filterResult.params); values.push(...filterResult.params);
paramIndex += filterResult.params.length; paramIndex += filterResult.params.length;
console.log(`🔍 Entity 조인에 데이터 필터 적용 (${rightTable}):`, filterResult.whereClause); console.log(
`🔍 Entity 조인에 데이터 필터 적용 (${rightTable}):`,
filterResult.whereClause
);
console.log(`📊 필터 파라미터:`, filterResult.params); console.log(`📊 필터 파라미터:`, filterResult.params);
} }
} }
const whereClause = whereConditions.length > 0 ? whereConditions.join(" AND ") : ""; const whereClause =
whereConditions.length > 0 ? whereConditions.join(" AND ") : "";
// Entity 조인 쿼리 빌드 // Entity 조인 쿼리 빌드
// buildJoinQuery가 자동으로 main.* 처리하므로 ["*"]만 전달 // buildJoinQuery가 자동으로 main.* 처리하므로 ["*"]만 전달
const selectColumns = ["*"]; const selectColumns = ["*"];
const { query: finalQuery, aliasMap } = entityJoinService.buildJoinQuery( const { query: finalQuery, aliasMap } =
rightTable, entityJoinService.buildJoinQuery(
joinConfigs, rightTable,
selectColumns, joinConfigs,
whereClause, selectColumns,
"", whereClause,
undefined, "",
undefined undefined,
); undefined
);
console.log(`🔍 Entity 조인 쿼리 실행 (전체):`, finalQuery); console.log(`🔍 Entity 조인 쿼리 실행 (전체):`, finalQuery);
console.log(`🔍 파라미터:`, values); console.log(`🔍 파라미터:`, values);
const result = await pool.query(finalQuery, values); const result = await pool.query(finalQuery, values);
// 🔧 날짜 타입 타임존 문제 해결 // 🔧 날짜 타입 타임존 문제 해결
const normalizeDates = (rows: any[]) => { const normalizeDates = (rows: any[]) => {
return rows.map(row => { return rows.map((row) => {
const normalized: any = {}; const normalized: any = {};
for (const [key, value] of Object.entries(row)) { for (const [key, value] of Object.entries(row)) {
if (value instanceof Date) { if (value instanceof Date) {
const year = value.getFullYear(); const year = value.getFullYear();
const month = String(value.getMonth() + 1).padStart(2, '0'); const month = String(value.getMonth() + 1).padStart(2, "0");
const day = String(value.getDate()).padStart(2, '0'); const day = String(value.getDate()).padStart(2, "0");
normalized[key] = `${year}-${month}-${day}`; normalized[key] = `${year}-${month}-${day}`;
} else { } else {
normalized[key] = value; normalized[key] = value;
@ -794,18 +840,24 @@ class DataService {
return normalized; return normalized;
}); });
}; };
const normalizedRows = normalizeDates(result.rows); const normalizedRows = normalizeDates(result.rows);
console.log(`✅ Entity 조인 성공! 반환된 데이터 개수: ${normalizedRows.length}개 (날짜 정규화됨)`); console.log(
`✅ Entity 조인 성공! 반환된 데이터 개수: ${normalizedRows.length}개 (날짜 정규화됨)`
);
// 🆕 중복 제거 처리 // 🆕 중복 제거 처리
let finalData = normalizedRows; let finalData = normalizedRows;
if (deduplication?.enabled && deduplication.groupByColumn) { if (deduplication?.enabled && deduplication.groupByColumn) {
console.log(`🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}`); console.log(
`🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}`
);
finalData = this.deduplicateData(normalizedRows, deduplication); finalData = this.deduplicateData(normalizedRows, deduplication);
console.log(`✅ 중복 제거 완료: ${normalizedRows.length}개 → ${finalData.length}`); console.log(
`✅ 중복 제거 완료: ${normalizedRows.length}개 → ${finalData.length}`
);
} }
return { return {
success: true, success: true,
data: finalData, data: finalData,
@ -838,23 +890,40 @@ class DataService {
// 우측 테이블 회사별 필터링 (company_code 컬럼이 있는 경우) // 우측 테이블 회사별 필터링 (company_code 컬럼이 있는 경우)
if (userCompany && userCompany !== "*") { if (userCompany && userCompany !== "*") {
const hasCompanyCode = await this.checkColumnExists(rightTable, "company_code"); const hasCompanyCode = await this.checkColumnExists(
rightTable,
"company_code"
);
if (hasCompanyCode) { if (hasCompanyCode) {
whereConditions.push(`r.company_code = $${paramIndex}`); whereConditions.push(`r.company_code = $${paramIndex}`);
values.push(userCompany); values.push(userCompany);
paramIndex++; paramIndex++;
console.log(`🏢 우측 패널 회사별 필터링 적용: ${rightTable}.company_code = ${userCompany}`); console.log(
`🏢 우측 패널 회사별 필터링 적용: ${rightTable}.company_code = ${userCompany}`
);
} }
} }
// 🆕 데이터 필터 적용 (우측 패널에 대해, 테이블 별칭 "r" 사용) // 🆕 데이터 필터 적용 (우측 패널에 대해, 테이블 별칭 "r" 사용)
if (dataFilter && dataFilter.enabled && dataFilter.filters && dataFilter.filters.length > 0) { if (
const filterResult = buildDataFilterWhereClause(dataFilter, "r", paramIndex); dataFilter &&
dataFilter.enabled &&
dataFilter.filters &&
dataFilter.filters.length > 0
) {
const filterResult = buildDataFilterWhereClause(
dataFilter,
"r",
paramIndex
);
if (filterResult.whereClause) { if (filterResult.whereClause) {
whereConditions.push(filterResult.whereClause); whereConditions.push(filterResult.whereClause);
values.push(...filterResult.params); values.push(...filterResult.params);
paramIndex += filterResult.params.length; paramIndex += filterResult.params.length;
console.log(`🔍 데이터 필터 적용 (${rightTable}):`, filterResult.whereClause); console.log(
`🔍 데이터 필터 적용 (${rightTable}):`,
filterResult.whereClause
);
} }
} }
@ -871,9 +940,13 @@ class DataService {
// 🆕 중복 제거 처리 // 🆕 중복 제거 처리
let finalData = result; let finalData = result;
if (deduplication?.enabled && deduplication.groupByColumn) { if (deduplication?.enabled && deduplication.groupByColumn) {
console.log(`🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}`); console.log(
`🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}`
);
finalData = this.deduplicateData(result, deduplication); finalData = this.deduplicateData(result, deduplication);
console.log(`✅ 중복 제거 완료: ${result.length}개 → ${finalData.length}`); console.log(
`✅ 중복 제거 완료: ${result.length}개 → ${finalData.length}`
);
} }
return { return {
@ -909,8 +982,10 @@ class DataService {
// 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼 제외) // 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼 제외)
const tableColumns = await this.getTableColumnsSimple(tableName); 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 invalidColumns: string[] = [];
const filteredData = Object.fromEntries( const filteredData = Object.fromEntries(
Object.entries(data).filter(([key]) => { Object.entries(data).filter(([key]) => {
@ -921,9 +996,11 @@ class DataService {
return false; return false;
}) })
); );
if (invalidColumns.length > 0) { if (invalidColumns.length > 0) {
console.log(`⚠️ [createRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}`); console.log(
`⚠️ [createRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}`
);
} }
const columns = Object.keys(filteredData); const columns = Object.keys(filteredData);
@ -975,8 +1052,10 @@ class DataService {
// 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼 제외) // 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼 제외)
const tableColumns = await this.getTableColumnsSimple(tableName); 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 invalidColumns: string[] = [];
cleanData = Object.fromEntries( cleanData = Object.fromEntries(
Object.entries(cleanData).filter(([key]) => { Object.entries(cleanData).filter(([key]) => {
@ -987,9 +1066,11 @@ class DataService {
return false; return false;
}) })
); );
if (invalidColumns.length > 0) { if (invalidColumns.length > 0) {
console.log(`⚠️ [updateRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}`); console.log(
`⚠️ [updateRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}`
);
} }
// Primary Key 컬럼 찾기 // Primary Key 컬럼 찾기
@ -1031,8 +1112,14 @@ class DataService {
} }
// 🔗 조인 관계가 있는 경우, 연결된 테이블의 FK도 업데이트 // 🔗 조인 관계가 있는 경우, 연결된 테이블의 FK도 업데이트
if (relationInfo && relationInfo.rightTable && relationInfo.leftColumn && relationInfo.rightColumn) { if (
const { rightTable, leftColumn, rightColumn, oldLeftValue } = relationInfo; relationInfo &&
relationInfo.rightTable &&
relationInfo.leftColumn &&
relationInfo.rightColumn
) {
const { rightTable, leftColumn, rightColumn, oldLeftValue } =
relationInfo;
const newLeftValue = cleanData[leftColumn]; const newLeftValue = cleanData[leftColumn];
// leftColumn 값이 변경된 경우에만 우측 테이블 업데이트 // leftColumn 값이 변경된 경우에만 우측 테이블 업데이트
@ -1050,8 +1137,13 @@ class DataService {
SET "${rightColumn}" = $1 SET "${rightColumn}" = $1
WHERE "${rightColumn}" = $2 WHERE "${rightColumn}" = $2
`; `;
const updateResult = await query(updateRelatedQuery, [newLeftValue, oldLeftValue]); const updateResult = await query(updateRelatedQuery, [
console.log(`✅ 연결된 ${rightTable} 테이블의 ${updateResult.length}개 레코드 업데이트 완료`); newLeftValue,
oldLeftValue,
]);
console.log(
`✅ 연결된 ${rightTable} 테이블의 ${updateResult.length}개 레코드 업데이트 완료`
);
} catch (relError) { } catch (relError) {
console.error("❌ 연결된 테이블 업데이트 실패:", relError); console.error("❌ 연결된 테이블 업데이트 실패:", relError);
// 연결된 테이블 업데이트 실패 시 롤백은 하지 않고 경고만 로그 // 연결된 테이블 업데이트 실패 시 롤백은 하지 않고 경고만 로그
@ -1102,9 +1194,11 @@ class DataService {
if (pkResult.length > 1) { if (pkResult.length > 1) {
// 복합키인 경우: id가 객체여야 함 // 복합키인 경우: id가 객체여야 함
console.log(`🔑 복합키 테이블: ${tableName}, PK: [${pkResult.map(r => r.attname).join(', ')}]`); console.log(
`🔑 복합키 테이블: ${tableName}, PK: [${pkResult.map((r) => r.attname).join(", ")}]`
if (typeof id === 'object' && !Array.isArray(id)) { );
if (typeof id === "object" && !Array.isArray(id)) {
// id가 객체인 경우: { user_id: 'xxx', dept_code: 'yyy' } // id가 객체인 경우: { user_id: 'xxx', dept_code: 'yyy' }
pkResult.forEach((pk, index) => { pkResult.forEach((pk, index) => {
whereClauses.push(`"${pk.attname}" = $${index + 1}`); whereClauses.push(`"${pk.attname}" = $${index + 1}`);
@ -1119,15 +1213,17 @@ class DataService {
// 단일키인 경우 // 단일키인 경우
const pkColumn = pkResult.length > 0 ? pkResult[0].attname : "id"; const pkColumn = pkResult.length > 0 ? pkResult[0].attname : "id";
whereClauses.push(`"${pkColumn}" = $1`); 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); console.log(`🗑️ 삭제 쿼리:`, queryText, params);
const result = await query<any>(queryText, params); const result = await query<any>(queryText, params);
console.log(`✅ 레코드 삭제 완료: ${tableName}, 영향받은 행: ${result.length}`); console.log(
`✅ 레코드 삭제 완료: ${tableName}, 영향받은 행: ${result.length}`
);
return { return {
success: true, success: true,
@ -1166,7 +1262,11 @@ class DataService {
} }
if (whereConditions.length === 0) { if (whereConditions.length === 0) {
return { success: false, message: "삭제 조건이 없습니다.", error: "NO_CONDITIONS" }; return {
success: false,
message: "삭제 조건이 없습니다.",
error: "NO_CONDITIONS",
};
} }
const whereClause = whereConditions.join(" AND "); const whereClause = whereConditions.join(" AND ");
@ -1201,7 +1301,9 @@ class DataService {
records: Array<Record<string, any>>, records: Array<Record<string, any>>,
userCompany?: string, userCompany?: string,
userId?: string userId?: string
): Promise<ServiceResponse<{ inserted: number; updated: number; deleted: number }>> { ): Promise<
ServiceResponse<{ inserted: number; updated: number; deleted: number }>
> {
try { try {
// 테이블 접근 권한 검증 // 테이블 접근 권한 검증
const validation = await this.validateTableAccess(tableName); const validation = await this.validateTableAccess(tableName);
@ -1239,11 +1341,14 @@ class DataService {
const whereClause = whereConditions.join(" AND "); const whereClause = whereConditions.join(" AND ");
const selectQuery = `SELECT * FROM "${tableName}" WHERE ${whereClause}`; 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); const existingRecords = await pool.query(selectQuery, whereValues);
console.log(`✅ 기존 레코드: ${existingRecords.rows.length}`); console.log(`✅ 기존 레코드: ${existingRecords.rows.length}`);
// 2. 새 레코드와 기존 레코드 비교 // 2. 새 레코드와 기존 레코드 비교
@ -1254,50 +1359,53 @@ class DataService {
// 날짜 필드를 YYYY-MM-DD 형식으로 변환하는 헬퍼 함수 // 날짜 필드를 YYYY-MM-DD 형식으로 변환하는 헬퍼 함수
const normalizeDateValue = (value: any): any => { const normalizeDateValue = (value: any): any => {
if (value == null) return value; if (value == null) return value;
// ISO 날짜 문자열 감지 (YYYY-MM-DDTHH:mm:ss.sssZ) // ISO 날짜 문자열 감지 (YYYY-MM-DDTHH:mm:ss.sssZ)
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) { if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return value.split('T')[0]; // YYYY-MM-DD 만 추출 return value.split("T")[0]; // YYYY-MM-DD 만 추출
} }
return value; return value;
}; };
// 새 레코드 처리 (INSERT or UPDATE) // 새 레코드 처리 (INSERT or UPDATE)
for (const newRecord of records) { for (const newRecord of records) {
console.log(`🔍 처리할 새 레코드:`, newRecord); console.log(`🔍 처리할 새 레코드:`, newRecord);
// 날짜 필드 정규화 // 날짜 필드 정규화
const normalizedRecord: Record<string, any> = {}; const normalizedRecord: Record<string, any> = {};
for (const [key, value] of Object.entries(newRecord)) { for (const [key, value] of Object.entries(newRecord)) {
normalizedRecord[key] = normalizeDateValue(value); normalizedRecord[key] = normalizeDateValue(value);
} }
console.log(`🔄 정규화된 레코드:`, normalizedRecord); console.log(`🔄 정규화된 레코드:`, normalizedRecord);
// 전체 레코드 데이터 (parentKeys + normalizedRecord) // 전체 레코드 데이터 (parentKeys + normalizedRecord)
const fullRecord = { ...parentKeys, ...normalizedRecord }; const fullRecord = { ...parentKeys, ...normalizedRecord };
// 고유 키: parentKeys 제외한 나머지 필드들 // 고유 키: parentKeys 제외한 나머지 필드들
const uniqueFields = Object.keys(normalizedRecord); const uniqueFields = Object.keys(normalizedRecord);
console.log(`🔑 고유 필드들:`, uniqueFields); console.log(`🔑 고유 필드들:`, uniqueFields);
// 기존 레코드에서 일치하는 것 찾기 // 기존 레코드에서 일치하는 것 찾기
const existingRecord = existingRecords.rows.find((existing) => { const existingRecord = existingRecords.rows.find((existing) => {
return uniqueFields.every((field) => { return uniqueFields.every((field) => {
const existingValue = existing[field]; const existingValue = existing[field];
const newValue = normalizedRecord[field]; const newValue = normalizedRecord[field];
// null/undefined 처리 // null/undefined 처리
if (existingValue == null && newValue == null) return true; if (existingValue == null && newValue == null) return true;
if (existingValue == null || newValue == null) return false; if (existingValue == null || newValue == null) return false;
// Date 타입 처리 // Date 타입 처리
if (existingValue instanceof Date && typeof newValue === 'string') { if (existingValue instanceof Date && typeof newValue === "string") {
return existingValue.toISOString().split('T')[0] === newValue.split('T')[0]; return (
existingValue.toISOString().split("T")[0] ===
newValue.split("T")[0]
);
} }
// 문자열 비교 // 문자열 비교
return String(existingValue) === String(newValue); return String(existingValue) === String(newValue);
}); });
@ -1310,7 +1418,8 @@ class DataService {
let updateParamIndex = 1; let updateParamIndex = 1;
for (const [key, value] of Object.entries(fullRecord)) { for (const [key, value] of Object.entries(fullRecord)) {
if (key !== pkColumn) { // Primary Key는 업데이트하지 않음 if (key !== pkColumn) {
// Primary Key는 업데이트하지 않음
updateFields.push(`"${key}" = $${updateParamIndex}`); updateFields.push(`"${key}" = $${updateParamIndex}`);
updateValues.push(value); updateValues.push(value);
updateParamIndex++; updateParamIndex++;
@ -1326,36 +1435,42 @@ class DataService {
await pool.query(updateQuery, updateValues); await pool.query(updateQuery, updateValues);
updated++; updated++;
console.log(`✏️ UPDATE: ${pkColumn} = ${existingRecord[pkColumn]}`); console.log(`✏️ UPDATE: ${pkColumn} = ${existingRecord[pkColumn]}`);
} else { } else {
// INSERT: 기존 레코드가 없으면 삽입 // INSERT: 기존 레코드가 없으면 삽입
// 🆕 자동 필드 추가 (company_code, writer, created_date, updated_date, id) // 🆕 자동 필드 추가 (company_code, writer, created_date, updated_date, id)
// created_date는 프론트엔드에서 전달된 값 무시하고 항상 현재 시간 설정
const { created_date: _, ...recordWithoutCreatedDate } = fullRecord;
const recordWithMeta: Record<string, any> = { const recordWithMeta: Record<string, any> = {
...fullRecord, ...recordWithoutCreatedDate,
id: uuidv4(), // 새 ID 생성 id: uuidv4(), // 새 ID 생성
created_date: "NOW()", created_date: "NOW()",
updated_date: "NOW()", updated_date: "NOW()",
}; };
// company_code가 없으면 userCompany 사용 (단, userCompany가 "*"가 아닐 때만) // company_code가 없으면 userCompany 사용 (단, userCompany가 "*"가 아닐 때만)
if (!recordWithMeta.company_code && userCompany && userCompany !== "*") { if (
!recordWithMeta.company_code &&
userCompany &&
userCompany !== "*"
) {
recordWithMeta.company_code = userCompany; recordWithMeta.company_code = userCompany;
} }
// writer가 없으면 userId 사용 // writer가 없으면 userId 사용
if (!recordWithMeta.writer && userId) { if (!recordWithMeta.writer && userId) {
recordWithMeta.writer = userId; recordWithMeta.writer = userId;
} }
const insertFields = Object.keys(recordWithMeta).filter(key => const insertFields = Object.keys(recordWithMeta).filter(
recordWithMeta[key] !== "NOW()" (key) => recordWithMeta[key] !== "NOW()"
); );
const insertPlaceholders: string[] = []; const insertPlaceholders: string[] = [];
const insertValues: any[] = []; const insertValues: any[] = [];
let insertParamIndex = 1; let insertParamIndex = 1;
for (const field of Object.keys(recordWithMeta)) { for (const field of Object.keys(recordWithMeta)) {
if (recordWithMeta[field] === "NOW()") { if (recordWithMeta[field] === "NOW()") {
insertPlaceholders.push("NOW()"); insertPlaceholders.push("NOW()");
@ -1367,15 +1482,20 @@ class DataService {
} }
const insertQuery = ` 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(", ")}) VALUES (${insertPlaceholders.join(", ")})
`; `;
console.log(` INSERT 쿼리:`, { query: insertQuery, values: insertValues }); console.log(` INSERT 쿼리:`, {
query: insertQuery,
values: insertValues,
});
await pool.query(insertQuery, insertValues); await pool.query(insertQuery, insertValues);
inserted++; inserted++;
console.log(` INSERT: 새 레코드`); console.log(` INSERT: 새 레코드`);
} }
} }
@ -1383,19 +1503,22 @@ class DataService {
// 3. 삭제할 레코드 찾기 (기존 레코드 중 새 레코드에 없는 것) // 3. 삭제할 레코드 찾기 (기존 레코드 중 새 레코드에 없는 것)
for (const existingRecord of existingRecords.rows) { for (const existingRecord of existingRecords.rows) {
const uniqueFields = Object.keys(records[0] || {}); const uniqueFields = Object.keys(records[0] || {});
const stillExists = records.some((newRecord) => { const stillExists = records.some((newRecord) => {
return uniqueFields.every((field) => { return uniqueFields.every((field) => {
const existingValue = existingRecord[field]; const existingValue = existingRecord[field];
const newValue = newRecord[field]; const newValue = newRecord[field];
if (existingValue == null && newValue == null) return true; if (existingValue == null && newValue == null) return true;
if (existingValue == null || newValue == null) return false; if (existingValue == null || newValue == null) return false;
if (existingValue instanceof Date && typeof newValue === 'string') { if (existingValue instanceof Date && typeof newValue === "string") {
return existingValue.toISOString().split('T')[0] === newValue.split('T')[0]; return (
existingValue.toISOString().split("T")[0] ===
newValue.split("T")[0]
);
} }
return String(existingValue) === String(newValue); return String(existingValue) === String(newValue);
}); });
}); });
@ -1405,7 +1528,7 @@ class DataService {
const deleteQuery = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`; const deleteQuery = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`;
await pool.query(deleteQuery, [existingRecord[pkColumn]]); await pool.query(deleteQuery, [existingRecord[pkColumn]]);
deleted++; deleted++;
console.log(`🗑️ DELETE: ${pkColumn} = ${existingRecord[pkColumn]}`); console.log(`🗑️ DELETE: ${pkColumn} = ${existingRecord[pkColumn]}`);
} }
} }

View File

@ -103,12 +103,16 @@ export class DynamicFormService {
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
// DATE 타입이면 문자열 그대로 유지 // DATE 타입이면 문자열 그대로 유지
if (lowerDataType === "date") { if (lowerDataType === "date") {
console.log(`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`); console.log(
`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`
);
return value; // 문자열 그대로 반환 return value; // 문자열 그대로 반환
} }
// TIMESTAMP 타입이면 Date 객체로 변환 // TIMESTAMP 타입이면 Date 객체로 변환
else { else {
console.log(`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`); console.log(
`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`
);
return new Date(value + "T00:00:00"); return new Date(value + "T00:00:00");
} }
} }
@ -250,7 +254,8 @@ export class DynamicFormService {
if (tableColumns.includes("regdate") && !dataToInsert.regdate) { if (tableColumns.includes("regdate") && !dataToInsert.regdate) {
dataToInsert.regdate = new Date(); dataToInsert.regdate = new Date();
} }
if (tableColumns.includes("created_date") && !dataToInsert.created_date) { // created_date는 항상 현재 시간으로 설정 (기존 값 무시)
if (tableColumns.includes("created_date")) {
dataToInsert.created_date = new Date(); dataToInsert.created_date = new Date();
} }
if (tableColumns.includes("updated_date") && !dataToInsert.updated_date) { if (tableColumns.includes("updated_date") && !dataToInsert.updated_date) {
@ -313,7 +318,9 @@ export class DynamicFormService {
} }
// YYYY-MM-DD 형태의 문자열은 그대로 유지 (DATE 타입으로 저장) // YYYY-MM-DD 형태의 문자열은 그대로 유지 (DATE 타입으로 저장)
else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) { else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) {
console.log(`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`); console.log(
`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`
);
// dataToInsert[key] = value; // 문자열 그대로 유지 (이미 올바른 형식) // dataToInsert[key] = value; // 문자열 그대로 유지 (이미 올바른 형식)
} }
} }
@ -346,35 +353,37 @@ export class DynamicFormService {
) { ) {
try { try {
parsedArray = JSON.parse(value); parsedArray = JSON.parse(value);
console.log( console.log(
`🔄 JSON 문자열 Repeater 데이터 감지: ${key}, ${parsedArray?.length || 0}개 항목` `🔄 JSON 문자열 Repeater 데이터 감지: ${key}, ${parsedArray?.length || 0}개 항목`
); );
} catch (parseError) { } catch (parseError) {
console.log(`⚠️ JSON 파싱 실패: ${key}`); console.log(`⚠️ JSON 파싱 실패: ${key}`);
} }
} }
// 파싱된 배열이 있으면 처리 // 파싱된 배열이 있으면 처리
if (parsedArray && Array.isArray(parsedArray) && parsedArray.length > 0) { if (
// 컴포넌트 설정에서 targetTable 추출 (컴포넌트 ID를 통해) parsedArray &&
// 프론트엔드에서 { data: [...], targetTable: "..." } 형식으로 전달될 수 있음 Array.isArray(parsedArray) &&
let targetTable: string | undefined; parsedArray.length > 0
let actualData = parsedArray; ) {
// 컴포넌트 설정에서 targetTable 추출 (컴포넌트 ID를 통해)
// 프론트엔드에서 { data: [...], targetTable: "..." } 형식으로 전달될 수 있음
let targetTable: string | undefined;
let actualData = parsedArray;
// 첫 번째 항목에 _targetTable이 있는지 확인 (프론트엔드에서 메타데이터 전달) // 첫 번째 항목에 _targetTable이 있는지 확인 (프론트엔드에서 메타데이터 전달)
if (parsedArray[0] && parsedArray[0]._targetTable) { if (parsedArray[0] && parsedArray[0]._targetTable) {
targetTable = parsedArray[0]._targetTable; targetTable = parsedArray[0]._targetTable;
actualData = parsedArray.map( actualData = parsedArray.map(({ _targetTable, ...item }) => item);
({ _targetTable, ...item }) => item }
);
}
repeaterData.push({ repeaterData.push({
data: actualData, data: actualData,
targetTable, targetTable,
componentId: key, componentId: key,
}); });
delete dataToInsert[key]; // 원본 배열 데이터는 제거 delete dataToInsert[key]; // 원본 배열 데이터는 제거
console.log(`✅ Repeater 데이터 추가: ${key}`, { console.log(`✅ Repeater 데이터 추가: ${key}`, {
targetTable: targetTable || "없음 (화면 설계에서 설정 필요)", targetTable: targetTable || "없음 (화면 설계에서 설정 필요)",
@ -387,8 +396,8 @@ export class DynamicFormService {
// 🔥 Repeater targetTable이 메인 테이블과 같으면 분리해서 저장 // 🔥 Repeater targetTable이 메인 테이블과 같으면 분리해서 저장
const separateRepeaterData: typeof repeaterData = []; const separateRepeaterData: typeof repeaterData = [];
const mergedRepeaterData: typeof repeaterData = []; const mergedRepeaterData: typeof repeaterData = [];
repeaterData.forEach(repeater => { repeaterData.forEach((repeater) => {
if (repeater.targetTable && repeater.targetTable !== tableName) { if (repeater.targetTable && repeater.targetTable !== tableName) {
// 다른 테이블: 나중에 별도 저장 // 다른 테이블: 나중에 별도 저장
separateRepeaterData.push(repeater); separateRepeaterData.push(repeater);
@ -397,10 +406,10 @@ export class DynamicFormService {
mergedRepeaterData.push(repeater); mergedRepeaterData.push(repeater);
} }
}); });
console.log(`🔄 Repeater 데이터 분류:`, { console.log(`🔄 Repeater 데이터 분류:`, {
separate: separateRepeaterData.length, // 별도 테이블 separate: separateRepeaterData.length, // 별도 테이블
merged: mergedRepeaterData.length, // 메인 테이블과 병합 merged: mergedRepeaterData.length, // 메인 테이블과 병합
}); });
// 존재하지 않는 컬럼 제거 // 존재하지 않는 컬럼 제거
@ -494,23 +503,30 @@ export class DynamicFormService {
const clientIp = ipAddress || "unknown"; const clientIp = ipAddress || "unknown";
let result: any[]; let result: any[];
// 🔥 메인 테이블과 병합할 Repeater가 있으면 각 품목별로 INSERT // 🔥 메인 테이블과 병합할 Repeater가 있으면 각 품목별로 INSERT
if (mergedRepeaterData.length > 0) { if (mergedRepeaterData.length > 0) {
console.log(`🔄 메인 테이블 병합 모드: ${mergedRepeaterData.length}개 Repeater를 개별 레코드로 저장`); console.log(
`🔄 메인 테이블 병합 모드: ${mergedRepeaterData.length}개 Repeater를 개별 레코드로 저장`
);
result = []; result = [];
for (const repeater of mergedRepeaterData) { for (const repeater of mergedRepeaterData) {
for (const item of repeater.data) { 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 생성되도록 함 // 🆕 새 레코드 저장 시 id 제거하여 새 UUID 생성되도록 함
// _existingRecord가 명시적으로 true인 경우에만 기존 레코드로 처리 (UPDATE) // _existingRecord가 명시적으로 true인 경우에만 기존 레코드로 처리 (UPDATE)
// 그 외의 경우는 모두 새 레코드로 처리 (INSERT) // 그 외의 경우는 모두 새 레코드로 처리 (INSERT)
const isExistingRecord = rawMergedData._existingRecord === true; const isExistingRecord = rawMergedData._existingRecord === true;
if (!isExistingRecord) { if (!isExistingRecord) {
// 새 레코드: id 제거하여 새 UUID 자동 생성 // 새 레코드: id 제거하여 새 UUID 자동 생성
const oldId = rawMergedData.id; const oldId = rawMergedData.id;
@ -519,37 +535,43 @@ export class DynamicFormService {
} else { } else {
console.log(`📝 기존 레코드 수정 (id 유지: ${rawMergedData.id})`); console.log(`📝 기존 레코드 수정 (id 유지: ${rawMergedData.id})`);
} }
// 메타 플래그 제거 // 메타 플래그 제거
delete rawMergedData._isNewItem; delete rawMergedData._isNewItem;
delete rawMergedData._existingRecord; delete rawMergedData._existingRecord;
// 🆕 실제 테이블 컬럼만 필터링 (조인/계산 컬럼 제외) // 🆕 실제 테이블 컬럼만 필터링 (조인/계산 컬럼 제외)
const validColumnNames = columnInfo.map((col) => col.column_name); const validColumnNames = columnInfo.map((col) => col.column_name);
const mergedData: Record<string, any> = {}; const mergedData: Record<string, any> = {};
Object.keys(rawMergedData).forEach((columnName) => { Object.keys(rawMergedData).forEach((columnName) => {
// 실제 테이블 컬럼인지 확인 // 실제 테이블 컬럼인지 확인
if (validColumnNames.includes(columnName)) { if (validColumnNames.includes(columnName)) {
const column = columnInfo.find((col) => col.column_name === columnName); const column = columnInfo.find(
if (column) { (col) => col.column_name === columnName
// 타입 변환
mergedData[columnName] = this.convertValueForPostgreSQL(
rawMergedData[columnName],
column.data_type
); );
if (column) {
// 타입 변환
mergedData[columnName] = this.convertValueForPostgreSQL(
rawMergedData[columnName],
column.data_type
);
} else { } else {
mergedData[columnName] = rawMergedData[columnName]; mergedData[columnName] = rawMergedData[columnName];
} }
} else { } else {
console.log(`⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})`); console.log(
`⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})`
);
} }
}); });
const mergedColumns = Object.keys(mergedData); const mergedColumns = Object.keys(mergedData);
const mergedValues: any[] = Object.values(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; let mergedUpsertQuery: string;
if (primaryKeys.length > 0) { if (primaryKeys.length > 0) {
const conflictColumns = primaryKeys.join(", "); const conflictColumns = primaryKeys.join(", ");
@ -557,7 +579,7 @@ export class DynamicFormService {
.filter((col) => !primaryKeys.includes(col)) .filter((col) => !primaryKeys.includes(col))
.map((col) => `${col} = EXCLUDED.${col}`) .map((col) => `${col} = EXCLUDED.${col}`)
.join(", "); .join(", ");
mergedUpsertQuery = updateSet mergedUpsertQuery = updateSet
? `INSERT INTO ${tableName} (${mergedColumns.join(", ")}) ? `INSERT INTO ${tableName} (${mergedColumns.join(", ")})
VALUES (${mergedPlaceholders}) VALUES (${mergedPlaceholders})
@ -574,20 +596,20 @@ export class DynamicFormService {
VALUES (${mergedPlaceholders}) VALUES (${mergedPlaceholders})
RETURNING *`; RETURNING *`;
} }
console.log(`📝 병합 INSERT:`, { mergedData }); console.log(`📝 병합 INSERT:`, { mergedData });
const itemResult = await transaction(async (client) => { const itemResult = await transaction(async (client) => {
await client.query(`SET LOCAL app.user_id = '${userId}'`); await client.query(`SET LOCAL app.user_id = '${userId}'`);
await client.query(`SET LOCAL app.ip_address = '${clientIp}'`); await client.query(`SET LOCAL app.ip_address = '${clientIp}'`);
const res = await client.query(mergedUpsertQuery, mergedValues); const res = await client.query(mergedUpsertQuery, mergedValues);
return res.rows[0]; return res.rows[0];
}); });
result.push(itemResult); result.push(itemResult);
} }
} }
console.log(`✅ 병합 저장 완료: ${result.length}개 레코드`); console.log(`✅ 병합 저장 완료: ${result.length}개 레코드`);
} else { } else {
// 일반 모드: 헤더만 저장 // 일반 모드: 헤더만 저장
@ -597,7 +619,7 @@ export class DynamicFormService {
const res = await client.query(upsertQuery, values); const res = await client.query(upsertQuery, values);
return res.rows; return res.rows;
}); });
console.log("✅ 서비스: 실제 테이블 저장 성공:", result); console.log("✅ 서비스: 실제 테이블 저장 성공:", result);
} }
@ -843,10 +865,10 @@ export class DynamicFormService {
FROM information_schema.columns FROM information_schema.columns
WHERE table_name = $1 AND table_schema = 'public' WHERE table_name = $1 AND table_schema = 'public'
`; `;
const columnTypesResult = await query<{ column_name: string; data_type: string }>( const columnTypesResult = await query<{
columnTypesQuery, column_name: string;
[tableName] data_type: string;
); }>(columnTypesQuery, [tableName]);
const columnTypes: Record<string, string> = {}; const columnTypes: Record<string, string> = {};
columnTypesResult.forEach((row) => { columnTypesResult.forEach((row) => {
columnTypes[row.column_name] = row.data_type; columnTypes[row.column_name] = row.data_type;
@ -859,11 +881,20 @@ export class DynamicFormService {
.map((key, index) => { .map((key, index) => {
const dataType = columnTypes[key]; const dataType = columnTypes[key];
// 숫자 타입인 경우 명시적 캐스팅 // 숫자 타입인 경우 명시적 캐스팅
if (dataType === 'integer' || dataType === 'bigint' || dataType === 'smallint') { if (
dataType === "integer" ||
dataType === "bigint" ||
dataType === "smallint"
) {
return `${key} = $${index + 1}::integer`; 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`; return `${key} = $${index + 1}::numeric`;
} else if (dataType === 'boolean') { } else if (dataType === "boolean") {
return `${key} = $${index + 1}::boolean`; return `${key} = $${index + 1}::boolean`;
} else { } else {
// 문자열 타입은 캐스팅 불필요 // 문자열 타입은 캐스팅 불필요
@ -877,13 +908,17 @@ export class DynamicFormService {
// 🔑 Primary Key 타입에 맞게 캐스팅 // 🔑 Primary Key 타입에 맞게 캐스팅
const pkDataType = columnTypes[primaryKeyColumn]; const pkDataType = columnTypes[primaryKeyColumn];
let pkCast = ''; let pkCast = "";
if (pkDataType === 'integer' || pkDataType === 'bigint' || pkDataType === 'smallint') { if (
pkCast = '::integer'; pkDataType === "integer" ||
} else if (pkDataType === 'numeric' || pkDataType === 'decimal') { pkDataType === "bigint" ||
pkCast = '::numeric'; pkDataType === "smallint"
} else if (pkDataType === 'uuid') { ) {
pkCast = '::uuid'; pkCast = "::integer";
} else if (pkDataType === "numeric" || pkDataType === "decimal") {
pkCast = "::numeric";
} else if (pkDataType === "uuid") {
pkCast = "::uuid";
} }
// text, varchar 등은 캐스팅 불필요 // text, varchar 등은 캐스팅 불필요
@ -1556,9 +1591,11 @@ export class DynamicFormService {
componentId: layout.component_id, componentId: layout.component_id,
componentType: properties?.componentType, componentType: properties?.componentType,
actionType: properties?.componentConfig?.action?.type, actionType: properties?.componentConfig?.action?.type,
enableDataflowControl: properties?.webTypeConfig?.enableDataflowControl, enableDataflowControl:
properties?.webTypeConfig?.enableDataflowControl,
hasDataflowConfig: !!properties?.webTypeConfig?.dataflowConfig, hasDataflowConfig: !!properties?.webTypeConfig?.dataflowConfig,
hasDiagramId: !!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId, hasDiagramId:
!!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId,
}); });
// 버튼 컴포넌트이고 저장 액션이며 제어관리가 활성화된 경우 // 버튼 컴포넌트이고 저장 액션이며 제어관리가 활성화된 경우
@ -1583,21 +1620,26 @@ export class DynamicFormService {
// 노드 플로우 실행 (relationshipId가 없는 경우 노드 플로우로 간주) // 노드 플로우 실행 (relationshipId가 없는 경우 노드 플로우로 간주)
let controlResult: any; let controlResult: any;
if (!relationshipId) { if (!relationshipId) {
// 노드 플로우 실행 // 노드 플로우 실행
console.log(`🚀 노드 플로우 실행 (flowId: ${diagramId})`); console.log(`🚀 노드 플로우 실행 (flowId: ${diagramId})`);
const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService"); const { NodeFlowExecutionService } = await import(
"./nodeFlowExecutionService"
const executionResult = await NodeFlowExecutionService.executeFlow(diagramId, { );
sourceData: [savedData],
dataSourceType: "formData", const executionResult = await NodeFlowExecutionService.executeFlow(
buttonId: "save-button", diagramId,
screenId: screenId, {
userId: userId, sourceData: [savedData],
formData: savedData, dataSourceType: "formData",
}); buttonId: "save-button",
screenId: screenId,
userId: userId,
formData: savedData,
}
);
controlResult = { controlResult = {
success: executionResult.success, success: executionResult.success,
message: executionResult.message, message: executionResult.message,
@ -1612,15 +1654,18 @@ export class DynamicFormService {
}; };
} else { } else {
// 관계 기반 제어관리 실행 // 관계 기반 제어관리 실행
console.log(`🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})`); console.log(
controlResult = await this.dataflowControlService.executeDataflowControl( `🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})`
diagramId,
relationshipId,
triggerType,
savedData,
tableName,
userId
); );
controlResult =
await this.dataflowControlService.executeDataflowControl(
diagramId,
relationshipId,
triggerType,
savedData,
tableName,
userId
);
} }
console.log(`🎯 제어관리 실행 결과:`, controlResult); console.log(`🎯 제어관리 실행 결과:`, controlResult);
@ -1677,7 +1722,7 @@ export class DynamicFormService {
): Promise<{ affectedRows: number }> { ): Promise<{ affectedRows: number }> {
const pool = getPool(); const pool = getPool();
const client = await pool.connect(); const client = await pool.connect();
try { try {
console.log("🔄 [updateFieldValue] 업데이트 실행:", { console.log("🔄 [updateFieldValue] 업데이트 실행:", {
tableName, tableName,
@ -1695,11 +1740,13 @@ export class DynamicFormService {
WHERE table_name = $1 AND column_name IN ('updated_by', 'updated_at', 'company_code') WHERE table_name = $1 AND column_name IN ('updated_by', 'updated_at', 'company_code')
`; `;
const columnResult = await client.query(columnQuery, [tableName]); const columnResult = await client.query(columnQuery, [tableName]);
const existingColumns = columnResult.rows.map((row: any) => row.column_name); 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 hasUpdatedBy = existingColumns.includes("updated_by");
const hasUpdatedAt = existingColumns.includes("updated_at");
const hasCompanyCode = existingColumns.includes("company_code");
console.log("🔍 [updateFieldValue] 테이블 컬럼 확인:", { console.log("🔍 [updateFieldValue] 테이블 컬럼 확인:", {
hasUpdatedBy, hasUpdatedBy,
@ -1896,7 +1943,8 @@ export class DynamicFormService {
paramIndex++; 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 limitClause = params.limit ? `LIMIT ${params.limit}` : "LIMIT 1000";
const sqlQuery = ` const sqlQuery = `

View File

@ -292,9 +292,10 @@ export class EnhancedFormService {
} }
// 시스템 필드 자동 추가 // 시스템 필드 자동 추가
const now = new Date().toISOString(); // created_date는 백엔드에서 처리하도록 프론트엔드에서 제거
if (!transformed.created_date && tableColumns.some((col) => col.columnName === "created_date")) { // (기존 데이터 조회 시 포함된 created_date가 그대로 전송되는 문제 방지)
transformed.created_date = now; if (tableColumns.some((col) => col.columnName === "created_date")) {
delete transformed.created_date;
} }
if (!transformed.updated_date && tableColumns.some((col) => col.columnName === "updated_date")) { if (!transformed.updated_date && tableColumns.some((col) => col.columnName === "updated_date")) {
transformed.updated_date = now; transformed.updated_date = now;