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:
parent
38dda2f807
commit
262221e300
|
|
@ -939,6 +939,24 @@ export async function addTableData(
|
||||||
return;
|
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);
|
await tableManagementService.addTableData(tableName, data);
|
||||||
|
|
||||||
|
|
@ -1041,6 +1059,26 @@ export async function editTableData(
|
||||||
return;
|
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(
|
await tableManagementService.editTableData(
|
||||||
tableName,
|
tableName,
|
||||||
|
|
@ -2653,8 +2691,22 @@ export async function toggleTableIndex(
|
||||||
logger.info(`인덱스 ${action}: ${indexName} (${indexType})`);
|
logger.info(`인덱스 ${action}: ${indexName} (${indexType})`);
|
||||||
|
|
||||||
if (action === "create") {
|
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 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}`);
|
logger.info(`인덱스 생성: ${sql}`);
|
||||||
await query(sql);
|
await query(sql);
|
||||||
} else if (action === "drop") {
|
} else if (action === "drop") {
|
||||||
|
|
@ -2675,15 +2727,45 @@ export async function toggleTableIndex(
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("인덱스 토글 오류:", error);
|
logger.error("인덱스 토글 오류:", error);
|
||||||
|
|
||||||
// 중복 데이터로 인한 UNIQUE 인덱스 생성 실패 안내
|
const errMsg = error.message || "";
|
||||||
const errorMsg = error.message?.includes("duplicate key")
|
let userMessage = "인덱스 설정 중 오류가 발생했습니다.";
|
||||||
? "중복 데이터가 있어 UNIQUE 인덱스를 생성할 수 없습니다. 중복 데이터를 먼저 정리해주세요."
|
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({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: errorMsg,
|
message: userMessage,
|
||||||
error: error instanceof Error ? error.message : "Unknown error",
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
setTablePrimaryKey, // 🆕 PK 설정
|
setTablePrimaryKey, // 🆕 PK 설정
|
||||||
toggleTableIndex, // 🆕 인덱스 토글
|
toggleTableIndex, // 🆕 인덱스 토글
|
||||||
toggleColumnNullable, // 🆕 NOT NULL 토글
|
toggleColumnNullable, // 🆕 NOT NULL 토글
|
||||||
|
toggleColumnUnique, // 🆕 UNIQUE 토글
|
||||||
} from "../controllers/tableManagementController";
|
} from "../controllers/tableManagementController";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
@ -161,6 +162,12 @@ router.post("/tables/:tableName/indexes", toggleTableIndex);
|
||||||
*/
|
*/
|
||||||
router.put("/tables/:tableName/columns/:columnName/nullable", toggleColumnNullable);
|
router.put("/tables/:tableName/columns/:columnName/nullable", toggleColumnNullable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UNIQUE 토글
|
||||||
|
* PUT /api/table-management/tables/:tableName/columns/:columnName/unique
|
||||||
|
*/
|
||||||
|
router.put("/tables/:tableName/columns/:columnName/unique", toggleColumnUnique);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블 존재 여부 확인
|
* 테이블 존재 여부 확인
|
||||||
* GET /api/table-management/tables/:tableName/exists
|
* GET /api/table-management/tables/:tableName/exists
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,10 @@ export class TableManagementService {
|
||||||
THEN CASE WHEN COALESCE(ttc.is_nullable, cl.is_nullable) = 'N' THEN 'NO' ELSE 'YES' END
|
THEN CASE WHEN COALESCE(ttc.is_nullable, cl.is_nullable) = 'N' THEN 'NO' ELSE 'YES' END
|
||||||
ELSE c.is_nullable
|
ELSE c.is_nullable
|
||||||
END as "isNullable",
|
END as "isNullable",
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(ttc.is_unique, cl.is_unique) = 'Y' THEN 'YES'
|
||||||
|
ELSE 'NO'
|
||||||
|
END as "isUnique",
|
||||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
||||||
c.column_default as "defaultValue",
|
c.column_default as "defaultValue",
|
||||||
c.character_maximum_length as "maxLength",
|
c.character_maximum_length as "maxLength",
|
||||||
|
|
@ -250,6 +254,10 @@ export class TableManagementService {
|
||||||
THEN CASE WHEN cl.is_nullable = 'N' THEN 'NO' ELSE 'YES' END
|
THEN CASE WHEN cl.is_nullable = 'N' THEN 'NO' ELSE 'YES' END
|
||||||
ELSE c.is_nullable
|
ELSE c.is_nullable
|
||||||
END as "isNullable",
|
END as "isNullable",
|
||||||
|
CASE
|
||||||
|
WHEN cl.is_unique = 'Y' THEN 'YES'
|
||||||
|
ELSE 'NO'
|
||||||
|
END as "isUnique",
|
||||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
||||||
c.column_default as "defaultValue",
|
c.column_default as "defaultValue",
|
||||||
c.character_maximum_length as "maxLength",
|
c.character_maximum_length as "maxLength",
|
||||||
|
|
@ -2534,6 +2542,93 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회사별 UNIQUE 소프트 제약조건 검증
|
||||||
|
* table_type_columns.is_unique = 'Y'인 컬럼에 중복 값이 들어오면 위반 목록을 반환한다.
|
||||||
|
* @param excludeId 수정 시 자기 자신은 제외
|
||||||
|
*/
|
||||||
|
async validateUniqueConstraints(
|
||||||
|
tableName: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
companyCode: string,
|
||||||
|
excludeId?: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
// 회사별 설정 우선, 없으면 공통(*) 설정 사용
|
||||||
|
let uniqueColumns = await query<{ column_name: string; column_label: string }>(
|
||||||
|
`SELECT
|
||||||
|
ttc.column_name,
|
||||||
|
COALESCE(ttc.column_label, ttc.column_name) as column_label
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
WHERE ttc.table_name = $1
|
||||||
|
AND ttc.is_unique = 'Y'
|
||||||
|
AND ttc.company_code = $2`,
|
||||||
|
[tableName, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 회사별 설정이 없으면 공통 설정 확인
|
||||||
|
if (uniqueColumns.length === 0 && companyCode !== "*") {
|
||||||
|
const globalUnique = await query<{ column_name: string; column_label: string }>(
|
||||||
|
`SELECT
|
||||||
|
ttc.column_name,
|
||||||
|
COALESCE(ttc.column_label, ttc.column_name) as column_label
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
WHERE ttc.table_name = $1
|
||||||
|
AND ttc.is_unique = 'Y'
|
||||||
|
AND ttc.company_code = '*'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM table_type_columns ttc2
|
||||||
|
WHERE ttc2.table_name = ttc.table_name
|
||||||
|
AND ttc2.column_name = ttc.column_name
|
||||||
|
AND ttc2.company_code = $2
|
||||||
|
)`,
|
||||||
|
[tableName, companyCode]
|
||||||
|
);
|
||||||
|
uniqueColumns = globalUnique;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueColumns.length === 0) return [];
|
||||||
|
|
||||||
|
const violations: string[] = [];
|
||||||
|
for (const col of uniqueColumns) {
|
||||||
|
const value = data[col.column_name];
|
||||||
|
if (value === null || value === undefined || value === "") continue;
|
||||||
|
|
||||||
|
// 해당 회사 내에서 같은 값이 이미 존재하는지 확인
|
||||||
|
const hasCompanyCode = await query(
|
||||||
|
`SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_name = $1 AND column_name = 'company_code'`,
|
||||||
|
[tableName]
|
||||||
|
);
|
||||||
|
|
||||||
|
let dupQuery: string;
|
||||||
|
let dupParams: any[];
|
||||||
|
|
||||||
|
if (hasCompanyCode.length > 0 && companyCode !== "*") {
|
||||||
|
dupQuery = excludeId
|
||||||
|
? `SELECT 1 FROM "${tableName}" WHERE "${col.column_name}" = $1 AND company_code = $2 AND id != $3 LIMIT 1`
|
||||||
|
: `SELECT 1 FROM "${tableName}" WHERE "${col.column_name}" = $1 AND company_code = $2 LIMIT 1`;
|
||||||
|
dupParams = excludeId ? [value, companyCode, excludeId] : [value, companyCode];
|
||||||
|
} else {
|
||||||
|
dupQuery = excludeId
|
||||||
|
? `SELECT 1 FROM "${tableName}" WHERE "${col.column_name}" = $1 AND id != $2 LIMIT 1`
|
||||||
|
: `SELECT 1 FROM "${tableName}" WHERE "${col.column_name}" = $1 LIMIT 1`;
|
||||||
|
dupParams = excludeId ? [value, excludeId] : [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dupResult = await query(dupQuery, dupParams);
|
||||||
|
if (dupResult.length > 0) {
|
||||||
|
violations.push(`${col.column_label} (${value})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return violations;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`UNIQUE 검증 오류: ${tableName}`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블에 데이터 추가
|
* 테이블에 데이터 추가
|
||||||
* @returns 무시된 컬럼 정보 (디버깅용)
|
* @returns 무시된 컬럼 정보 (디버깅용)
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ interface ColumnTypeInfo {
|
||||||
detailSettings: string;
|
detailSettings: string;
|
||||||
description: string;
|
description: string;
|
||||||
isNullable: string;
|
isNullable: string;
|
||||||
|
isUnique: string;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
numericPrecision?: number;
|
numericPrecision?: number;
|
||||||
|
|
@ -382,10 +383,11 @@ export default function TableManagementPage() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...col,
|
...col,
|
||||||
inputType: col.inputType || "text", // 기본값: text
|
inputType: col.inputType || "text",
|
||||||
numberingRuleId, // 🆕 채번규칙 ID
|
isUnique: col.isUnique || "NO",
|
||||||
categoryMenus: col.categoryMenus || [], // 카테고리 메뉴 매핑 정보
|
numberingRuleId,
|
||||||
hierarchyRole, // 계층구조 역할
|
categoryMenus: col.categoryMenus || [],
|
||||||
|
hierarchyRole,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1091,9 +1093,9 @@ export default function TableManagementPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 인덱스 토글 핸들러
|
// 인덱스 토글 핸들러 (일반 인덱스만 DB 레벨 - 유니크는 앱 레벨 소프트 제약조건으로 분리됨)
|
||||||
const handleIndexToggle = useCallback(
|
const handleIndexToggle = useCallback(
|
||||||
async (columnName: string, indexType: "index" | "unique", checked: boolean) => {
|
async (columnName: string, indexType: "index", checked: boolean) => {
|
||||||
if (!selectedTable) return;
|
if (!selectedTable) return;
|
||||||
const action = checked ? "create" : "drop";
|
const action = checked ? "create" : "drop";
|
||||||
try {
|
try {
|
||||||
|
|
@ -1122,14 +1124,41 @@ export default function TableManagementPage() {
|
||||||
const hasIndex = constraints.indexes.some(
|
const hasIndex = constraints.indexes.some(
|
||||||
(idx) => !idx.isUnique && idx.columns.length === 1 && idx.columns[0] === columnName,
|
(idx) => !idx.isUnique && idx.columns.length === 1 && idx.columns[0] === columnName,
|
||||||
);
|
);
|
||||||
const hasUnique = constraints.indexes.some(
|
return { isPk, hasIndex };
|
||||||
(idx) => idx.isUnique && idx.columns.length === 1 && idx.columns[0] === columnName,
|
|
||||||
);
|
|
||||||
return { isPk, hasIndex, hasUnique };
|
|
||||||
},
|
},
|
||||||
[constraints],
|
[constraints],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// UNIQUE 토글 핸들러 (앱 레벨 소프트 제약조건 - NOT NULL과 동일 패턴)
|
||||||
|
const handleUniqueToggle = useCallback(
|
||||||
|
async (columnName: string, currentIsUnique: string) => {
|
||||||
|
if (!selectedTable) return;
|
||||||
|
const isCurrentlyUnique = currentIsUnique === "YES";
|
||||||
|
const newUnique = !isCurrentlyUnique;
|
||||||
|
try {
|
||||||
|
const response = await apiClient.put(
|
||||||
|
`/table-management/tables/${selectedTable}/columns/${columnName}/unique`,
|
||||||
|
{ unique: newUnique },
|
||||||
|
);
|
||||||
|
if (response.data.success) {
|
||||||
|
toast.success(response.data.message);
|
||||||
|
setColumns((prev) =>
|
||||||
|
prev.map((col) =>
|
||||||
|
col.columnName === columnName
|
||||||
|
? { ...col, isUnique: newUnique ? "YES" : "NO" }
|
||||||
|
: col,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(response.data.message || "UNIQUE 설정 실패");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error?.response?.data?.message || "UNIQUE 설정 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedTable],
|
||||||
|
);
|
||||||
|
|
||||||
// NOT NULL 토글 핸들러
|
// NOT NULL 토글 핸들러
|
||||||
const handleNullableToggle = useCallback(
|
const handleNullableToggle = useCallback(
|
||||||
async (columnName: string, currentIsNullable: string) => {
|
async (columnName: string, currentIsNullable: string) => {
|
||||||
|
|
@ -2029,12 +2058,12 @@ export default function TableManagementPage() {
|
||||||
aria-label={`${column.columnName} 인덱스 설정`}
|
aria-label={`${column.columnName} 인덱스 설정`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* UQ 체크박스 */}
|
{/* UQ 체크박스 (앱 레벨 소프트 제약조건) */}
|
||||||
<div className="flex items-center justify-center pt-1">
|
<div className="flex items-center justify-center pt-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={idxState.hasUnique}
|
checked={column.isUnique === "YES"}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={() =>
|
||||||
handleIndexToggle(column.columnName, "unique", checked as boolean)
|
handleUniqueToggle(column.columnName, column.isUnique)
|
||||||
}
|
}
|
||||||
aria-label={`${column.columnName} 유니크 설정`}
|
aria-label={`${column.columnName} 유니크 설정`}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -942,17 +942,22 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 채번 적용: 각 행마다 채번 API 호출 (신규 등록 시에만, 자동 감지된 채번 규칙 사용)
|
// 채번 적용: 엑셀에 값이 없거나 빈 값이면 채번 규칙으로 자동 생성, 값이 있으면 그대로 사용
|
||||||
if (hasNumbering && numberingInfo && uploadMode === "insert" && !shouldUpdate) {
|
if (hasNumbering && numberingInfo && uploadMode === "insert" && !shouldUpdate) {
|
||||||
try {
|
const existingValue = dataToSave[numberingInfo.columnName];
|
||||||
const { apiClient } = await import("@/lib/api/client");
|
const hasExcelValue = existingValue !== undefined && existingValue !== null && String(existingValue).trim() !== "";
|
||||||
const numberingResponse = await apiClient.post(`/numbering-rules/${numberingInfo.numberingRuleId}/allocate`);
|
|
||||||
const generatedCode = numberingResponse.data?.data?.generatedCode || numberingResponse.data?.data?.code;
|
if (!hasExcelValue) {
|
||||||
if (numberingResponse.data?.success && generatedCode) {
|
try {
|
||||||
dataToSave[numberingInfo.columnName] = generatedCode;
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const numberingResponse = await apiClient.post(`/numbering-rules/${numberingInfo.numberingRuleId}/allocate`);
|
||||||
|
const generatedCode = numberingResponse.data?.data?.generatedCode || numberingResponse.data?.data?.code;
|
||||||
|
if (numberingResponse.data?.success && generatedCode) {
|
||||||
|
dataToSave[numberingInfo.columnName] = generatedCode;
|
||||||
|
}
|
||||||
|
} catch (numError) {
|
||||||
|
console.error("채번 오류:", numError);
|
||||||
}
|
}
|
||||||
} catch (numError) {
|
|
||||||
console.error("채번 오류:", numError);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4204,9 +4204,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선)
|
// inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선)
|
||||||
const inputType = meta?.inputType || column.inputType;
|
const inputType = meta?.inputType || column.inputType;
|
||||||
|
|
||||||
// 🖼️ 이미지 타입: 작은 썸네일 표시
|
// 🖼️ 이미지 타입: 작은 썸네일 표시 (다중 이미지인 경우 대표 이미지 1개만)
|
||||||
if (inputType === "image" && value && typeof value === "string") {
|
if (inputType === "image" && value && typeof value === "string") {
|
||||||
const imageUrl = getFullImageUrl(value);
|
const firstImage = value.includes(",") ? value.split(",")[0].trim() : value;
|
||||||
|
const imageUrl = getFullImageUrl(firstImage);
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
const strValue = String(value);
|
// 다중 이미지인 경우 대표 이미지(첫 번째)만 사용
|
||||||
|
const rawValue = String(value);
|
||||||
|
const strValue = rawValue.includes(",") ? rawValue.split(",")[0].trim() : rawValue;
|
||||||
const isObjid = /^\d+$/.test(strValue);
|
const isObjid = /^\d+$/.test(strValue);
|
||||||
|
|
||||||
if (isObjid) {
|
if (isObjid) {
|
||||||
|
|
@ -89,8 +91,8 @@ const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => {
|
||||||
style={{ maxWidth: "40px", maxHeight: "40px" }}
|
style={{ maxWidth: "40px", maxHeight: "40px" }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// objid인 경우 preview URL로 열기, 아니면 full URL로 열기
|
const rawValue = String(value);
|
||||||
const strValue = String(value);
|
const strValue = rawValue.includes(",") ? rawValue.split(",")[0].trim() : rawValue;
|
||||||
const isObjid = /^\d+$/.test(strValue);
|
const isObjid = /^\d+$/.test(strValue);
|
||||||
const openUrl = isObjid ? getFilePreviewUrl(strValue) : getFullImageUrl(strValue);
|
const openUrl = isObjid ? getFilePreviewUrl(strValue) : getFullImageUrl(strValue);
|
||||||
window.open(openUrl, "_blank");
|
window.open(openUrl, "_blank");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue