feat(universal-form-modal): 범용 다중 테이블 저장 기능 추가
This commit is contained in:
@@ -1811,3 +1811,299 @@ export async function getCategoryColumnsByMenu(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 범용 다중 테이블 저장 API
|
||||
*
|
||||
* 메인 테이블과 서브 테이블(들)에 트랜잭션으로 데이터를 저장합니다.
|
||||
*
|
||||
* 요청 본문:
|
||||
* {
|
||||
* mainTable: { tableName: string, primaryKeyColumn: string },
|
||||
* mainData: Record<string, any>,
|
||||
* subTables: Array<{
|
||||
* tableName: string,
|
||||
* linkColumn: { mainField: string, subColumn: string },
|
||||
* items: Record<string, any>[],
|
||||
* options?: {
|
||||
* saveMainAsFirst?: boolean,
|
||||
* mainFieldMappings?: Array<{ formField: string, targetColumn: string }>,
|
||||
* mainMarkerColumn?: string,
|
||||
* mainMarkerValue?: any,
|
||||
* subMarkerValue?: any,
|
||||
* deleteExistingBefore?: boolean,
|
||||
* }
|
||||
* }>,
|
||||
* isUpdate?: boolean
|
||||
* }
|
||||
*/
|
||||
export async function multiTableSave(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
const pool = require("../database/db").getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const { mainTable, mainData, subTables, isUpdate } = req.body;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
logger.info("=== 다중 테이블 저장 시작 ===", {
|
||||
mainTable,
|
||||
mainDataKeys: Object.keys(mainData || {}),
|
||||
subTablesCount: subTables?.length || 0,
|
||||
isUpdate,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
// 유효성 검사
|
||||
if (!mainTable?.tableName || !mainTable?.primaryKeyColumn) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "메인 테이블 설정이 올바르지 않습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mainData || Object.keys(mainData).length === 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "저장할 메인 데이터가 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. 메인 테이블 저장
|
||||
const mainTableName = mainTable.tableName;
|
||||
const pkColumn = mainTable.primaryKeyColumn;
|
||||
const pkValue = mainData[pkColumn];
|
||||
|
||||
// company_code 자동 추가 (최고 관리자가 아닌 경우)
|
||||
if (companyCode !== "*" && !mainData.company_code) {
|
||||
mainData.company_code = companyCode;
|
||||
}
|
||||
|
||||
let mainResult: any;
|
||||
|
||||
if (isUpdate && pkValue) {
|
||||
// UPDATE
|
||||
const updateColumns = Object.keys(mainData)
|
||||
.filter(col => col !== pkColumn)
|
||||
.map((col, idx) => `"${col}" = $${idx + 1}`)
|
||||
.join(", ");
|
||||
const updateValues = Object.keys(mainData)
|
||||
.filter(col => col !== pkColumn)
|
||||
.map(col => mainData[col]);
|
||||
|
||||
// updated_at 컬럼 존재 여부 확인
|
||||
const hasUpdatedAt = await client.query(`
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = $1 AND column_name = 'updated_at'
|
||||
`, [mainTableName]);
|
||||
const updatedAtClause = hasUpdatedAt.rowCount && hasUpdatedAt.rowCount > 0 ? ", updated_at = NOW()" : "";
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE "${mainTableName}"
|
||||
SET ${updateColumns}${updatedAtClause}
|
||||
WHERE "${pkColumn}" = $${updateValues.length + 1}
|
||||
${companyCode !== "*" ? `AND company_code = $${updateValues.length + 2}` : ""}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const updateParams = companyCode !== "*"
|
||||
? [...updateValues, pkValue, companyCode]
|
||||
: [...updateValues, pkValue];
|
||||
|
||||
logger.info("메인 테이블 UPDATE:", { query: updateQuery, paramsCount: updateParams.length });
|
||||
mainResult = await client.query(updateQuery, updateParams);
|
||||
} else {
|
||||
// INSERT
|
||||
const columns = Object.keys(mainData).map(col => `"${col}"`).join(", ");
|
||||
const placeholders = Object.keys(mainData).map((_, idx) => `$${idx + 1}`).join(", ");
|
||||
const values = Object.values(mainData);
|
||||
|
||||
// updated_at 컬럼 존재 여부 확인
|
||||
const hasUpdatedAt = await client.query(`
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = $1 AND column_name = 'updated_at'
|
||||
`, [mainTableName]);
|
||||
const updatedAtClause = hasUpdatedAt.rowCount && hasUpdatedAt.rowCount > 0 ? ", updated_at = NOW()" : "";
|
||||
|
||||
const updateSetClause = Object.keys(mainData)
|
||||
.filter(col => col !== pkColumn)
|
||||
.map(col => `"${col}" = EXCLUDED."${col}"`)
|
||||
.join(", ");
|
||||
|
||||
const insertQuery = `
|
||||
INSERT INTO "${mainTableName}" (${columns})
|
||||
VALUES (${placeholders})
|
||||
ON CONFLICT ("${pkColumn}") DO UPDATE SET
|
||||
${updateSetClause}${updatedAtClause}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
logger.info("메인 테이블 INSERT/UPSERT:", { query: insertQuery, paramsCount: values.length });
|
||||
mainResult = await client.query(insertQuery, values);
|
||||
}
|
||||
|
||||
if (mainResult.rowCount === 0) {
|
||||
throw new Error("메인 테이블 저장 실패");
|
||||
}
|
||||
|
||||
const savedMainData = mainResult.rows[0];
|
||||
const savedPkValue = savedMainData[pkColumn];
|
||||
logger.info("메인 테이블 저장 완료:", { pkColumn, savedPkValue });
|
||||
|
||||
// 2. 서브 테이블 저장
|
||||
const subTableResults: any[] = [];
|
||||
|
||||
for (const subTableConfig of subTables || []) {
|
||||
const { tableName, linkColumn, items, options } = subTableConfig;
|
||||
|
||||
if (!tableName || !items || items.length === 0) {
|
||||
logger.info(`서브 테이블 ${tableName} 스킵: 데이터 없음`);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`서브 테이블 ${tableName} 저장 시작:`, {
|
||||
itemsCount: items.length,
|
||||
linkColumn,
|
||||
options,
|
||||
});
|
||||
|
||||
// 기존 데이터 삭제 옵션
|
||||
if (options?.deleteExistingBefore && linkColumn?.subColumn) {
|
||||
const deleteQuery = options?.deleteOnlySubItems && options?.mainMarkerColumn
|
||||
? `DELETE FROM "${tableName}" WHERE "${linkColumn.subColumn}" = $1 AND "${options.mainMarkerColumn}" = $2`
|
||||
: `DELETE FROM "${tableName}" WHERE "${linkColumn.subColumn}" = $1`;
|
||||
|
||||
const deleteParams = options?.deleteOnlySubItems && options?.mainMarkerColumn
|
||||
? [savedPkValue, options.subMarkerValue ?? false]
|
||||
: [savedPkValue];
|
||||
|
||||
logger.info(`서브 테이블 ${tableName} 기존 데이터 삭제:`, { deleteQuery, deleteParams });
|
||||
await client.query(deleteQuery, deleteParams);
|
||||
}
|
||||
|
||||
// 메인 데이터도 서브 테이블에 저장 (옵션)
|
||||
if (options?.saveMainAsFirst && options?.mainFieldMappings && linkColumn?.subColumn) {
|
||||
const mainSubItem: Record<string, any> = {
|
||||
[linkColumn.subColumn]: savedPkValue,
|
||||
};
|
||||
|
||||
// 메인 필드 매핑 적용
|
||||
for (const mapping of options.mainFieldMappings) {
|
||||
if (mapping.formField && mapping.targetColumn) {
|
||||
mainSubItem[mapping.targetColumn] = mainData[mapping.formField];
|
||||
}
|
||||
}
|
||||
|
||||
// 메인 마커 설정
|
||||
if (options.mainMarkerColumn) {
|
||||
mainSubItem[options.mainMarkerColumn] = options.mainMarkerValue ?? true;
|
||||
}
|
||||
|
||||
// company_code 추가
|
||||
if (companyCode !== "*") {
|
||||
mainSubItem.company_code = companyCode;
|
||||
}
|
||||
|
||||
const mainSubColumns = Object.keys(mainSubItem).map(col => `"${col}"`).join(", ");
|
||||
const mainSubPlaceholders = Object.keys(mainSubItem).map((_, idx) => `$${idx + 1}`).join(", ");
|
||||
const mainSubValues = Object.values(mainSubItem);
|
||||
|
||||
// UPSERT 쿼리 (PK가 있다면)
|
||||
const mainSubInsertQuery = `
|
||||
INSERT INTO "${tableName}" (${mainSubColumns})
|
||||
VALUES (${mainSubPlaceholders})
|
||||
ON CONFLICT ("${linkColumn.subColumn}"${options.mainMarkerColumn ? `, "${options.mainMarkerColumn}"` : ""})
|
||||
DO UPDATE SET
|
||||
${Object.keys(mainSubItem)
|
||||
.filter(col => col !== linkColumn.subColumn && col !== options.mainMarkerColumn)
|
||||
.map(col => `"${col}" = EXCLUDED."${col}"`)
|
||||
.join(", ") || "updated_at = NOW()"}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
try {
|
||||
logger.info(`서브 테이블 ${tableName} 메인 데이터 저장:`, { mainSubInsertQuery, mainSubValues });
|
||||
const mainSubResult = await client.query(mainSubInsertQuery, mainSubValues);
|
||||
subTableResults.push({ tableName, type: "main", data: mainSubResult.rows[0] });
|
||||
} catch (err: any) {
|
||||
// ON CONFLICT 실패 시 일반 INSERT 시도
|
||||
logger.warn(`서브 테이블 ${tableName} UPSERT 실패, 일반 INSERT 시도:`, err.message);
|
||||
const simpleInsertQuery = `
|
||||
INSERT INTO "${tableName}" (${mainSubColumns})
|
||||
VALUES (${mainSubPlaceholders})
|
||||
RETURNING *
|
||||
`;
|
||||
const simpleResult = await client.query(simpleInsertQuery, mainSubValues);
|
||||
subTableResults.push({ tableName, type: "main", data: simpleResult.rows[0] });
|
||||
}
|
||||
}
|
||||
|
||||
// 서브 아이템들 저장
|
||||
for (const item of items) {
|
||||
// 연결 컬럼 값 설정
|
||||
if (linkColumn?.subColumn) {
|
||||
item[linkColumn.subColumn] = savedPkValue;
|
||||
}
|
||||
|
||||
// company_code 추가
|
||||
if (companyCode !== "*" && !item.company_code) {
|
||||
item.company_code = companyCode;
|
||||
}
|
||||
|
||||
const subColumns = Object.keys(item).map(col => `"${col}"`).join(", ");
|
||||
const subPlaceholders = Object.keys(item).map((_, idx) => `$${idx + 1}`).join(", ");
|
||||
const subValues = Object.values(item);
|
||||
|
||||
const subInsertQuery = `
|
||||
INSERT INTO "${tableName}" (${subColumns})
|
||||
VALUES (${subPlaceholders})
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
logger.info(`서브 테이블 ${tableName} 아이템 저장:`, { subInsertQuery, subValuesCount: subValues.length });
|
||||
const subResult = await client.query(subInsertQuery, subValues);
|
||||
subTableResults.push({ tableName, type: "sub", data: subResult.rows[0] });
|
||||
}
|
||||
|
||||
logger.info(`서브 테이블 ${tableName} 저장 완료`);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("=== 다중 테이블 저장 완료 ===", {
|
||||
mainTable: mainTableName,
|
||||
mainPk: savedPkValue,
|
||||
subTableResultsCount: subTableResults.length,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "다중 테이블 저장이 완료되었습니다.",
|
||||
data: {
|
||||
main: savedMainData,
|
||||
subTables: subTableResults,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
|
||||
logger.error("다중 테이블 저장 실패:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "다중 테이블 저장에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user