refactor: Enhance unique constraint validation across data operations
- Integrated `TableManagementService` to validate unique constraints before insert, update, and upsert actions in various controllers, including `dataflowExecutionController`, `dynamicFormController`, and `tableManagementController`. - Improved error handling in `errorHandler` to provide detailed messages indicating which field has a unique constraint violation. - Updated the `formatPgError` utility to extract and display specific column labels for unique constraint violations, enhancing user feedback. - Adjusted the table schema retrieval to include company-specific nullable and unique constraints, ensuring accurate representation of database rules. These changes improve data integrity by preventing duplicate entries and enhance user experience through clearer error messages related to unique constraints.
This commit is contained in:
parent
d56e46b17c
commit
3982aabc24
|
|
@ -3563,19 +3563,21 @@ export async function getTableSchema(
|
||||||
|
|
||||||
logger.info("테이블 스키마 조회", { tableName, companyCode });
|
logger.info("테이블 스키마 조회", { tableName, companyCode });
|
||||||
|
|
||||||
// information_schema와 table_type_columns를 JOIN하여 컬럼 정보와 라벨 정보 함께 가져오기
|
// information_schema와 table_type_columns를 JOIN하여 컬럼 정보 + 회사별 제약조건 함께 가져오기
|
||||||
// 회사별 라벨 우선, 없으면 공통(*) 라벨 사용
|
// 회사별 설정 우선, 없으면 공통(*) 설정 사용
|
||||||
const schemaQuery = `
|
const schemaQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
ic.column_name,
|
ic.column_name,
|
||||||
ic.data_type,
|
ic.data_type,
|
||||||
ic.is_nullable,
|
ic.is_nullable AS db_is_nullable,
|
||||||
ic.column_default,
|
ic.column_default,
|
||||||
ic.character_maximum_length,
|
ic.character_maximum_length,
|
||||||
ic.numeric_precision,
|
ic.numeric_precision,
|
||||||
ic.numeric_scale,
|
ic.numeric_scale,
|
||||||
COALESCE(ttc_company.column_label, ttc_common.column_label) AS column_label,
|
COALESCE(ttc_company.column_label, ttc_common.column_label) AS column_label,
|
||||||
COALESCE(ttc_company.display_order, ttc_common.display_order) AS display_order
|
COALESCE(ttc_company.display_order, ttc_common.display_order) AS display_order,
|
||||||
|
COALESCE(ttc_company.is_nullable, ttc_common.is_nullable) AS ttc_is_nullable,
|
||||||
|
COALESCE(ttc_company.is_unique, ttc_common.is_unique) AS ttc_is_unique
|
||||||
FROM information_schema.columns ic
|
FROM information_schema.columns ic
|
||||||
LEFT JOIN table_type_columns ttc_common
|
LEFT JOIN table_type_columns ttc_common
|
||||||
ON ttc_common.table_name = ic.table_name
|
ON ttc_common.table_name = ic.table_name
|
||||||
|
|
@ -3600,17 +3602,28 @@ export async function getTableSchema(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컬럼 정보를 간단한 형태로 변환 (라벨 정보 포함)
|
// 컬럼 정보를 간단한 형태로 변환 (회사별 제약조건 반영)
|
||||||
const columnList = columns.map((col: any) => ({
|
const columnList = columns.map((col: any) => {
|
||||||
name: col.column_name,
|
// DB level nullable + 회사별 table_type_columns 제약조건 통합
|
||||||
label: col.column_label || col.column_name, // 라벨이 없으면 컬럼명 사용
|
// table_type_columns에서 is_nullable = 'N'이면 필수 (DB가 nullable이어도)
|
||||||
type: col.data_type,
|
const dbNullable = col.db_is_nullable === "YES";
|
||||||
nullable: col.is_nullable === "YES",
|
const ttcNotNull = col.ttc_is_nullable === "N";
|
||||||
default: col.column_default,
|
const effectiveNullable = ttcNotNull ? false : dbNullable;
|
||||||
maxLength: col.character_maximum_length,
|
|
||||||
precision: col.numeric_precision,
|
const ttcUnique = col.ttc_is_unique === "Y";
|
||||||
scale: col.numeric_scale,
|
|
||||||
}));
|
return {
|
||||||
|
name: col.column_name,
|
||||||
|
label: col.column_label || col.column_name,
|
||||||
|
type: col.data_type,
|
||||||
|
nullable: effectiveNullable,
|
||||||
|
unique: ttcUnique,
|
||||||
|
default: col.column_default,
|
||||||
|
maxLength: col.character_maximum_length,
|
||||||
|
precision: col.numeric_precision,
|
||||||
|
scale: col.numeric_scale,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
logger.info(`테이블 스키마 조회 성공: ${columnList.length}개 컬럼`);
|
logger.info(`테이블 스키마 조회 성공: ${columnList.length}개 컬럼`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Request, Response } from "express";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import { query } from "../database/db";
|
import { query } from "../database/db";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger";
|
||||||
|
import { TableManagementService } from "../services/tableManagementService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 데이터 액션 실행
|
* 데이터 액션 실행
|
||||||
|
|
@ -81,6 +82,19 @@ async function executeMainDatabaseAction(
|
||||||
company_code: companyCode,
|
company_code: companyCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// UNIQUE 제약조건 검증 (INSERT/UPDATE/UPSERT 전)
|
||||||
|
if (["insert", "update", "upsert"].includes(actionType.toLowerCase())) {
|
||||||
|
const tms = new TableManagementService();
|
||||||
|
const uniqueViolations = await tms.validateUniqueConstraints(
|
||||||
|
tableName,
|
||||||
|
dataWithCompany,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
if (uniqueViolations.length > 0) {
|
||||||
|
throw new Error(`중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (actionType.toLowerCase()) {
|
switch (actionType.toLowerCase()) {
|
||||||
case "insert":
|
case "insert":
|
||||||
return await executeInsert(tableName, dataWithCompany);
|
return await executeInsert(tableName, dataWithCompany);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { dynamicFormService } from "../services/dynamicFormService";
|
import { dynamicFormService } from "../services/dynamicFormService";
|
||||||
import { enhancedDynamicFormService } from "../services/enhancedDynamicFormService";
|
import { enhancedDynamicFormService } from "../services/enhancedDynamicFormService";
|
||||||
|
import { TableManagementService } from "../services/tableManagementService";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import { formatPgError } from "../utils/pgErrorUtil";
|
import { formatPgError } from "../utils/pgErrorUtil";
|
||||||
|
|
||||||
|
|
@ -48,6 +49,21 @@ export const saveFormData = async (
|
||||||
formDataWithMeta.company_code = companyCode;
|
formDataWithMeta.company_code = companyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UNIQUE 제약조건 검증 (INSERT 전)
|
||||||
|
const tms = new TableManagementService();
|
||||||
|
const uniqueViolations = await tms.validateUniqueConstraints(
|
||||||
|
tableName,
|
||||||
|
formDataWithMeta,
|
||||||
|
companyCode || "*"
|
||||||
|
);
|
||||||
|
if (uniqueViolations.length > 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 클라이언트 IP 주소 추출
|
// 클라이언트 IP 주소 추출
|
||||||
const ipAddress =
|
const ipAddress =
|
||||||
req.ip ||
|
req.ip ||
|
||||||
|
|
@ -112,6 +128,21 @@ export const saveFormDataEnhanced = async (
|
||||||
formDataWithMeta.company_code = companyCode;
|
formDataWithMeta.company_code = companyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UNIQUE 제약조건 검증 (INSERT 전)
|
||||||
|
const tmsEnhanced = new TableManagementService();
|
||||||
|
const uniqueViolations = await tmsEnhanced.validateUniqueConstraints(
|
||||||
|
tableName,
|
||||||
|
formDataWithMeta,
|
||||||
|
companyCode || "*"
|
||||||
|
);
|
||||||
|
if (uniqueViolations.length > 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 개선된 서비스 사용
|
// 개선된 서비스 사용
|
||||||
const result = await enhancedDynamicFormService.saveFormData(
|
const result = await enhancedDynamicFormService.saveFormData(
|
||||||
screenId,
|
screenId,
|
||||||
|
|
@ -153,12 +184,28 @@ export const updateFormData = async (
|
||||||
const formDataWithMeta = {
|
const formDataWithMeta = {
|
||||||
...data,
|
...data,
|
||||||
updated_by: userId,
|
updated_by: userId,
|
||||||
writer: data.writer || userId, // ✅ writer가 없으면 userId로 설정
|
writer: data.writer || userId,
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// UNIQUE 제약조건 검증 (UPDATE 시 자기 자신 제외)
|
||||||
|
const tmsUpdate = new TableManagementService();
|
||||||
|
const uniqueViolations = await tmsUpdate.validateUniqueConstraints(
|
||||||
|
tableName,
|
||||||
|
formDataWithMeta,
|
||||||
|
companyCode || "*",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
if (uniqueViolations.length > 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await dynamicFormService.updateFormData(
|
const result = await dynamicFormService.updateFormData(
|
||||||
id, // parseInt 제거 - 문자열 ID 지원
|
id,
|
||||||
tableName,
|
tableName,
|
||||||
formDataWithMeta
|
formDataWithMeta
|
||||||
);
|
);
|
||||||
|
|
@ -209,11 +256,27 @@ export const updateFormDataPartial = async (
|
||||||
const newDataWithMeta = {
|
const newDataWithMeta = {
|
||||||
...newData,
|
...newData,
|
||||||
updated_by: userId,
|
updated_by: userId,
|
||||||
writer: newData.writer || userId, // ✅ writer가 없으면 userId로 설정
|
writer: newData.writer || userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// UNIQUE 제약조건 검증 (부분 UPDATE 시 자기 자신 제외)
|
||||||
|
const tmsPartial = new TableManagementService();
|
||||||
|
const uniqueViolations = await tmsPartial.validateUniqueConstraints(
|
||||||
|
tableName,
|
||||||
|
newDataWithMeta,
|
||||||
|
companyCode || "*",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
if (uniqueViolations.length > 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await dynamicFormService.updateFormDataPartial(
|
const result = await dynamicFormService.updateFormDataPartial(
|
||||||
id, // 🔧 parseInt 제거 - UUID 문자열도 지원
|
id,
|
||||||
tableName,
|
tableName,
|
||||||
originalData,
|
originalData,
|
||||||
newDataWithMeta
|
newDataWithMeta
|
||||||
|
|
|
||||||
|
|
@ -2087,6 +2087,23 @@ export async function multiTableSave(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UNIQUE 제약조건 검증 (트랜잭션 전에)
|
||||||
|
const tmsMulti = new TableManagementService();
|
||||||
|
const uniqueViolations = await tmsMulti.validateUniqueConstraints(
|
||||||
|
mainTable.tableName,
|
||||||
|
mainData,
|
||||||
|
companyCode,
|
||||||
|
isUpdate ? mainData[mainTable.primaryKeyColumn] : undefined
|
||||||
|
);
|
||||||
|
if (uniqueViolations.length > 0) {
|
||||||
|
client.release();
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await client.query("BEGIN");
|
await client.query("BEGIN");
|
||||||
|
|
||||||
// 1. 메인 테이블 저장
|
// 1. 메인 테이블 저장
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,18 @@ export const errorHandler = (
|
||||||
// PostgreSQL 에러 코드 참조: https://www.postgresql.org/docs/current/errcodes-appendix.html
|
// PostgreSQL 에러 코드 참조: https://www.postgresql.org/docs/current/errcodes-appendix.html
|
||||||
if (pgError.code === "23505") {
|
if (pgError.code === "23505") {
|
||||||
// unique_violation
|
// unique_violation
|
||||||
error = new AppError("중복된 데이터가 존재합니다.", 400);
|
const constraint = pgError.constraint || "";
|
||||||
|
const tbl = pgError.table || "";
|
||||||
|
let col = "";
|
||||||
|
if (constraint && tbl) {
|
||||||
|
const prefix = `${tbl}_`;
|
||||||
|
const suffix = "_key";
|
||||||
|
if (constraint.startsWith(prefix) && constraint.endsWith(suffix)) {
|
||||||
|
col = constraint.slice(prefix.length, -suffix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const detail = col ? ` [${col}]` : "";
|
||||||
|
error = new AppError(`중복된 데이터가 존재합니다.${detail}`, 400);
|
||||||
} else if (pgError.code === "23503") {
|
} else if (pgError.code === "23503") {
|
||||||
// foreign_key_violation
|
// foreign_key_violation
|
||||||
error = new AppError("참조 무결성 제약 조건 위반입니다.", 400);
|
error = new AppError("참조 무결성 제약 조건 위반입니다.", 400);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { multiTableExcelService, TableChainConfig } from "../services/multiTable
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import { auditLogService } from "../services/auditLogService";
|
import { auditLogService } from "../services/auditLogService";
|
||||||
|
import { TableManagementService } from "../services/tableManagementService";
|
||||||
|
import { formatPgError } from "../utils/pgErrorUtil";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -950,6 +952,20 @@ router.post(
|
||||||
console.log(`🏢 company_name 자동 추가: ${req.user.companyName}`);
|
console.log(`🏢 company_name 자동 추가: ${req.user.companyName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UNIQUE 제약조건 검증
|
||||||
|
const tms = new TableManagementService();
|
||||||
|
const uniqueViolations = await tms.validateUniqueConstraints(
|
||||||
|
tableName,
|
||||||
|
enrichedData,
|
||||||
|
req.user?.companyCode || "*"
|
||||||
|
);
|
||||||
|
if (uniqueViolations.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 레코드 생성
|
// 레코드 생성
|
||||||
const result = await dataService.createRecord(tableName, enrichedData);
|
const result = await dataService.createRecord(tableName, enrichedData);
|
||||||
|
|
||||||
|
|
@ -1019,6 +1035,21 @@ router.put(
|
||||||
|
|
||||||
console.log(`✏️ 레코드 수정: ${tableName}/${id}`, data);
|
console.log(`✏️ 레코드 수정: ${tableName}/${id}`, data);
|
||||||
|
|
||||||
|
// UNIQUE 제약조건 검증 (자기 자신 제외)
|
||||||
|
const tmsUpdate = new TableManagementService();
|
||||||
|
const uniqueViolations = await tmsUpdate.validateUniqueConstraints(
|
||||||
|
tableName,
|
||||||
|
data,
|
||||||
|
req.user?.companyCode || "*",
|
||||||
|
String(id)
|
||||||
|
);
|
||||||
|
if (uniqueViolations.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 레코드 수정
|
// 레코드 수정
|
||||||
const result = await dataService.updateRecord(tableName, id, data);
|
const result = await dataService.updateRecord(tableName, id, data);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -970,10 +970,11 @@ class MultiTableExcelService {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
c.column_name,
|
c.column_name,
|
||||||
c.is_nullable,
|
c.is_nullable AS db_is_nullable,
|
||||||
c.column_default,
|
c.column_default,
|
||||||
COALESCE(ttc.column_label, cl.column_label) AS column_label,
|
COALESCE(ttc.column_label, cl.column_label) AS column_label,
|
||||||
COALESCE(ttc.reference_table, cl.reference_table) AS reference_table
|
COALESCE(ttc.reference_table, cl.reference_table) AS reference_table,
|
||||||
|
COALESCE(ttc.is_nullable, cl.is_nullable) AS ttc_is_nullable
|
||||||
FROM information_schema.columns c
|
FROM information_schema.columns c
|
||||||
LEFT JOIN table_type_columns cl
|
LEFT JOIN table_type_columns cl
|
||||||
ON c.table_name = cl.table_name AND c.column_name = cl.column_name AND cl.company_code = '*'
|
ON c.table_name = cl.table_name AND c.column_name = cl.column_name AND cl.company_code = '*'
|
||||||
|
|
@ -991,13 +992,13 @@ class MultiTableExcelService {
|
||||||
// 시스템 컬럼 제외
|
// 시스템 컬럼 제외
|
||||||
if (MultiTableExcelService.SYSTEM_COLUMNS.has(colName)) continue;
|
if (MultiTableExcelService.SYSTEM_COLUMNS.has(colName)) continue;
|
||||||
|
|
||||||
// FK 컬럼 제외 (reference_table이 있는 컬럼 = 다른 테이블의 PK를 참조)
|
// FK 컬럼 제외
|
||||||
// 단, 비즈니스적으로 의미 있는 FK는 남길 수 있으므로,
|
|
||||||
// _id로 끝나면서 reference_table이 있는 경우만 제외
|
|
||||||
if (row.reference_table && colName.endsWith("_id")) continue;
|
if (row.reference_table && colName.endsWith("_id")) continue;
|
||||||
|
|
||||||
const hasDefault = row.column_default !== null;
|
const hasDefault = row.column_default !== null;
|
||||||
const isNullable = row.is_nullable === "YES";
|
const dbNullable = row.db_is_nullable === "YES";
|
||||||
|
const ttcNotNull = row.ttc_is_nullable === "N";
|
||||||
|
const isNullable = ttcNotNull ? false : dbNullable;
|
||||||
const isRequired = !isNullable && !hasDefault;
|
const isRequired = !isNullable && !hasDefault;
|
||||||
|
|
||||||
columns.push({
|
columns.push({
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,38 @@ export async function formatPgError(
|
||||||
const detail = colName ? ` [${colName}]` : "";
|
const detail = colName ? ` [${colName}]` : "";
|
||||||
return `필수 입력값이 누락되었습니다.${detail}`;
|
return `필수 입력값이 누락되었습니다.${detail}`;
|
||||||
}
|
}
|
||||||
case "23505":
|
case "23505": {
|
||||||
return "중복된 데이터가 존재합니다.";
|
// unique_violation
|
||||||
|
const constraint = error.constraint || "";
|
||||||
|
const tblName = error.table || "";
|
||||||
|
// constraint 이름에서 컬럼명 추출 시도 (예: item_mst_item_code_key → item_code)
|
||||||
|
let colName = "";
|
||||||
|
if (constraint && tblName) {
|
||||||
|
const prefix = `${tblName}_`;
|
||||||
|
const suffix = "_key";
|
||||||
|
if (constraint.startsWith(prefix) && constraint.endsWith(suffix)) {
|
||||||
|
colName = constraint.slice(prefix.length, -suffix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (colName && tblName && companyCode) {
|
||||||
|
try {
|
||||||
|
const rows = await query(
|
||||||
|
`SELECT column_label FROM table_type_columns
|
||||||
|
WHERE table_name = $1 AND column_name = $2 AND company_code = $3
|
||||||
|
LIMIT 1`,
|
||||||
|
[tblName, colName, companyCode]
|
||||||
|
);
|
||||||
|
const label = rows[0]?.column_label;
|
||||||
|
if (label) {
|
||||||
|
return `중복된 데이터가 존재합니다: ${label}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 폴백
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const detail = colName ? ` [${colName}]` : "";
|
||||||
|
return `중복된 데이터가 존재합니다.${detail}`;
|
||||||
|
}
|
||||||
case "23503":
|
case "23503":
|
||||||
return "참조 무결성 제약 조건 위반입니다.";
|
return "참조 무결성 제약 조건 위반입니다.";
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -1061,19 +1061,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 백엔드 DISTINCT API로 전체 고유값 조회 (페이징과 무관하게 모든 값 반환)
|
// 카테고리/엔티티/코드 타입이 아닌 경우에만 DISTINCT API 사용
|
||||||
try {
|
// (카테고리 등은 DISTINCT로 조회하면 코드만 반환되어 label이 없음)
|
||||||
const { apiClient } = await import("@/lib/api/client");
|
if (!["category", "entity", "code"].includes(inputType)) {
|
||||||
const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`);
|
try {
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||||||
return response.data.data.map((item: any) => ({
|
return response.data.data.map((item: any) => ({
|
||||||
value: String(item.value),
|
value: String(item.value),
|
||||||
label: String(item.label),
|
label: String(item.label),
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// DISTINCT API 실패 시 현재 데이터 기반으로 fallback
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
|
||||||
// DISTINCT API 실패 시 현재 데이터 기반으로 fallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback: 현재 로드된 데이터에서 고유 값 추출
|
// fallback: 현재 로드된 데이터에서 고유 값 추출
|
||||||
|
|
|
||||||
|
|
@ -467,14 +467,9 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
|
|
||||||
if (hasNewOptions) {
|
if (hasNewOptions) {
|
||||||
setSelectOptions((prev) => {
|
setSelectOptions((prev) => {
|
||||||
// 이미 로드된 옵션은 유지, 새로 로드된 옵션만 병합
|
// 새로 로드된 옵션으로 항상 갱신 (카테고리 label 정보가 나중에 로드될 수 있으므로)
|
||||||
const merged = { ...prev };
|
// 로드 실패한 컬럼의 기존 옵션은 유지
|
||||||
for (const [key, value] of Object.entries(loadedOptions)) {
|
return { ...prev, ...loadedOptions };
|
||||||
if (!merged[key] || merged[key].length === 0) {
|
|
||||||
merged[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue