우측 패널 일괄삭제 기능
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
import { query, queryOne } from "../database/db";
|
||||
import { pool } from "../database/db"; // 🆕 Entity 조인을 위한 pool import
|
||||
import { buildDataFilterWhereClause } from "../utils/dataFilterUtil"; // 🆕 데이터 필터 유틸
|
||||
import { v4 as uuidv4 } from "uuid"; // 🆕 UUID 생성
|
||||
|
||||
interface GetTableDataParams {
|
||||
tableName: string;
|
||||
@@ -530,7 +531,27 @@ class DataService {
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`✅ Entity Join 데이터 조회 성공:`, result.rows[0]);
|
||||
// 🔧 날짜 타입 타임존 문제 해결: Date 객체를 YYYY-MM-DD 문자열로 변환
|
||||
const normalizeDates = (rows: any[]) => {
|
||||
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');
|
||||
normalized[key] = `${year}-${month}-${day}`;
|
||||
} else {
|
||||
normalized[key] = value;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
};
|
||||
|
||||
const normalizedRows = normalizeDates(result.rows);
|
||||
console.log(`✅ Entity Join 데이터 조회 성공 (날짜 정규화됨):`, normalizedRows[0]);
|
||||
|
||||
// 🆕 groupByColumns가 있으면 그룹핑 기반 다중 레코드 조회
|
||||
if (groupByColumns.length > 0) {
|
||||
@@ -542,7 +563,7 @@ class DataService {
|
||||
let paramIndex = 1;
|
||||
|
||||
for (const col of groupByColumns) {
|
||||
const value = baseRecord[col];
|
||||
const value = normalizedRows[0][col];
|
||||
if (value !== undefined && value !== null) {
|
||||
groupConditions.push(`main."${col}" = $${paramIndex}`);
|
||||
groupValues.push(value);
|
||||
@@ -565,18 +586,19 @@ class DataService {
|
||||
|
||||
const groupResult = await pool.query(groupQuery, groupValues);
|
||||
|
||||
console.log(`✅ 그룹 레코드 조회 성공: ${groupResult.rows.length}개`);
|
||||
const normalizedGroupRows = normalizeDates(groupResult.rows);
|
||||
console.log(`✅ 그룹 레코드 조회 성공: ${normalizedGroupRows.length}개`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: groupResult.rows, // 🔧 배열로 반환!
|
||||
data: normalizedGroupRows, // 🔧 배열로 반환!
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.rows[0], // 그룹핑 없으면 단일 레코드
|
||||
data: normalizedRows[0], // 그룹핑 없으면 단일 레코드
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -755,14 +777,33 @@ class DataService {
|
||||
|
||||
const result = await pool.query(finalQuery, values);
|
||||
|
||||
console.log(`✅ Entity 조인 성공! 반환된 데이터 개수: ${result.rows.length}개`);
|
||||
// 🔧 날짜 타입 타임존 문제 해결
|
||||
const normalizeDates = (rows: any[]) => {
|
||||
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');
|
||||
normalized[key] = `${year}-${month}-${day}`;
|
||||
} else {
|
||||
normalized[key] = value;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
};
|
||||
|
||||
const normalizedRows = normalizeDates(result.rows);
|
||||
console.log(`✅ Entity 조인 성공! 반환된 데이터 개수: ${normalizedRows.length}개 (날짜 정규화됨)`);
|
||||
|
||||
// 🆕 중복 제거 처리
|
||||
let finalData = result.rows;
|
||||
let finalData = normalizedRows;
|
||||
if (deduplication?.enabled && deduplication.groupByColumn) {
|
||||
console.log(`🔄 중복 제거 시작: 기준 컬럼 = ${deduplication.groupByColumn}, 전략 = ${deduplication.keepStrategy}`);
|
||||
finalData = this.deduplicateData(result.rows, deduplication);
|
||||
console.log(`✅ 중복 제거 완료: ${result.rows.length}개 → ${finalData.length}개`);
|
||||
finalData = this.deduplicateData(normalizedRows, deduplication);
|
||||
console.log(`✅ 중복 제거 완료: ${normalizedRows.length}개 → ${finalData.length}개`);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1063,6 +1104,53 @@ class DataService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건에 맞는 모든 레코드 삭제 (그룹 삭제)
|
||||
*/
|
||||
async deleteGroupRecords(
|
||||
tableName: string,
|
||||
filterConditions: Record<string, any>
|
||||
): Promise<ServiceResponse<{ deleted: number }>> {
|
||||
try {
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
if (!validation.valid) {
|
||||
return validation.error!;
|
||||
}
|
||||
|
||||
const whereConditions: string[] = [];
|
||||
const whereValues: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
for (const [key, value] of Object.entries(filterConditions)) {
|
||||
whereConditions.push(`"${key}" = $${paramIndex}`);
|
||||
whereValues.push(value);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (whereConditions.length === 0) {
|
||||
return { success: false, message: "삭제 조건이 없습니다.", error: "NO_CONDITIONS" };
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(" AND ");
|
||||
const deleteQuery = `DELETE FROM "${tableName}" WHERE ${whereClause} RETURNING *`;
|
||||
|
||||
console.log(`🗑️ 그룹 삭제:`, { tableName, conditions: filterConditions });
|
||||
|
||||
const result = await pool.query(deleteQuery, whereValues);
|
||||
|
||||
console.log(`✅ 그룹 삭제 성공: ${result.rowCount}개`);
|
||||
|
||||
return { success: true, data: { deleted: result.rowCount || 0 } };
|
||||
} catch (error) {
|
||||
console.error("그룹 삭제 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "그룹 삭제 실패",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹화된 데이터 UPSERT
|
||||
* - 부모 키(예: customer_id, item_id)와 레코드 배열을 받아
|
||||
@@ -1072,27 +1160,27 @@ class DataService {
|
||||
async upsertGroupedRecords(
|
||||
tableName: string,
|
||||
parentKeys: Record<string, any>,
|
||||
records: Array<Record<string, any>>
|
||||
records: Array<Record<string, any>>,
|
||||
userCompany?: string,
|
||||
userId?: string
|
||||
): Promise<ServiceResponse<{ inserted: number; updated: number; deleted: number }>> {
|
||||
try {
|
||||
// 테이블 접근 권한 검증
|
||||
if (!this.canAccessTable(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `테이블 '${tableName}'에 접근할 수 없습니다.`,
|
||||
error: "ACCESS_DENIED",
|
||||
};
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
if (!validation.valid) {
|
||||
return validation.error!;
|
||||
}
|
||||
|
||||
// Primary Key 감지
|
||||
const pkColumn = await this.detectPrimaryKey(tableName);
|
||||
if (!pkColumn) {
|
||||
const pkColumns = await this.getPrimaryKeyColumns(tableName);
|
||||
if (!pkColumns || pkColumns.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: `테이블 '${tableName}'의 Primary Key를 찾을 수 없습니다.`,
|
||||
error: "PRIMARY_KEY_NOT_FOUND",
|
||||
};
|
||||
}
|
||||
const pkColumn = pkColumns[0]; // 첫 번째 PK 사용
|
||||
|
||||
console.log(`🔍 UPSERT 시작: ${tableName}`, {
|
||||
parentKeys,
|
||||
@@ -1125,19 +1213,37 @@ class DataService {
|
||||
let updated = 0;
|
||||
let deleted = 0;
|
||||
|
||||
// 날짜 필드를 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 만 추출
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
// 새 레코드 처리 (INSERT or UPDATE)
|
||||
for (const newRecord of records) {
|
||||
// 전체 레코드 데이터 (parentKeys + newRecord)
|
||||
const fullRecord = { ...parentKeys, ...newRecord };
|
||||
// 날짜 필드 정규화
|
||||
const normalizedRecord: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(newRecord)) {
|
||||
normalizedRecord[key] = normalizeDateValue(value);
|
||||
}
|
||||
|
||||
// 전체 레코드 데이터 (parentKeys + normalizedRecord)
|
||||
const fullRecord = { ...parentKeys, ...normalizedRecord };
|
||||
|
||||
// 고유 키: parentKeys 제외한 나머지 필드들
|
||||
const uniqueFields = Object.keys(newRecord);
|
||||
const uniqueFields = Object.keys(normalizedRecord);
|
||||
|
||||
// 기존 레코드에서 일치하는 것 찾기
|
||||
const existingRecord = existingRecords.rows.find((existing) => {
|
||||
return uniqueFields.every((field) => {
|
||||
const existingValue = existing[field];
|
||||
const newValue = newRecord[field];
|
||||
const newValue = normalizedRecord[field];
|
||||
|
||||
// null/undefined 처리
|
||||
if (existingValue == null && newValue == null) return true;
|
||||
@@ -1180,15 +1286,49 @@ class DataService {
|
||||
console.log(`✏️ UPDATE: ${pkColumn} = ${existingRecord[pkColumn]}`);
|
||||
} else {
|
||||
// INSERT: 기존 레코드가 없으면 삽입
|
||||
const insertFields = Object.keys(fullRecord);
|
||||
const insertPlaceholders = insertFields.map((_, idx) => `$${idx + 1}`);
|
||||
const insertValues = Object.values(fullRecord);
|
||||
|
||||
// 🆕 자동 필드 추가 (company_code, writer, created_date, updated_date, id)
|
||||
const recordWithMeta: Record<string, any> = {
|
||||
...fullRecord,
|
||||
id: uuidv4(), // 새 ID 생성
|
||||
created_date: "NOW()",
|
||||
updated_date: "NOW()",
|
||||
};
|
||||
|
||||
// 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 insertPlaceholders: string[] = [];
|
||||
const insertValues: any[] = [];
|
||||
let insertParamIndex = 1;
|
||||
|
||||
for (const field of Object.keys(recordWithMeta)) {
|
||||
if (recordWithMeta[field] === "NOW()") {
|
||||
insertPlaceholders.push("NOW()");
|
||||
} else {
|
||||
insertPlaceholders.push(`$${insertParamIndex}`);
|
||||
insertValues.push(recordWithMeta[field]);
|
||||
insertParamIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
const insertQuery = `
|
||||
INSERT INTO "${tableName}" (${insertFields.map(f => `"${f}"`).join(", ")})
|
||||
INSERT INTO "${tableName}" (${Object.keys(recordWithMeta).map(f => `"${f}"`).join(", ")})
|
||||
VALUES (${insertPlaceholders.join(", ")})
|
||||
`;
|
||||
|
||||
console.log(`➕ INSERT 쿼리:`, { query: insertQuery, values: insertValues });
|
||||
|
||||
await pool.query(insertQuery, insertValues);
|
||||
inserted++;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user