Merge branch 'main' into feature/screen-management
This commit is contained in:
@@ -24,10 +24,20 @@ export class DashboardService {
|
||||
const dashboardId = uuidv4();
|
||||
const now = new Date();
|
||||
|
||||
console.log("🔍 [createDashboard] 받은 데이터:", {
|
||||
title: data.title,
|
||||
settings: data.settings,
|
||||
settingsType: typeof data.settings,
|
||||
settingsStringified: JSON.stringify(data.settings || {}),
|
||||
});
|
||||
|
||||
try {
|
||||
// 트랜잭션으로 대시보드와 요소들을 함께 생성
|
||||
const result = await PostgreSQLService.transaction(async (client) => {
|
||||
// 1. 대시보드 메인 정보 저장
|
||||
const settingsJson = JSON.stringify(data.settings || {});
|
||||
console.log("🔍 [createDashboard] DB INSERT settings:", settingsJson);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO dashboards (
|
||||
@@ -46,7 +56,7 @@ export class DashboardService {
|
||||
JSON.stringify(data.tags || []),
|
||||
data.category || null,
|
||||
0,
|
||||
JSON.stringify(data.settings || {}),
|
||||
settingsJson,
|
||||
companyCode || "DEFAULT",
|
||||
]
|
||||
);
|
||||
@@ -351,6 +361,13 @@ export class DashboardService {
|
||||
|
||||
const dashboard = dashboardResult.rows[0];
|
||||
|
||||
// 🔍 디버깅: settings 원본 확인
|
||||
console.log("🔍 [getDashboardById] dashboard.settings 원본:", {
|
||||
type: typeof dashboard.settings,
|
||||
value: dashboard.settings,
|
||||
raw: JSON.stringify(dashboard.settings),
|
||||
});
|
||||
|
||||
// 2. 대시보드 요소들 조회
|
||||
const elementsQuery = `
|
||||
SELECT * FROM dashboard_elements
|
||||
@@ -400,7 +417,21 @@ export class DashboardService {
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
// 🔍 디버깅: settings 파싱
|
||||
let parsedSettings = undefined;
|
||||
if (dashboard.settings) {
|
||||
if (typeof dashboard.settings === 'string') {
|
||||
parsedSettings = JSON.parse(dashboard.settings);
|
||||
console.log("🔍 [getDashboardById] settings 문자열 파싱:", parsedSettings);
|
||||
} else {
|
||||
parsedSettings = dashboard.settings;
|
||||
console.log("🔍 [getDashboardById] settings 이미 객체:", parsedSettings);
|
||||
}
|
||||
} else {
|
||||
console.log("🔍 [getDashboardById] settings 없음 (null/undefined)");
|
||||
}
|
||||
|
||||
const result = {
|
||||
id: dashboard.id,
|
||||
title: dashboard.title,
|
||||
description: dashboard.description,
|
||||
@@ -412,9 +443,13 @@ export class DashboardService {
|
||||
tags: JSON.parse(dashboard.tags || "[]"),
|
||||
category: dashboard.category,
|
||||
viewCount: parseInt(dashboard.view_count || "0"),
|
||||
settings: dashboard.settings || undefined,
|
||||
settings: parsedSettings,
|
||||
elements,
|
||||
};
|
||||
|
||||
console.log("🔍 [getDashboardById] 최종 반환 settings:", result.settings);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Dashboard get error:", error);
|
||||
throw error;
|
||||
@@ -477,8 +512,13 @@ export class DashboardService {
|
||||
paramIndex++;
|
||||
}
|
||||
if (data.settings !== undefined) {
|
||||
const settingsJson = JSON.stringify(data.settings);
|
||||
console.log("🔍 [updateDashboard] DB UPDATE settings:", {
|
||||
original: data.settings,
|
||||
stringified: settingsJson,
|
||||
});
|
||||
updateFields.push(`settings = $${paramIndex}`);
|
||||
updateParams.push(JSON.stringify(data.settings));
|
||||
updateParams.push(settingsJson);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
|
||||
@@ -320,19 +320,34 @@ export class DynamicFormService {
|
||||
Object.keys(dataToInsert).forEach((key) => {
|
||||
const value = dataToInsert[key];
|
||||
|
||||
// RepeaterInput 데이터인지 확인 (JSON 배열 문자열)
|
||||
if (
|
||||
// 🔥 RepeaterInput 데이터인지 확인 (배열 객체 또는 JSON 문자열)
|
||||
let parsedArray: any[] | null = null;
|
||||
|
||||
// 1️⃣ 이미 배열 객체인 경우 (ModalRepeaterTable, SelectedItemsDetailInput 등)
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
parsedArray = value;
|
||||
console.log(
|
||||
`🔄 배열 객체 Repeater 데이터 감지: ${key}, ${parsedArray.length}개 항목`
|
||||
);
|
||||
}
|
||||
// 2️⃣ JSON 문자열인 경우 (레거시 RepeaterInput)
|
||||
else if (
|
||||
typeof value === "string" &&
|
||||
value.trim().startsWith("[") &&
|
||||
value.trim().endsWith("]")
|
||||
) {
|
||||
try {
|
||||
const parsedArray = JSON.parse(value);
|
||||
if (Array.isArray(parsedArray) && parsedArray.length > 0) {
|
||||
parsedArray = JSON.parse(value);
|
||||
console.log(
|
||||
`🔄 RepeaterInput 데이터 감지: ${key}, ${parsedArray.length}개 항목`
|
||||
`🔄 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;
|
||||
@@ -352,13 +367,34 @@ export class DynamicFormService {
|
||||
componentId: key,
|
||||
});
|
||||
delete dataToInsert[key]; // 원본 배열 데이터는 제거
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.log(`⚠️ JSON 파싱 실패: ${key}`);
|
||||
}
|
||||
|
||||
console.log(`✅ Repeater 데이터 추가: ${key}`, {
|
||||
targetTable: targetTable || "없음 (화면 설계에서 설정 필요)",
|
||||
itemCount: actualData.length,
|
||||
firstItem: actualData[0],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 🔥 Repeater targetTable이 메인 테이블과 같으면 분리해서 저장
|
||||
const separateRepeaterData: typeof repeaterData = [];
|
||||
const mergedRepeaterData: typeof repeaterData = [];
|
||||
|
||||
repeaterData.forEach(repeater => {
|
||||
if (repeater.targetTable && repeater.targetTable !== tableName) {
|
||||
// 다른 테이블: 나중에 별도 저장
|
||||
separateRepeaterData.push(repeater);
|
||||
} else {
|
||||
// 같은 테이블: 메인 INSERT와 병합 (헤더+품목을 한 번에)
|
||||
mergedRepeaterData.push(repeater);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`🔄 Repeater 데이터 분류:`, {
|
||||
separate: separateRepeaterData.length, // 별도 테이블
|
||||
merged: mergedRepeaterData.length, // 메인 테이블과 병합
|
||||
});
|
||||
|
||||
// 존재하지 않는 컬럼 제거
|
||||
Object.keys(dataToInsert).forEach((key) => {
|
||||
if (!tableColumns.includes(key)) {
|
||||
@@ -369,9 +405,6 @@ export class DynamicFormService {
|
||||
}
|
||||
});
|
||||
|
||||
// RepeaterInput 데이터 처리 로직은 메인 저장 후에 처리
|
||||
// (각 Repeater가 다른 테이블에 저장될 수 있으므로)
|
||||
|
||||
console.log("🎯 실제 테이블에 삽입할 데이터:", {
|
||||
tableName,
|
||||
dataToInsert,
|
||||
@@ -452,28 +485,95 @@ export class DynamicFormService {
|
||||
const userId = data.updated_by || data.created_by || "system";
|
||||
const clientIp = ipAddress || "unknown";
|
||||
|
||||
const result = await transaction(async (client) => {
|
||||
// 세션 변수 설정
|
||||
await client.query(`SET LOCAL app.user_id = '${userId}'`);
|
||||
await client.query(`SET LOCAL app.ip_address = '${clientIp}'`);
|
||||
|
||||
// UPSERT 실행
|
||||
const res = await client.query(upsertQuery, values);
|
||||
return res.rows;
|
||||
});
|
||||
|
||||
console.log("✅ 서비스: 실제 테이블 저장 성공:", result);
|
||||
let result: any[];
|
||||
|
||||
// 🔥 메인 테이블과 병합할 Repeater가 있으면 각 품목별로 INSERT
|
||||
if (mergedRepeaterData.length > 0) {
|
||||
console.log(`🔄 메인 테이블 병합 모드: ${mergedRepeaterData.length}개 Repeater를 개별 레코드로 저장`);
|
||||
|
||||
result = [];
|
||||
|
||||
for (const repeater of mergedRepeaterData) {
|
||||
for (const item of repeater.data) {
|
||||
// 헤더 + 품목을 병합
|
||||
const mergedData = { ...dataToInsert, ...item };
|
||||
|
||||
// 타입 변환
|
||||
Object.keys(mergedData).forEach((columnName) => {
|
||||
const column = columnInfo.find((col) => col.column_name === columnName);
|
||||
if (column) {
|
||||
mergedData[columnName] = this.convertValueForPostgreSQL(
|
||||
mergedData[columnName],
|
||||
column.data_type
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const mergedColumns = Object.keys(mergedData);
|
||||
const mergedValues: any[] = Object.values(mergedData);
|
||||
const mergedPlaceholders = mergedValues.map((_, index) => `$${index + 1}`).join(", ");
|
||||
|
||||
let mergedUpsertQuery: string;
|
||||
if (primaryKeys.length > 0) {
|
||||
const conflictColumns = primaryKeys.join(", ");
|
||||
const updateSet = mergedColumns
|
||||
.filter((col) => !primaryKeys.includes(col))
|
||||
.map((col) => `${col} = EXCLUDED.${col}`)
|
||||
.join(", ");
|
||||
|
||||
mergedUpsertQuery = updateSet
|
||||
? `INSERT INTO ${tableName} (${mergedColumns.join(", ")})
|
||||
VALUES (${mergedPlaceholders})
|
||||
ON CONFLICT (${conflictColumns})
|
||||
DO UPDATE SET ${updateSet}
|
||||
RETURNING *`
|
||||
: `INSERT INTO ${tableName} (${mergedColumns.join(", ")})
|
||||
VALUES (${mergedPlaceholders})
|
||||
ON CONFLICT (${conflictColumns})
|
||||
DO NOTHING
|
||||
RETURNING *`;
|
||||
} else {
|
||||
mergedUpsertQuery = `INSERT INTO ${tableName} (${mergedColumns.join(", ")})
|
||||
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 {
|
||||
// 일반 모드: 헤더만 저장
|
||||
result = 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(upsertQuery, values);
|
||||
return res.rows;
|
||||
});
|
||||
|
||||
console.log("✅ 서비스: 실제 테이블 저장 성공:", result);
|
||||
}
|
||||
|
||||
// 결과를 표준 형식으로 변환
|
||||
const insertedRecord = Array.isArray(result) ? result[0] : result;
|
||||
|
||||
// 📝 RepeaterInput 데이터 저장 (각 Repeater를 해당 테이블에 저장)
|
||||
if (repeaterData.length > 0) {
|
||||
// 📝 별도 테이블 Repeater 데이터 저장
|
||||
if (separateRepeaterData.length > 0) {
|
||||
console.log(
|
||||
`🔄 RepeaterInput 데이터 저장 시작: ${repeaterData.length}개 Repeater`
|
||||
`🔄 별도 테이블 Repeater 저장 시작: ${separateRepeaterData.length}개`
|
||||
);
|
||||
|
||||
for (const repeater of repeaterData) {
|
||||
for (const repeater of separateRepeaterData) {
|
||||
const targetTableName = repeater.targetTable || tableName;
|
||||
console.log(
|
||||
`📝 Repeater "${repeater.componentId}" → 테이블 "${targetTableName}"에 ${repeater.data.length}개 항목 저장`
|
||||
@@ -497,8 +597,13 @@ export class DynamicFormService {
|
||||
created_by,
|
||||
updated_by,
|
||||
regdate: new Date(),
|
||||
// 🔥 멀티테넌시: company_code 필수 추가
|
||||
company_code: data.company_code || company_code,
|
||||
};
|
||||
|
||||
// 🔥 별도 테이블인 경우에만 외래키 추가
|
||||
// (같은 테이블이면 이미 병합 모드에서 처리됨)
|
||||
|
||||
// 대상 테이블에 존재하는 컬럼만 필터링
|
||||
Object.keys(itemData).forEach((key) => {
|
||||
if (!targetColumnNames.includes(key)) {
|
||||
|
||||
Reference in New Issue
Block a user