fix: Refine ExcelUploadModal and TableListComponent for improved data handling
- Updated ExcelUploadModal to automatically generate numbering codes when Excel values are empty, enhancing user experience during data uploads. - Modified TableListComponent to display only the first image in case of multiple images, ensuring clarity in image representation. - Improved data handling logic in TableListComponent to prevent unnecessary processing of string values.
This commit is contained in:
@@ -939,6 +939,24 @@ export async function addTableData(
|
||||
return;
|
||||
}
|
||||
|
||||
// 회사별 UNIQUE 소프트 제약조건 검증
|
||||
const uniqueViolations = await tableManagementService.validateUniqueConstraints(
|
||||
tableName,
|
||||
data,
|
||||
companyCode || "*"
|
||||
);
|
||||
if (uniqueViolations.length > 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||
error: {
|
||||
code: "UNIQUE_VIOLATION",
|
||||
details: uniqueViolations,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 데이터 추가
|
||||
await tableManagementService.addTableData(tableName, data);
|
||||
|
||||
@@ -1041,6 +1059,26 @@ export async function editTableData(
|
||||
return;
|
||||
}
|
||||
|
||||
// 회사별 UNIQUE 소프트 제약조건 검증 (수정 시 자기 자신 제외)
|
||||
const excludeId = originalData?.id ? String(originalData.id) : undefined;
|
||||
const uniqueViolations = await tableManagementService.validateUniqueConstraints(
|
||||
tableName,
|
||||
updatedData,
|
||||
companyCode,
|
||||
excludeId
|
||||
);
|
||||
if (uniqueViolations.length > 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||
error: {
|
||||
code: "UNIQUE_VIOLATION",
|
||||
details: uniqueViolations,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 데이터 수정
|
||||
await tableManagementService.editTableData(
|
||||
tableName,
|
||||
@@ -2653,8 +2691,22 @@ export async function toggleTableIndex(
|
||||
logger.info(`인덱스 ${action}: ${indexName} (${indexType})`);
|
||||
|
||||
if (action === "create") {
|
||||
let indexColumns = `"${columnName}"`;
|
||||
|
||||
// 유니크 인덱스: company_code 컬럼이 있으면 복합 유니크 (회사별 유니크 보장)
|
||||
if (indexType === "unique") {
|
||||
const hasCompanyCode = await query(
|
||||
`SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 AND column_name = 'company_code'`,
|
||||
[tableName]
|
||||
);
|
||||
if (hasCompanyCode.length > 0) {
|
||||
indexColumns = `"company_code", "${columnName}"`;
|
||||
logger.info(`멀티테넌시: company_code + ${columnName} 복합 유니크 인덱스 생성`);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueClause = indexType === "unique" ? "UNIQUE " : "";
|
||||
const sql = `CREATE ${uniqueClause}INDEX "${indexName}" ON "public"."${tableName}" ("${columnName}")`;
|
||||
const sql = `CREATE ${uniqueClause}INDEX IF NOT EXISTS "${indexName}" ON "public"."${tableName}" (${indexColumns})`;
|
||||
logger.info(`인덱스 생성: ${sql}`);
|
||||
await query(sql);
|
||||
} else if (action === "drop") {
|
||||
@@ -2675,15 +2727,45 @@ export async function toggleTableIndex(
|
||||
} catch (error: any) {
|
||||
logger.error("인덱스 토글 오류:", error);
|
||||
|
||||
// 중복 데이터로 인한 UNIQUE 인덱스 생성 실패 안내
|
||||
const errorMsg = error.message?.includes("duplicate key")
|
||||
? "중복 데이터가 있어 UNIQUE 인덱스를 생성할 수 없습니다. 중복 데이터를 먼저 정리해주세요."
|
||||
: "인덱스 설정 중 오류가 발생했습니다.";
|
||||
const errMsg = error.message || "";
|
||||
let userMessage = "인덱스 설정 중 오류가 발생했습니다.";
|
||||
let duplicates: any[] = [];
|
||||
|
||||
// 중복 데이터로 인한 UNIQUE 인덱스 생성 실패
|
||||
if (
|
||||
errMsg.includes("could not create unique index") ||
|
||||
errMsg.includes("duplicate key")
|
||||
) {
|
||||
const { columnName, tableName } = { ...req.params, ...req.body };
|
||||
try {
|
||||
duplicates = await query(
|
||||
`SELECT company_code, "${columnName}", COUNT(*) as cnt FROM "${tableName}" GROUP BY company_code, "${columnName}" HAVING COUNT(*) > 1 ORDER BY cnt DESC LIMIT 10`
|
||||
);
|
||||
} catch {
|
||||
try {
|
||||
duplicates = await query(
|
||||
`SELECT "${columnName}", COUNT(*) as cnt FROM "${tableName}" GROUP BY "${columnName}" HAVING COUNT(*) > 1 ORDER BY cnt DESC LIMIT 10`
|
||||
);
|
||||
} catch { /* 중복 조회 실패 시 무시 */ }
|
||||
}
|
||||
|
||||
const dupDetails = duplicates.length > 0
|
||||
? duplicates.map((d: any) => {
|
||||
const company = d.company_code ? `[${d.company_code}] ` : "";
|
||||
return `${company}"${d[columnName] ?? 'NULL'}" (${d.cnt}건)`;
|
||||
}).join(", ")
|
||||
: "";
|
||||
|
||||
userMessage = dupDetails
|
||||
? `[${columnName}] 컬럼에 같은 회사 내 중복 데이터가 있어 유니크 인덱스를 생성할 수 없습니다. 중복 값: ${dupDetails}`
|
||||
: `[${columnName}] 컬럼에 중복 데이터가 있어 유니크 인덱스를 생성할 수 없습니다. 중복 데이터를 먼저 정리해주세요.`;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMsg,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
message: userMessage,
|
||||
error: errMsg,
|
||||
duplicates,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2776,3 +2858,89 @@ export async function toggleColumnNullable(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UNIQUE 토글 (회사별 소프트 제약조건)
|
||||
* PUT /api/table-management/tables/:tableName/columns/:columnName/unique
|
||||
*
|
||||
* DB 레벨 인덱스 대신 table_type_columns.is_unique를 회사별로 관리한다.
|
||||
* 저장 시 앱 레벨에서 중복 검증을 수행한다.
|
||||
*/
|
||||
export async function toggleColumnUnique(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const { unique } = req.body;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
if (!tableName || !columnName || typeof unique !== "boolean") {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "tableName, columnName, unique(boolean)이 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isUniqueValue = unique ? "Y" : "N";
|
||||
|
||||
if (unique) {
|
||||
// UNIQUE 설정 전 - 해당 회사의 기존 데이터에 중복이 있는지 확인
|
||||
const hasCompanyCode = await query<{ column_name: string }>(
|
||||
`SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = $1 AND column_name = 'company_code'`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
if (hasCompanyCode.length > 0) {
|
||||
const dupQuery = companyCode === "*"
|
||||
? `SELECT "${columnName}", COUNT(*) as cnt FROM "${tableName}" WHERE "${columnName}" IS NOT NULL GROUP BY "${columnName}" HAVING COUNT(*) > 1 LIMIT 10`
|
||||
: `SELECT "${columnName}", COUNT(*) as cnt FROM "${tableName}" WHERE "${columnName}" IS NOT NULL AND company_code = $1 GROUP BY "${columnName}" HAVING COUNT(*) > 1 LIMIT 10`;
|
||||
const dupParams = companyCode === "*" ? [] : [companyCode];
|
||||
|
||||
const dupResult = await query<any>(dupQuery, dupParams);
|
||||
|
||||
if (dupResult.length > 0) {
|
||||
const dupDetails = dupResult
|
||||
.map((d: any) => `"${d[columnName]}" (${d.cnt}건)`)
|
||||
.join(", ");
|
||||
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: `현재 회사 데이터에 중복 값이 존재합니다. 중복 데이터를 먼저 정리해주세요. 중복 값: ${dupDetails}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// table_type_columns에 회사별 is_unique 설정 UPSERT
|
||||
await query(
|
||||
`INSERT INTO table_type_columns (table_name, column_name, is_unique, company_code, created_date, updated_date)
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
ON CONFLICT (table_name, column_name, company_code)
|
||||
DO UPDATE SET is_unique = $3, updated_date = NOW()`,
|
||||
[tableName, columnName, isUniqueValue, companyCode]
|
||||
);
|
||||
|
||||
logger.info(`UNIQUE 소프트 제약조건 변경: ${tableName}.${columnName} → is_unique=${isUniqueValue}`, {
|
||||
companyCode,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: unique
|
||||
? `${columnName} 컬럼이 UNIQUE로 설정되었습니다.`
|
||||
: `${columnName} 컬럼의 UNIQUE 제약이 해제되었습니다.`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("UNIQUE 토글 오류:", error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "UNIQUE 설정 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user