테이블 추가기능 수정사항
This commit is contained in:
parent
474cc33aee
commit
e653effac0
|
|
@ -42,11 +42,17 @@ export class DDLController {
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// inputType을 webType으로 변환 (레거시 호환성)
|
||||||
|
const processedColumns = columns.map((col) => ({
|
||||||
|
...col,
|
||||||
|
webType: (col.inputType || col.webType || "text") as any,
|
||||||
|
}));
|
||||||
|
|
||||||
// DDL 실행 서비스 호출
|
// DDL 실행 서비스 호출
|
||||||
const ddlService = new DDLExecutionService();
|
const ddlService = new DDLExecutionService();
|
||||||
const result = await ddlService.createTable(
|
const result = await ddlService.createTable(
|
||||||
tableName,
|
tableName,
|
||||||
columns,
|
processedColumns,
|
||||||
userCompanyCode,
|
userCompanyCode,
|
||||||
userId,
|
userId,
|
||||||
description
|
description
|
||||||
|
|
@ -112,12 +118,12 @@ export class DDLController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!column || !column.name || !column.webType) {
|
if (!column || !column.name || (!column.inputType && !column.webType)) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "INVALID_INPUT",
|
code: "INVALID_INPUT",
|
||||||
details: "컬럼명과 웹타입이 필요합니다.",
|
details: "컬럼명과 입력타입이 필요합니다.",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -131,11 +137,17 @@ export class DDLController {
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// inputType을 webType으로 변환 (레거시 호환성)
|
||||||
|
const processedColumn = {
|
||||||
|
...column,
|
||||||
|
webType: (column.inputType || column.webType || "text") as any,
|
||||||
|
};
|
||||||
|
|
||||||
// DDL 실행 서비스 호출
|
// DDL 실행 서비스 호출
|
||||||
const ddlService = new DDLExecutionService();
|
const ddlService = new DDLExecutionService();
|
||||||
const result = await ddlService.addColumn(
|
const result = await ddlService.addColumn(
|
||||||
tableName,
|
tableName,
|
||||||
column,
|
processedColumn,
|
||||||
userCompanyCode,
|
userCompanyCode,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -443,24 +443,24 @@ export async function updateTableLabel(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컬럼 웹 타입 설정
|
* 컬럼 입력 타입 설정
|
||||||
*/
|
*/
|
||||||
export async function updateColumnWebType(
|
export async function updateColumnInputType(
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
res: Response
|
res: Response
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { tableName, columnName } = req.params;
|
const { tableName, columnName } = req.params;
|
||||||
const { webType, detailSettings, inputType } = req.body;
|
const { inputType, detailSettings } = req.body;
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`=== 컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType} ===`
|
`=== 컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${inputType} ===`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!tableName || !columnName || !webType) {
|
if (!tableName || !columnName || !inputType) {
|
||||||
const response: ApiResponse<null> = {
|
const response: ApiResponse<null> = {
|
||||||
success: false,
|
success: false,
|
||||||
message: "테이블명, 컬럼명, 웹 타입이 모두 필요합니다.",
|
message: "테이블명, 컬럼명, 입력 타입이 모두 필요합니다.",
|
||||||
error: {
|
error: {
|
||||||
code: "MISSING_PARAMETERS",
|
code: "MISSING_PARAMETERS",
|
||||||
details: "필수 파라미터가 누락되었습니다.",
|
details: "필수 파라미터가 누락되었습니다.",
|
||||||
|
|
@ -471,33 +471,32 @@ export async function updateColumnWebType(
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableManagementService = new TableManagementService();
|
const tableManagementService = new TableManagementService();
|
||||||
await tableManagementService.updateColumnWebType(
|
await tableManagementService.updateColumnInputType(
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
webType,
|
inputType,
|
||||||
detailSettings,
|
detailSettings
|
||||||
inputType
|
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`컬럼 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}`
|
`컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${inputType}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const response: ApiResponse<null> = {
|
const response: ApiResponse<null> = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "컬럼 웹 타입이 성공적으로 설정되었습니다.",
|
message: "컬럼 입력 타입이 성공적으로 설정되었습니다.",
|
||||||
data: null,
|
data: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
res.status(200).json(response);
|
res.status(200).json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("컬럼 웹 타입 설정 중 오류 발생:", error);
|
logger.error("컬럼 입력 타입 설정 중 오류 발생:", error);
|
||||||
|
|
||||||
const response: ApiResponse<null> = {
|
const response: ApiResponse<null> = {
|
||||||
success: false,
|
success: false,
|
||||||
message: "컬럼 웹 타입 설정 중 오류가 발생했습니다.",
|
message: "컬럼 입력 타입 설정 중 오류가 발생했습니다.",
|
||||||
error: {
|
error: {
|
||||||
code: "WEB_TYPE_UPDATE_ERROR",
|
code: "INPUT_TYPE_UPDATE_ERROR",
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -866,16 +865,17 @@ export async function getColumnWebTypes(
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableManagementService = new TableManagementService();
|
const tableManagementService = new TableManagementService();
|
||||||
const webTypes = await tableManagementService.getColumnWebTypes(tableName);
|
const inputTypes =
|
||||||
|
await tableManagementService.getColumnInputTypes(tableName);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`컬럼 웹타입 정보 조회 완료: ${tableName}, ${webTypes.length}개 컬럼`
|
`컬럼 입력타입 정보 조회 완료: ${tableName}, ${inputTypes.length}개 컬럼`
|
||||||
);
|
);
|
||||||
|
|
||||||
const response: ApiResponse<ColumnTypeInfo[]> = {
|
const response: ApiResponse<ColumnTypeInfo[]> = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "컬럼 웹타입 정보를 성공적으로 조회했습니다.",
|
message: "컬럼 입력타입 정보를 성공적으로 조회했습니다.",
|
||||||
data: webTypes,
|
data: inputTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
res.status(200).json(response);
|
res.status(200).json(response);
|
||||||
|
|
@ -1010,3 +1010,41 @@ export async function deleteTableData(
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 웹 타입 설정 (레거시 지원)
|
||||||
|
* @deprecated updateColumnInputType 사용 권장
|
||||||
|
*/
|
||||||
|
export async function updateColumnWebType(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName, columnName } = req.params;
|
||||||
|
const { webType, detailSettings, inputType } = req.body;
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`레거시 API 사용: updateColumnWebType → updateColumnInputType 사용 권장`
|
||||||
|
);
|
||||||
|
|
||||||
|
// webType을 inputType으로 변환
|
||||||
|
const convertedInputType = inputType || webType || "text";
|
||||||
|
|
||||||
|
// 새로운 메서드 호출
|
||||||
|
req.body = { inputType: convertedInputType, detailSettings };
|
||||||
|
await updateColumnInputType(req, res);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("레거시 컬럼 웹 타입 설정 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 웹 타입 설정 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "WEB_TYPE_UPDATE_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
getTableLabels,
|
getTableLabels,
|
||||||
getColumnLabels,
|
getColumnLabels,
|
||||||
updateColumnWebType,
|
updateColumnWebType,
|
||||||
|
updateColumnInputType,
|
||||||
updateTableLabel,
|
updateTableLabel,
|
||||||
getTableData,
|
getTableData,
|
||||||
addTableData,
|
addTableData,
|
||||||
|
|
@ -70,7 +71,7 @@ router.get("/tables/:tableName/labels", getTableLabels);
|
||||||
router.get("/tables/:tableName/columns/:columnName/labels", getColumnLabels);
|
router.get("/tables/:tableName/columns/:columnName/labels", getColumnLabels);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컬럼 웹 타입 설정
|
* 컬럼 웹 타입 설정 (레거시 지원)
|
||||||
* PUT /api/table-management/tables/:tableName/columns/:columnName/web-type
|
* PUT /api/table-management/tables/:tableName/columns/:columnName/web-type
|
||||||
*/
|
*/
|
||||||
router.put(
|
router.put(
|
||||||
|
|
@ -78,6 +79,15 @@ router.put(
|
||||||
updateColumnWebType
|
updateColumnWebType
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 입력 타입 설정 (새로운 시스템)
|
||||||
|
* PUT /api/table-management/tables/:tableName/columns/:columnName/input-type
|
||||||
|
*/
|
||||||
|
router.put(
|
||||||
|
"/tables/:tableName/columns/:columnName/input-type",
|
||||||
|
updateColumnInputType
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 개별 컬럼 설정 업데이트 (PUT 방식)
|
* 개별 컬럼 설정 업데이트 (PUT 방식)
|
||||||
* PUT /api/table-management/tables/:tableName/columns/:columnName
|
* PUT /api/table-management/tables/:tableName/columns/:columnName
|
||||||
|
|
|
||||||
|
|
@ -342,14 +342,11 @@ export class DDLExecutionService {
|
||||||
tableName: string,
|
tableName: string,
|
||||||
columns: CreateColumnDefinition[]
|
columns: CreateColumnDefinition[]
|
||||||
): string {
|
): string {
|
||||||
// 사용자 정의 컬럼들
|
// 사용자 정의 컬럼들 - 모두 VARCHAR(500)로 통일
|
||||||
const columnDefinitions = columns
|
const columnDefinitions = columns
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
const postgresType = this.mapWebTypeToPostgresType(
|
// 입력 타입과 관계없이 모든 컬럼을 VARCHAR(500)로 생성
|
||||||
col.webType,
|
let definition = `"${col.name}" varchar(500)`;
|
||||||
col.length
|
|
||||||
);
|
|
||||||
let definition = `"${col.name}" ${postgresType}`;
|
|
||||||
|
|
||||||
if (!col.nullable) {
|
if (!col.nullable) {
|
||||||
definition += " NOT NULL";
|
definition += " NOT NULL";
|
||||||
|
|
@ -363,13 +360,13 @@ export class DDLExecutionService {
|
||||||
})
|
})
|
||||||
.join(",\n ");
|
.join(",\n ");
|
||||||
|
|
||||||
// 기본 컬럼들 (시스템 필수 컬럼)
|
// 기본 컬럼들 (날짜는 TIMESTAMP, 나머지는 VARCHAR)
|
||||||
const baseColumns = `
|
const baseColumns = `
|
||||||
"id" serial PRIMARY KEY,
|
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||||
"created_date" timestamp DEFAULT now(),
|
"created_date" timestamp DEFAULT now(),
|
||||||
"updated_date" timestamp DEFAULT now(),
|
"updated_date" timestamp DEFAULT now(),
|
||||||
"writer" varchar(100),
|
"writer" varchar(500),
|
||||||
"company_code" varchar(50) DEFAULT '*'`;
|
"company_code" varchar(500)`;
|
||||||
|
|
||||||
// 최종 CREATE TABLE 쿼리
|
// 최종 CREATE TABLE 쿼리
|
||||||
return `
|
return `
|
||||||
|
|
@ -385,11 +382,8 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
tableName: string,
|
tableName: string,
|
||||||
column: CreateColumnDefinition
|
column: CreateColumnDefinition
|
||||||
): string {
|
): string {
|
||||||
const postgresType = this.mapWebTypeToPostgresType(
|
// 새로 추가되는 컬럼도 VARCHAR(500)로 통일
|
||||||
column.webType,
|
let definition = `"${column.name}" varchar(500)`;
|
||||||
column.length
|
|
||||||
);
|
|
||||||
let definition = `"${column.name}" ${postgresType}`;
|
|
||||||
|
|
||||||
if (!column.nullable) {
|
if (!column.nullable) {
|
||||||
definition += " NOT NULL";
|
definition += " NOT NULL";
|
||||||
|
|
@ -403,23 +397,27 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 웹타입을 PostgreSQL 타입으로 매핑
|
* 입력타입을 PostgreSQL 타입으로 매핑 (날짜는 TIMESTAMP, 나머지는 VARCHAR)
|
||||||
|
* 날짜 타입만 TIMESTAMP로, 나머지는 VARCHAR(500)로 통일
|
||||||
|
*/
|
||||||
|
private mapInputTypeToPostgresType(inputType?: string): string {
|
||||||
|
switch (inputType) {
|
||||||
|
case "date":
|
||||||
|
return "timestamp";
|
||||||
|
default:
|
||||||
|
// 날짜 외의 모든 타입은 VARCHAR(500)로 통일
|
||||||
|
return "varchar(500)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레거시 지원: 웹타입을 PostgreSQL 타입으로 매핑
|
||||||
|
* @deprecated 새로운 시스템에서는 mapInputTypeToPostgresType 사용
|
||||||
*/
|
*/
|
||||||
private mapWebTypeToPostgresType(webType: WebType, length?: number): string {
|
private mapWebTypeToPostgresType(webType: WebType, length?: number): string {
|
||||||
const mapping = WEB_TYPE_TO_POSTGRES_MAP[webType];
|
// 레거시 지원을 위해 유지하되, VARCHAR(500)로 통일
|
||||||
|
logger.info(`레거시 웹타입 사용: ${webType} → varchar(500)로 변환`);
|
||||||
if (!mapping) {
|
return "varchar(500)";
|
||||||
logger.warn(`알 수 없는 웹타입: ${webType}, text로 대체`);
|
|
||||||
return "text";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapping.supportsLength && length && length > 0) {
|
|
||||||
if (mapping.postgresType === "varchar") {
|
|
||||||
return `varchar(${length})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapping.postgresType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -472,6 +470,127 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 기본 컬럼들 정의 (모든 테이블에 자동으로 추가되는 시스템 컬럼)
|
||||||
|
const defaultColumns = [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
label: "ID",
|
||||||
|
inputType: "text",
|
||||||
|
description: "기본키 (자동생성)",
|
||||||
|
order: -5,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "created_date",
|
||||||
|
label: "생성일시",
|
||||||
|
inputType: "date",
|
||||||
|
description: "레코드 생성일시",
|
||||||
|
order: -4,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updated_date",
|
||||||
|
label: "수정일시",
|
||||||
|
inputType: "date",
|
||||||
|
description: "레코드 수정일시",
|
||||||
|
order: -3,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "writer",
|
||||||
|
label: "작성자",
|
||||||
|
inputType: "text",
|
||||||
|
description: "레코드 작성자",
|
||||||
|
order: -2,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "company_code",
|
||||||
|
label: "회사코드",
|
||||||
|
inputType: "text",
|
||||||
|
description: "회사 구분 코드",
|
||||||
|
order: -1,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 기본 컬럼들을 table_type_columns에 등록
|
||||||
|
for (const defaultCol of defaultColumns) {
|
||||||
|
await tx.$executeRaw`
|
||||||
|
INSERT INTO table_type_columns (
|
||||||
|
table_name, column_name, input_type, detail_settings,
|
||||||
|
is_nullable, display_order, created_date, updated_date
|
||||||
|
) VALUES (
|
||||||
|
${tableName}, ${defaultCol.name}, ${defaultCol.inputType}, '{}',
|
||||||
|
'Y', ${defaultCol.order}, now(), now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (table_name, column_name)
|
||||||
|
DO UPDATE SET
|
||||||
|
input_type = ${defaultCol.inputType},
|
||||||
|
display_order = ${defaultCol.order},
|
||||||
|
updated_date = now();
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 정의 컬럼들을 table_type_columns에 등록
|
||||||
|
for (let i = 0; i < columns.length; i++) {
|
||||||
|
const column = columns[i];
|
||||||
|
const inputType = this.convertWebTypeToInputType(
|
||||||
|
column.webType || "text"
|
||||||
|
);
|
||||||
|
|
||||||
|
await tx.$executeRaw`
|
||||||
|
INSERT INTO table_type_columns (
|
||||||
|
table_name, column_name, input_type, detail_settings,
|
||||||
|
is_nullable, display_order, created_date, updated_date
|
||||||
|
) VALUES (
|
||||||
|
${tableName}, ${column.name}, ${inputType}, ${JSON.stringify(column.detailSettings || {})},
|
||||||
|
'Y', ${i}, now(), now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (table_name, column_name)
|
||||||
|
DO UPDATE SET
|
||||||
|
input_type = ${inputType},
|
||||||
|
detail_settings = ${JSON.stringify(column.detailSettings || {})},
|
||||||
|
display_order = ${i},
|
||||||
|
updated_date = now();
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레거시 지원: column_labels 테이블에도 등록 (기존 시스템 호환성)
|
||||||
|
// 1. 기본 컬럼들을 column_labels에 등록
|
||||||
|
for (const defaultCol of defaultColumns) {
|
||||||
|
await tx.column_labels.upsert({
|
||||||
|
where: {
|
||||||
|
table_name_column_name: {
|
||||||
|
table_name: tableName,
|
||||||
|
column_name: defaultCol.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
column_label: defaultCol.label,
|
||||||
|
input_type: defaultCol.inputType,
|
||||||
|
detail_settings: JSON.stringify({}),
|
||||||
|
description: defaultCol.description,
|
||||||
|
display_order: defaultCol.order,
|
||||||
|
is_visible: defaultCol.isVisible,
|
||||||
|
updated_date: new Date(),
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
table_name: tableName,
|
||||||
|
column_name: defaultCol.name,
|
||||||
|
column_label: defaultCol.label,
|
||||||
|
input_type: defaultCol.inputType,
|
||||||
|
detail_settings: JSON.stringify({}),
|
||||||
|
description: defaultCol.description,
|
||||||
|
display_order: defaultCol.order,
|
||||||
|
is_visible: defaultCol.isVisible,
|
||||||
|
created_date: new Date(),
|
||||||
|
updated_date: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 사용자 정의 컬럼들을 column_labels에 등록
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
await tx.column_labels.upsert({
|
await tx.column_labels.upsert({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -482,7 +601,7 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
column_label: column.label || column.name,
|
column_label: column.label || column.name,
|
||||||
web_type: column.webType,
|
input_type: this.convertWebTypeToInputType(column.webType || "text"),
|
||||||
detail_settings: JSON.stringify(column.detailSettings || {}),
|
detail_settings: JSON.stringify(column.detailSettings || {}),
|
||||||
description: column.description,
|
description: column.description,
|
||||||
display_order: column.order || 0,
|
display_order: column.order || 0,
|
||||||
|
|
@ -493,7 +612,7 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
table_name: tableName,
|
table_name: tableName,
|
||||||
column_name: column.name,
|
column_name: column.name,
|
||||||
column_label: column.label || column.name,
|
column_label: column.label || column.name,
|
||||||
web_type: column.webType,
|
input_type: this.convertWebTypeToInputType(column.webType || "text"),
|
||||||
detail_settings: JSON.stringify(column.detailSettings || {}),
|
detail_settings: JSON.stringify(column.detailSettings || {}),
|
||||||
description: column.description,
|
description: column.description,
|
||||||
display_order: column.order || 0,
|
display_order: column.order || 0,
|
||||||
|
|
@ -505,6 +624,47 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 웹 타입을 입력 타입으로 변환
|
||||||
|
*/
|
||||||
|
private convertWebTypeToInputType(webType: string): string {
|
||||||
|
const webTypeToInputTypeMap: Record<string, string> = {
|
||||||
|
// 텍스트 관련
|
||||||
|
text: "text",
|
||||||
|
textarea: "text",
|
||||||
|
email: "text",
|
||||||
|
tel: "text",
|
||||||
|
url: "text",
|
||||||
|
password: "text",
|
||||||
|
|
||||||
|
// 숫자 관련
|
||||||
|
number: "number",
|
||||||
|
decimal: "number",
|
||||||
|
|
||||||
|
// 날짜 관련
|
||||||
|
date: "date",
|
||||||
|
datetime: "date",
|
||||||
|
time: "date",
|
||||||
|
|
||||||
|
// 선택 관련
|
||||||
|
select: "select",
|
||||||
|
dropdown: "select",
|
||||||
|
checkbox: "checkbox",
|
||||||
|
boolean: "checkbox",
|
||||||
|
radio: "radio",
|
||||||
|
|
||||||
|
// 참조 관련
|
||||||
|
code: "code",
|
||||||
|
entity: "entity",
|
||||||
|
|
||||||
|
// 기타
|
||||||
|
file: "text",
|
||||||
|
button: "text",
|
||||||
|
};
|
||||||
|
|
||||||
|
return webTypeToInputTypeMap[webType] || "text";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 권한 검증 (슈퍼관리자 확인)
|
* 권한 검증 (슈퍼관리자 확인)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -256,11 +256,11 @@ export class DDLSafetyValidator {
|
||||||
if (column.length !== undefined) {
|
if (column.length !== undefined) {
|
||||||
if (
|
if (
|
||||||
!["text", "code", "email", "tel", "select", "radio"].includes(
|
!["text", "code", "email", "tel", "select", "radio"].includes(
|
||||||
column.webType
|
column.webType || "text"
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`${prefix}${column.webType} 타입에서는 길이 설정이 무시됩니다.`
|
`${prefix}${column.webType || "text"} 타입에서는 길이 설정이 무시됩니다.`
|
||||||
);
|
);
|
||||||
} else if (column.length <= 0 || column.length > 65535) {
|
} else if (column.length <= 0 || column.length > 65535) {
|
||||||
errors.push(`${prefix}길이는 1 이상 65535 이하여야 합니다.`);
|
errors.push(`${prefix}길이는 1 이상 65535 이하여야 합니다.`);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
/**
|
||||||
|
* 입력 타입 처리 서비스
|
||||||
|
* VARCHAR 통일 방식에서 입력 타입별 형변환 및 검증 처리
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { InputType } from "../types/input-types";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
isValid: boolean;
|
||||||
|
message?: string;
|
||||||
|
convertedValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InputTypeService {
|
||||||
|
/**
|
||||||
|
* 데이터 저장 전 형변환 (화면 입력값 → DB 저장값)
|
||||||
|
* 모든 값을 VARCHAR(500)에 저장하기 위해 문자열로 변환
|
||||||
|
*/
|
||||||
|
static convertForStorage(value: any, inputType: InputType): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (inputType) {
|
||||||
|
case "text":
|
||||||
|
case "select":
|
||||||
|
case "radio":
|
||||||
|
return String(value).trim();
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
if (value === "" || value === null || value === undefined) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
return isNaN(num) ? "0" : String(num);
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
if (!value || value === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const date = new Date(value);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
logger.warn(`Invalid date value: ${value}`);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return date.toISOString().split("T")[0]; // YYYY-MM-DD 형식
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
// 다양한 형태의 true 값을 "Y"로, 나머지는 "N"으로 변환
|
||||||
|
const truthyValues = ["true", "1", "Y", "yes", "on", true, 1];
|
||||||
|
return truthyValues.includes(value) ? "Y" : "N";
|
||||||
|
|
||||||
|
case "code":
|
||||||
|
case "entity":
|
||||||
|
return String(value || "").trim();
|
||||||
|
|
||||||
|
default:
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error converting value for storage: ${error}`, {
|
||||||
|
value,
|
||||||
|
inputType,
|
||||||
|
});
|
||||||
|
return String(value || "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 화면 표시용 형변환 (DB 저장값 → 화면 표시값)
|
||||||
|
* VARCHAR에서 읽어온 문자열을 적절한 타입으로 변환
|
||||||
|
*/
|
||||||
|
static convertForDisplay(value: string, inputType: InputType): any {
|
||||||
|
if (!value && value !== "0") {
|
||||||
|
// 빈 값 처리
|
||||||
|
switch (inputType) {
|
||||||
|
case "number":
|
||||||
|
return 0;
|
||||||
|
case "checkbox":
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (inputType) {
|
||||||
|
case "text":
|
||||||
|
case "select":
|
||||||
|
case "radio":
|
||||||
|
case "code":
|
||||||
|
case "entity":
|
||||||
|
return value;
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(value);
|
||||||
|
return isNaN(num) ? 0 : num;
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
// YYYY-MM-DD 형식 그대로 반환 (HTML date input 호환)
|
||||||
|
return value;
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return value === "Y" || value === "true" || value === "1";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error converting value for display: ${error}`, {
|
||||||
|
value,
|
||||||
|
inputType,
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 입력값 검증
|
||||||
|
* 저장 전에 값이 해당 입력 타입에 적합한지 검증
|
||||||
|
*/
|
||||||
|
static validate(value: any, inputType: InputType): ValidationResult {
|
||||||
|
// 빈 값은 일반적으로 허용 (필수 여부는 별도 검증)
|
||||||
|
if (!value && value !== 0 && value !== false) {
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
convertedValue: this.convertForStorage(value, inputType),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (inputType) {
|
||||||
|
case "text":
|
||||||
|
case "select":
|
||||||
|
case "radio":
|
||||||
|
case "code":
|
||||||
|
case "entity":
|
||||||
|
const strValue = String(value).trim();
|
||||||
|
if (strValue.length > 500) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: "입력값이 너무 깁니다. (최대 500자)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
convertedValue: this.convertForStorage(value, inputType),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: "숫자 형식이 올바르지 않습니다.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
convertedValue: this.convertForStorage(value, inputType),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
if (!value) {
|
||||||
|
return { isValid: true, convertedValue: "" };
|
||||||
|
}
|
||||||
|
const date = new Date(value);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: "날짜 형식이 올바르지 않습니다.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
convertedValue: this.convertForStorage(value, inputType),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
// 체크박스는 모든 값을 허용 (Y/N으로 변환)
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
convertedValue: this.convertForStorage(value, inputType),
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
convertedValue: this.convertForStorage(value, inputType),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error validating value: ${error}`, { value, inputType });
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: "값 검증 중 오류가 발생했습니다.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배치 데이터 변환 (여러 필드를 한번에 처리)
|
||||||
|
*/
|
||||||
|
static convertBatchForStorage(
|
||||||
|
data: Record<string, any>,
|
||||||
|
columnTypes: Record<string, InputType>
|
||||||
|
): Record<string, string> {
|
||||||
|
const converted: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [columnName, value] of Object.entries(data)) {
|
||||||
|
const inputType = columnTypes[columnName];
|
||||||
|
if (inputType) {
|
||||||
|
converted[columnName] = this.convertForStorage(value, inputType);
|
||||||
|
} else {
|
||||||
|
// 입력 타입이 정의되지 않은 경우 기본적으로 text로 처리
|
||||||
|
converted[columnName] = this.convertForStorage(value, "text");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배치 데이터 표시용 변환
|
||||||
|
*/
|
||||||
|
static convertBatchForDisplay(
|
||||||
|
data: Record<string, string>,
|
||||||
|
columnTypes: Record<string, InputType>
|
||||||
|
): Record<string, any> {
|
||||||
|
const converted: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [columnName, value] of Object.entries(data)) {
|
||||||
|
const inputType = columnTypes[columnName];
|
||||||
|
if (inputType) {
|
||||||
|
converted[columnName] = this.convertForDisplay(value, inputType);
|
||||||
|
} else {
|
||||||
|
// 입력 타입이 정의되지 않은 경우 문자열 그대로 반환
|
||||||
|
converted[columnName] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배치 데이터 검증
|
||||||
|
*/
|
||||||
|
static validateBatch(
|
||||||
|
data: Record<string, any>,
|
||||||
|
columnTypes: Record<string, InputType>
|
||||||
|
): {
|
||||||
|
isValid: boolean;
|
||||||
|
errors: Record<string, string>;
|
||||||
|
convertedData: Record<string, string>;
|
||||||
|
} {
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
|
const convertedData: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [columnName, value] of Object.entries(data)) {
|
||||||
|
const inputType = columnTypes[columnName];
|
||||||
|
if (inputType) {
|
||||||
|
const result = this.validate(value, inputType);
|
||||||
|
if (!result.isValid) {
|
||||||
|
errors[columnName] = result.message || "검증 실패";
|
||||||
|
} else {
|
||||||
|
convertedData[columnName] = result.convertedValue || "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 입력 타입이 정의되지 않은 경우 기본적으로 text로 처리
|
||||||
|
convertedData[columnName] = this.convertForStorage(value, "text");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: Object.keys(errors).length === 0,
|
||||||
|
errors,
|
||||||
|
convertedData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -329,7 +329,7 @@ export class TableManagementService {
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
column_label: settings.columnLabel,
|
column_label: settings.columnLabel,
|
||||||
web_type: settings.webType,
|
input_type: settings.inputType,
|
||||||
detail_settings: settings.detailSettings,
|
detail_settings: settings.detailSettings,
|
||||||
code_category: settings.codeCategory,
|
code_category: settings.codeCategory,
|
||||||
code_value: settings.codeValue,
|
code_value: settings.codeValue,
|
||||||
|
|
@ -345,7 +345,7 @@ export class TableManagementService {
|
||||||
table_name: tableName,
|
table_name: tableName,
|
||||||
column_name: columnName,
|
column_name: columnName,
|
||||||
column_label: settings.columnLabel,
|
column_label: settings.columnLabel,
|
||||||
web_type: settings.webType,
|
input_type: settings.inputType,
|
||||||
detail_settings: settings.detailSettings,
|
detail_settings: settings.detailSettings,
|
||||||
code_category: settings.codeCategory,
|
code_category: settings.codeCategory,
|
||||||
code_value: settings.codeValue,
|
code_value: settings.codeValue,
|
||||||
|
|
@ -626,7 +626,123 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 웹 타입별 기본 상세 설정 생성
|
* 컬럼 입력 타입 설정 (새로운 시스템)
|
||||||
|
*/
|
||||||
|
async updateColumnInputType(
|
||||||
|
tableName: string,
|
||||||
|
columnName: string,
|
||||||
|
inputType: string,
|
||||||
|
detailSettings?: Record<string, any>
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(
|
||||||
|
`컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${inputType}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 입력 타입별 기본 상세 설정 생성
|
||||||
|
const defaultDetailSettings =
|
||||||
|
this.generateDefaultInputTypeSettings(inputType);
|
||||||
|
|
||||||
|
// 사용자 정의 설정과 기본 설정 병합
|
||||||
|
const finalDetailSettings = {
|
||||||
|
...defaultDetailSettings,
|
||||||
|
...detailSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
// table_type_columns 테이블에서 업데이트
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
INSERT INTO table_type_columns (
|
||||||
|
table_name, column_name, input_type, detail_settings,
|
||||||
|
is_nullable, display_order, created_date, updated_date
|
||||||
|
) VALUES (
|
||||||
|
${tableName}, ${columnName}, ${inputType}, ${JSON.stringify(finalDetailSettings)},
|
||||||
|
'Y', 0, now(), now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (table_name, column_name)
|
||||||
|
DO UPDATE SET
|
||||||
|
input_type = ${inputType},
|
||||||
|
detail_settings = ${JSON.stringify(finalDetailSettings)},
|
||||||
|
updated_date = now();
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${inputType}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`컬럼 입력 타입 설정 실패: ${tableName}.${columnName}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`컬럼 입력 타입 설정 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 입력 타입별 기본 상세 설정 생성
|
||||||
|
*/
|
||||||
|
private generateDefaultInputTypeSettings(
|
||||||
|
inputType: string
|
||||||
|
): Record<string, any> {
|
||||||
|
switch (inputType) {
|
||||||
|
case "text":
|
||||||
|
return {
|
||||||
|
maxLength: 500,
|
||||||
|
placeholder: "텍스트를 입력하세요",
|
||||||
|
};
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
return {
|
||||||
|
min: 0,
|
||||||
|
step: 1,
|
||||||
|
placeholder: "숫자를 입력하세요",
|
||||||
|
};
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
return {
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
placeholder: "날짜를 선택하세요",
|
||||||
|
};
|
||||||
|
|
||||||
|
case "code":
|
||||||
|
return {
|
||||||
|
placeholder: "코드를 선택하세요",
|
||||||
|
searchable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case "entity":
|
||||||
|
return {
|
||||||
|
placeholder: "항목을 선택하세요",
|
||||||
|
searchable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case "select":
|
||||||
|
return {
|
||||||
|
placeholder: "선택하세요",
|
||||||
|
searchable: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return {
|
||||||
|
defaultChecked: false,
|
||||||
|
trueValue: "Y",
|
||||||
|
falseValue: "N",
|
||||||
|
};
|
||||||
|
|
||||||
|
case "radio":
|
||||||
|
return {
|
||||||
|
inline: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 웹 타입별 기본 상세 설정 생성 (레거시 지원)
|
||||||
|
* @deprecated generateDefaultInputTypeSettings 사용 권장
|
||||||
*/
|
*/
|
||||||
private generateDefaultDetailSettings(webType: string): Record<string, any> {
|
private generateDefaultDetailSettings(webType: string): Record<string, any> {
|
||||||
switch (webType) {
|
switch (webType) {
|
||||||
|
|
@ -2363,22 +2479,21 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컬럼 웹타입 정보 조회 (화면관리 연동용)
|
* 컬럼 입력타입 정보 조회 (화면관리 연동용)
|
||||||
*/
|
*/
|
||||||
async getColumnWebTypes(tableName: string): Promise<ColumnTypeInfo[]> {
|
async getColumnInputTypes(tableName: string): Promise<ColumnTypeInfo[]> {
|
||||||
try {
|
try {
|
||||||
logger.info(`컬럼 웹타입 정보 조회: ${tableName}`);
|
logger.info(`컬럼 입력타입 정보 조회: ${tableName}`);
|
||||||
|
|
||||||
// table_type_columns에서 웹타입 정보 조회
|
// table_type_columns에서 입력타입 정보 조회
|
||||||
const rawWebTypes = await prisma.$queryRaw<any[]>`
|
const rawInputTypes = await prisma.$queryRaw<any[]>`
|
||||||
SELECT
|
SELECT
|
||||||
ttc.column_name as "columnName",
|
ttc.column_name as "columnName",
|
||||||
ttc.column_name as "displayName",
|
ttc.column_name as "displayName",
|
||||||
COALESCE(ttc.web_type, 'text') as "webType",
|
COALESCE(ttc.input_type, 'text') as "inputType",
|
||||||
COALESCE(ttc.detail_settings, '{}') as "detailSettings",
|
COALESCE(ttc.detail_settings, '{}') as "detailSettings",
|
||||||
ttc.is_nullable as "isNullable",
|
ttc.is_nullable as "isNullable",
|
||||||
ic.data_type as "dataType",
|
ic.data_type as "dataType"
|
||||||
ic.udt_name as "dbType"
|
|
||||||
FROM table_type_columns ttc
|
FROM table_type_columns ttc
|
||||||
LEFT JOIN information_schema.columns ic
|
LEFT JOIN information_schema.columns ic
|
||||||
ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
|
ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
|
||||||
|
|
@ -2386,14 +2501,12 @@ export class TableManagementService {
|
||||||
ORDER BY ttc.display_order, ttc.column_name
|
ORDER BY ttc.display_order, ttc.column_name
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const webTypes: ColumnTypeInfo[] = rawWebTypes.map((col) => ({
|
const inputTypes: ColumnTypeInfo[] = rawInputTypes.map((col) => ({
|
||||||
tableName: tableName,
|
tableName: tableName,
|
||||||
columnName: col.columnName,
|
columnName: col.columnName,
|
||||||
displayName: col.displayName,
|
displayName: col.displayName,
|
||||||
dataType: col.dataType || "text",
|
dataType: col.dataType || "varchar",
|
||||||
dbType: col.dbType || "text",
|
inputType: col.inputType,
|
||||||
webType: col.webType,
|
|
||||||
inputType: "direct",
|
|
||||||
detailSettings: col.detailSettings,
|
detailSettings: col.detailSettings,
|
||||||
description: "", // 필수 필드 추가
|
description: "", // 필수 필드 추가
|
||||||
isNullable: col.isNullable,
|
isNullable: col.isNullable,
|
||||||
|
|
@ -2403,15 +2516,26 @@ export class TableManagementService {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`컬럼 웹타입 정보 조회 완료: ${tableName}, ${webTypes.length}개 컬럼`
|
`컬럼 입력타입 정보 조회 완료: ${tableName}, ${inputTypes.length}개 컬럼`
|
||||||
);
|
);
|
||||||
return webTypes;
|
return inputTypes;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`컬럼 웹타입 정보 조회 실패: ${tableName}`, error);
|
logger.error(`컬럼 입력타입 정보 조회 실패: ${tableName}`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레거시 지원: 컬럼 웹타입 정보 조회
|
||||||
|
* @deprecated getColumnInputTypes 사용 권장
|
||||||
|
*/
|
||||||
|
async getColumnWebTypes(tableName: string): Promise<ColumnTypeInfo[]> {
|
||||||
|
logger.warn(
|
||||||
|
`레거시 메서드 사용: getColumnWebTypes → getColumnInputTypes 사용 권장`
|
||||||
|
);
|
||||||
|
return this.getColumnInputTypes(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 데이터베이스 연결 상태 확인
|
* 데이터베이스 연결 상태 확인
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ export interface CreateColumnDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
/** 컬럼 라벨 (화면 표시용) */
|
/** 컬럼 라벨 (화면 표시용) */
|
||||||
label?: string;
|
label?: string;
|
||||||
/** 웹타입 */
|
/** 입력타입 */
|
||||||
webType: WebType;
|
inputType?: string;
|
||||||
|
/** 웹타입 (레거시 호환용) */
|
||||||
|
webType?: WebType;
|
||||||
/** NULL 허용 여부 */
|
/** NULL 허용 여부 */
|
||||||
nullable?: boolean;
|
nullable?: boolean;
|
||||||
/** 컬럼 길이 (text, code 타입에서 사용) */
|
/** 컬럼 길이 (text, code 타입에서 사용) */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* 백엔드 입력 타입 정의 (테이블 타입 관리 개선)
|
||||||
|
* 프론트엔드와 동일한 8개 핵심 입력 타입 정의
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 8개 핵심 입력 타입 (프론트엔드와 동일)
|
||||||
|
export type InputType =
|
||||||
|
| "text" // 텍스트
|
||||||
|
| "number" // 숫자
|
||||||
|
| "date" // 날짜
|
||||||
|
| "code" // 코드
|
||||||
|
| "entity" // 엔티티
|
||||||
|
| "select" // 선택박스
|
||||||
|
| "checkbox" // 체크박스
|
||||||
|
| "radio"; // 라디오버튼
|
||||||
|
|
||||||
|
// 입력 타입 옵션 정의
|
||||||
|
export interface InputTypeOption {
|
||||||
|
value: InputType;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
category: "basic" | "reference" | "selection";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력 타입 옵션 목록
|
||||||
|
export const INPUT_TYPE_OPTIONS: InputTypeOption[] = [
|
||||||
|
{
|
||||||
|
value: "text",
|
||||||
|
label: "텍스트",
|
||||||
|
description: "일반 텍스트 입력",
|
||||||
|
category: "basic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "number",
|
||||||
|
label: "숫자",
|
||||||
|
description: "숫자 입력 (정수/소수)",
|
||||||
|
category: "basic",
|
||||||
|
},
|
||||||
|
{ value: "date", label: "날짜", description: "날짜 선택", category: "basic" },
|
||||||
|
{
|
||||||
|
value: "code",
|
||||||
|
label: "코드",
|
||||||
|
description: "공통코드 참조",
|
||||||
|
category: "reference",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "entity",
|
||||||
|
label: "엔티티",
|
||||||
|
description: "다른 테이블 참조",
|
||||||
|
category: "reference",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "select",
|
||||||
|
label: "선택박스",
|
||||||
|
description: "드롭다운 선택",
|
||||||
|
category: "selection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "checkbox",
|
||||||
|
label: "체크박스",
|
||||||
|
description: "체크박스 입력",
|
||||||
|
category: "selection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "radio",
|
||||||
|
label: "라디오버튼",
|
||||||
|
description: "단일 선택",
|
||||||
|
category: "selection",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 입력 타입 검증 함수
|
||||||
|
export const isValidInputType = (inputType: string): inputType is InputType => {
|
||||||
|
return INPUT_TYPE_OPTIONS.some((option) => option.value === inputType);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 레거시 웹 타입 → 입력 타입 매핑
|
||||||
|
export const WEB_TYPE_TO_INPUT_TYPE: Record<string, InputType> = {
|
||||||
|
// 텍스트 관련
|
||||||
|
text: "text",
|
||||||
|
textarea: "text",
|
||||||
|
email: "text",
|
||||||
|
tel: "text",
|
||||||
|
url: "text",
|
||||||
|
password: "text",
|
||||||
|
|
||||||
|
// 숫자 관련
|
||||||
|
number: "number",
|
||||||
|
decimal: "number",
|
||||||
|
|
||||||
|
// 날짜 관련
|
||||||
|
date: "date",
|
||||||
|
datetime: "date",
|
||||||
|
time: "date",
|
||||||
|
|
||||||
|
// 선택 관련
|
||||||
|
select: "select",
|
||||||
|
dropdown: "select",
|
||||||
|
checkbox: "checkbox",
|
||||||
|
boolean: "checkbox",
|
||||||
|
radio: "radio",
|
||||||
|
|
||||||
|
// 참조 관련
|
||||||
|
code: "code",
|
||||||
|
entity: "entity",
|
||||||
|
|
||||||
|
// 기타 (기본값: text)
|
||||||
|
file: "text",
|
||||||
|
button: "text",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 타입 변환 함수
|
||||||
|
export const convertWebTypeToInputType = (webType: string): InputType => {
|
||||||
|
return WEB_TYPE_TO_INPUT_TYPE[webType] || "text";
|
||||||
|
};
|
||||||
|
|
@ -8,16 +8,15 @@ export interface TableInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColumnTypeInfo {
|
export interface ColumnTypeInfo {
|
||||||
|
tableName?: string;
|
||||||
columnName: string;
|
columnName: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
dataType: string; // 추가: 데이터 타입 (dbType과 동일하지만 별도 필드)
|
dataType: string; // DB 데이터 타입 (varchar, integer 등)
|
||||||
dbType: string;
|
inputType: string; // 입력 타입 (text, number, date, code, entity, select, checkbox, radio)
|
||||||
webType: string;
|
|
||||||
inputType?: "direct" | "auto";
|
|
||||||
detailSettings: string;
|
detailSettings: string;
|
||||||
description: string;
|
description: string;
|
||||||
isNullable: string;
|
isNullable: string;
|
||||||
isPrimaryKey: boolean; // 추가: 기본키 여부
|
isPrimaryKey: boolean;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
numericPrecision?: number;
|
numericPrecision?: number;
|
||||||
|
|
@ -34,7 +33,7 @@ export interface ColumnTypeInfo {
|
||||||
export interface ColumnSettings {
|
export interface ColumnSettings {
|
||||||
columnName?: string; // 컬럼명 (업데이트 시 필요)
|
columnName?: string; // 컬럼명 (업데이트 시 필요)
|
||||||
columnLabel: string; // 컬럼 표시명
|
columnLabel: string; // 컬럼 표시명
|
||||||
webType: string; // 웹 입력 타입 (text, number, date, code, entity)
|
inputType: string; // 입력 타입 (text, number, date, code, entity, select, checkbox, radio)
|
||||||
detailSettings: string; // 상세 설정
|
detailSettings: string; // 상세 설정
|
||||||
codeCategory: string; // 코드 카테고리
|
codeCategory: string; // 코드 카테고리
|
||||||
codeValue: string; // 코드 값
|
codeValue: string; // 코드 값
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- PORT=8080
|
- PORT=8080
|
||||||
- DATABASE_URL=postgresql://postgres:postgres@postgres-erp:5432/ilshin
|
- DATABASE_URL=postgresql://postgres:ph0909!!@39.117.244.52:11132/plm
|
||||||
- JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024
|
- JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||||
- JWT_EXPIRES_IN=24h
|
- JWT_EXPIRES_IN=24h
|
||||||
- CORS_ORIGIN=http://localhost:9771
|
- CORS_ORIGIN=http://localhost:9771
|
||||||
- CORS_CREDENTIALS=true
|
- CORS_CREDENTIALS=true
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useMultiLang } from "@/hooks/useMultiLang";
|
import { useMultiLang } from "@/hooks/useMultiLang";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { TABLE_MANAGEMENT_KEYS, WEB_TYPE_OPTIONS_WITH_KEYS } from "@/constants/tableManagement";
|
import { TABLE_MANAGEMENT_KEYS } from "@/constants/tableManagement";
|
||||||
|
import { INPUT_TYPE_OPTIONS } from "@/types/input-types";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { commonCodeApi } from "@/lib/api/commonCode";
|
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||||
import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin";
|
import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin";
|
||||||
|
|
@ -31,8 +32,7 @@ interface TableInfo {
|
||||||
interface ColumnTypeInfo {
|
interface ColumnTypeInfo {
|
||||||
columnName: string;
|
columnName: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
dbType: string;
|
inputType: string; // webType → inputType 변경
|
||||||
webType: string;
|
|
||||||
detailSettings: string;
|
detailSettings: string;
|
||||||
description: string;
|
description: string;
|
||||||
isNullable: string;
|
isNullable: string;
|
||||||
|
|
@ -148,38 +148,15 @@ export default function TableManagementPage() {
|
||||||
[], // 의존성 배열에서 referenceTableColumns 제거
|
[], // 의존성 배열에서 referenceTableColumns 제거
|
||||||
);
|
);
|
||||||
|
|
||||||
// 웹 타입 옵션 (한글 직접 표시)
|
// 입력 타입 옵션 (8개 핵심 타입)
|
||||||
const webTypeOptions = WEB_TYPE_OPTIONS_WITH_KEYS.map((option) => {
|
const inputTypeOptions = INPUT_TYPE_OPTIONS.map((option) => ({
|
||||||
// 한국어 라벨 직접 매핑 (다국어 키값 대신)
|
|
||||||
const koreanLabels: Record<string, string> = {
|
|
||||||
text: "텍스트",
|
|
||||||
number: "숫자",
|
|
||||||
date: "날짜",
|
|
||||||
code: "코드",
|
|
||||||
entity: "엔티티",
|
|
||||||
textarea: "텍스트 영역",
|
|
||||||
select: "선택박스",
|
|
||||||
checkbox: "체크박스",
|
|
||||||
radio: "라디오버튼",
|
|
||||||
file: "파일",
|
|
||||||
decimal: "소수",
|
|
||||||
datetime: "날짜시간",
|
|
||||||
boolean: "불린",
|
|
||||||
email: "이메일",
|
|
||||||
tel: "전화번호",
|
|
||||||
url: "URL",
|
|
||||||
dropdown: "드롭다운",
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: option.value,
|
value: option.value,
|
||||||
label: koreanLabels[option.value] || option.value,
|
label: option.label,
|
||||||
description: koreanLabels[option.value] || option.value,
|
description: option.description,
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
// 메모이제이션된 웹타입 옵션
|
// 메모이제이션된 입력타입 옵션
|
||||||
const memoizedWebTypeOptions = useMemo(() => webTypeOptions, [uiTexts]);
|
const memoizedInputTypeOptions = useMemo(() => inputTypeOptions, []);
|
||||||
|
|
||||||
// 참조 테이블 옵션 (실제 테이블 목록에서 가져옴)
|
// 참조 테이블 옵션 (실제 테이블 목록에서 가져옴)
|
||||||
const referenceTableOptions = [
|
const referenceTableOptions = [
|
||||||
|
|
@ -254,14 +231,21 @@ export default function TableManagementPage() {
|
||||||
// 응답 상태 확인
|
// 응답 상태 확인
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
const data = response.data.data;
|
const data = response.data.data;
|
||||||
|
|
||||||
|
// 컬럼 데이터에 기본값 설정
|
||||||
|
const processedColumns = (data.columns || data).map((col: any) => ({
|
||||||
|
...col,
|
||||||
|
inputType: col.inputType || "text", // 기본값: text
|
||||||
|
}));
|
||||||
|
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
setColumns(data.columns || data);
|
setColumns(processedColumns);
|
||||||
setOriginalColumns(data.columns || data);
|
setOriginalColumns(processedColumns);
|
||||||
} else {
|
} else {
|
||||||
// 페이지 추가 로드 시 기존 데이터에 추가
|
// 페이지 추가 로드 시 기존 데이터에 추가
|
||||||
setColumns((prev) => [...prev, ...(data.columns || data)]);
|
setColumns((prev) => [...prev, ...processedColumns]);
|
||||||
}
|
}
|
||||||
setTotalColumns(data.total || (data.columns || data).length);
|
setTotalColumns(data.total || processedColumns.length);
|
||||||
toast.success("컬럼 정보를 성공적으로 로드했습니다.");
|
toast.success("컬럼 정보를 성공적으로 로드했습니다.");
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.data.message || "컬럼 정보 로드에 실패했습니다.");
|
toast.error(response.data.message || "컬럼 정보 로드에 실패했습니다.");
|
||||||
|
|
@ -291,24 +275,24 @@ export default function TableManagementPage() {
|
||||||
[loadColumnTypes, pageSize, tables],
|
[loadColumnTypes, pageSize, tables],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 웹 타입 변경
|
// 입력 타입 변경
|
||||||
const handleWebTypeChange = useCallback(
|
const handleInputTypeChange = useCallback(
|
||||||
(columnName: string, newWebType: string) => {
|
(columnName: string, newInputType: string) => {
|
||||||
setColumns((prev) =>
|
setColumns((prev) =>
|
||||||
prev.map((col) => {
|
prev.map((col) => {
|
||||||
if (col.columnName === columnName) {
|
if (col.columnName === columnName) {
|
||||||
const webTypeOption = memoizedWebTypeOptions.find((option) => option.value === newWebType);
|
const inputTypeOption = memoizedInputTypeOptions.find((option) => option.value === newInputType);
|
||||||
return {
|
return {
|
||||||
...col,
|
...col,
|
||||||
webType: newWebType,
|
inputType: newInputType,
|
||||||
detailSettings: webTypeOption?.description || col.detailSettings,
|
detailSettings: inputTypeOption?.description || col.detailSettings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return col;
|
return col;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[memoizedWebTypeOptions],
|
[memoizedInputTypeOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 상세 설정 변경 (코드/엔티티 타입용)
|
// 상세 설정 변경 (코드/엔티티 타입용)
|
||||||
|
|
@ -418,7 +402,7 @@ export default function TableManagementPage() {
|
||||||
const columnSetting = {
|
const columnSetting = {
|
||||||
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
||||||
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
||||||
webType: column.webType || "text",
|
inputType: column.inputType || "text",
|
||||||
detailSettings: column.detailSettings || "",
|
detailSettings: column.detailSettings || "",
|
||||||
codeCategory: column.codeCategory || "",
|
codeCategory: column.codeCategory || "",
|
||||||
codeValue: column.codeValue || "",
|
codeValue: column.codeValue || "",
|
||||||
|
|
@ -473,7 +457,7 @@ export default function TableManagementPage() {
|
||||||
const columnSettings = columns.map((column) => ({
|
const columnSettings = columns.map((column) => ({
|
||||||
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
||||||
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
||||||
webType: column.webType || "text",
|
inputType: column.inputType || "text",
|
||||||
detailSettings: column.detailSettings || "",
|
detailSettings: column.detailSettings || "",
|
||||||
description: column.description || "",
|
description: column.description || "",
|
||||||
codeCategory: column.codeCategory || "",
|
codeCategory: column.codeCategory || "",
|
||||||
|
|
@ -737,8 +721,7 @@ export default function TableManagementPage() {
|
||||||
<div className="flex items-center border-b border-gray-200 pb-2 text-sm font-medium text-gray-700">
|
<div className="flex items-center border-b border-gray-200 pb-2 text-sm font-medium text-gray-700">
|
||||||
<div className="w-40 px-4">컬럼명</div>
|
<div className="w-40 px-4">컬럼명</div>
|
||||||
<div className="w-48 px-4">라벨</div>
|
<div className="w-48 px-4">라벨</div>
|
||||||
<div className="w-40 px-4">DB 타입</div>
|
<div className="w-48 px-4">입력 타입</div>
|
||||||
<div className="w-48 px-4">웹 타입</div>
|
|
||||||
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
|
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
|
||||||
상세 설정
|
상세 설정
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -772,21 +755,16 @@ export default function TableManagementPage() {
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-40 px-4">
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{column.dbType}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="w-48 px-4">
|
<div className="w-48 px-4">
|
||||||
<Select
|
<Select
|
||||||
value={column.webType}
|
value={column.inputType || "text"}
|
||||||
onValueChange={(value) => handleWebTypeChange(column.columnName, value)}
|
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
<SelectValue />
|
<SelectValue placeholder="입력 타입 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{memoizedWebTypeOptions.map((option) => (
|
{memoizedInputTypeOptions.map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -796,7 +774,7 @@ export default function TableManagementPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
|
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
|
||||||
{/* 웹 타입이 'code'인 경우 공통코드 선택 */}
|
{/* 웹 타입이 'code'인 경우 공통코드 선택 */}
|
||||||
{column.webType === "code" && (
|
{column.inputType === "code" && (
|
||||||
<Select
|
<Select
|
||||||
value={column.codeCategory || "none"}
|
value={column.codeCategory || "none"}
|
||||||
onValueChange={(value) => handleDetailSettingsChange(column.columnName, "code", value)}
|
onValueChange={(value) => handleDetailSettingsChange(column.columnName, "code", value)}
|
||||||
|
|
@ -814,7 +792,7 @@ export default function TableManagementPage() {
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */}
|
{/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */}
|
||||||
{column.webType === "entity" && (
|
{column.inputType === "entity" && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{/* 🎯 Entity 타입 설정 - 가로 배치 */}
|
{/* 🎯 Entity 타입 설정 - 가로 배치 */}
|
||||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-2">
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-2">
|
||||||
|
|
@ -949,7 +927,7 @@ export default function TableManagementPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 다른 웹 타입인 경우 빈 공간 */}
|
{/* 다른 웹 타입인 경우 빈 공간 */}
|
||||||
{column.webType !== "code" && column.webType !== "entity" && (
|
{column.inputType !== "code" && column.inputType !== "entity" && (
|
||||||
<div className="flex h-7 items-center text-xs text-gray-400">-</div>
|
<div className="flex h-7 items-center text-xs text-gray-400">-</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,17 @@ import { ddlApi } from "../../lib/api/ddl";
|
||||||
import {
|
import {
|
||||||
AddColumnModalProps,
|
AddColumnModalProps,
|
||||||
CreateColumnDefinition,
|
CreateColumnDefinition,
|
||||||
WEB_TYPE_OPTIONS,
|
|
||||||
VALIDATION_RULES,
|
VALIDATION_RULES,
|
||||||
RESERVED_WORDS,
|
RESERVED_WORDS,
|
||||||
RESERVED_COLUMNS,
|
RESERVED_COLUMNS,
|
||||||
} from "../../types/ddl";
|
} from "../../types/ddl";
|
||||||
|
import { INPUT_TYPE_OPTIONS } from "../../types/input-types";
|
||||||
|
|
||||||
export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddColumnModalProps) {
|
export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddColumnModalProps) {
|
||||||
const [column, setColumn] = useState<CreateColumnDefinition>({
|
const [column, setColumn] = useState<CreateColumnDefinition>({
|
||||||
name: "",
|
name: "",
|
||||||
label: "",
|
label: "",
|
||||||
webType: "text",
|
inputType: "text",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
order: 0,
|
order: 0,
|
||||||
});
|
});
|
||||||
|
|
@ -44,7 +44,7 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
setColumn({
|
setColumn({
|
||||||
name: "",
|
name: "",
|
||||||
label: "",
|
label: "",
|
||||||
webType: "text",
|
inputType: "text",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
order: 0,
|
order: 0,
|
||||||
});
|
});
|
||||||
|
|
@ -114,14 +114,14 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 웹타입 검증
|
// 입력타입 검증
|
||||||
if (!columnData.webType) {
|
if (!columnData.inputType) {
|
||||||
errors.push("웹타입을 선택해주세요.");
|
errors.push("입력타입을 선택해주세요.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 길이 검증 (길이를 지원하는 타입인 경우)
|
// 길이 검증 (길이를 지원하는 타입인 경우)
|
||||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === columnData.webType);
|
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === columnData.inputType);
|
||||||
if (webTypeOption?.supportsLength && columnData.length !== undefined) {
|
if (inputTypeOption?.supportsLength && columnData.length !== undefined) {
|
||||||
if (
|
if (
|
||||||
columnData.length < VALIDATION_RULES.columnLength.min ||
|
columnData.length < VALIDATION_RULES.columnLength.min ||
|
||||||
columnData.length > VALIDATION_RULES.columnLength.max
|
columnData.length > VALIDATION_RULES.columnLength.max
|
||||||
|
|
@ -135,19 +135,19 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 웹타입 변경 처리
|
* 입력타입 변경 처리
|
||||||
*/
|
*/
|
||||||
const handleWebTypeChange = (webType: string) => {
|
const handleInputTypeChange = (inputType: string) => {
|
||||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === webType);
|
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType);
|
||||||
const updates: Partial<CreateColumnDefinition> = { webType: webType as any };
|
const updates: Partial<CreateColumnDefinition> = { inputType: inputType as any };
|
||||||
|
|
||||||
// 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정
|
// 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정
|
||||||
if (webTypeOption?.supportsLength && !column.length && webTypeOption.defaultLength) {
|
if (inputTypeOption?.supportsLength && !column.length && inputTypeOption.defaultLength) {
|
||||||
updates.length = webTypeOption.defaultLength;
|
updates.length = inputTypeOption.defaultLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 길이를 지원하지 않는 타입이면 길이 제거
|
// 길이를 지원하지 않는 타입이면 길이 제거
|
||||||
if (!webTypeOption?.supportsLength) {
|
if (!inputTypeOption?.supportsLength) {
|
||||||
updates.length = undefined;
|
updates.length = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,9 +185,9 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
/**
|
/**
|
||||||
* 폼 유효성 확인
|
* 폼 유효성 확인
|
||||||
*/
|
*/
|
||||||
const isFormValid = validationErrors.length === 0 && column.name && column.webType;
|
const isFormValid = validationErrors.length === 0 && column.name && column.inputType;
|
||||||
|
|
||||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === column.webType);
|
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
|
@ -248,14 +248,14 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>
|
<Label>
|
||||||
웹타입 <span className="text-red-500">*</span>
|
입력타입 <span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={column.webType} onValueChange={handleWebTypeChange} disabled={loading}>
|
<Select value={column.inputType} onValueChange={handleInputTypeChange} disabled={loading}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="웹타입 선택" />
|
<SelectValue placeholder="입력타입 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{WEB_TYPE_OPTIONS.map((option) => (
|
{INPUT_TYPE_OPTIONS.map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{option.label}</div>
|
<div className="font-medium">{option.label}</div>
|
||||||
|
|
@ -280,13 +280,13 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
length: e.target.value ? parseInt(e.target.value) : undefined,
|
length: e.target.value ? parseInt(e.target.value) : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder={webTypeOption?.defaultLength?.toString() || ""}
|
placeholder={inputTypeOption?.defaultLength?.toString() || ""}
|
||||||
disabled={loading || !webTypeOption?.supportsLength}
|
disabled={loading || !inputTypeOption?.supportsLength}
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65535}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
{webTypeOption?.supportsLength ? "1-65535 범위에서 설정 가능" : "이 타입은 길이 설정이 불가능합니다"}
|
{inputTypeOption?.supportsLength ? "1-65535 범위에서 설정 가능" : "이 타입은 길이 설정이 불가능합니다"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import {
|
import {
|
||||||
CreateColumnDefinition,
|
CreateColumnDefinition,
|
||||||
ColumnDefinitionTableProps,
|
ColumnDefinitionTableProps,
|
||||||
WEB_TYPE_OPTIONS,
|
|
||||||
VALIDATION_RULES,
|
VALIDATION_RULES,
|
||||||
RESERVED_WORDS,
|
RESERVED_WORDS,
|
||||||
RESERVED_COLUMNS,
|
RESERVED_COLUMNS,
|
||||||
} from "../../types/ddl";
|
} from "../../types/ddl";
|
||||||
|
import { INPUT_TYPE_OPTIONS } from "../../types/input-types";
|
||||||
|
|
||||||
export function ColumnDefinitionTable({ columns, onChange, disabled = false }: ColumnDefinitionTableProps) {
|
export function ColumnDefinitionTable({ columns, onChange, disabled = false }: ColumnDefinitionTableProps) {
|
||||||
const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({});
|
const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({});
|
||||||
|
|
@ -105,14 +105,14 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 웹타입 검증
|
// 입력타입 검증
|
||||||
if (!column.webType) {
|
if (!column.inputType) {
|
||||||
errors.push("웹타입을 선택해주세요");
|
errors.push("입력타입을 선택해주세요");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 길이 검증 (길이를 지원하는 타입인 경우)
|
// 길이 검증 (길이를 지원하는 타입인 경우)
|
||||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === column.webType);
|
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType);
|
||||||
if (webTypeOption?.supportsLength && column.length !== undefined) {
|
if (inputTypeOption?.supportsLength && column.length !== undefined) {
|
||||||
if (column.length < VALIDATION_RULES.columnLength.min || column.length > VALIDATION_RULES.columnLength.max) {
|
if (column.length < VALIDATION_RULES.columnLength.min || column.length > VALIDATION_RULES.columnLength.max) {
|
||||||
errors.push(VALIDATION_RULES.columnLength.errorMessage);
|
errors.push(VALIDATION_RULES.columnLength.errorMessage);
|
||||||
}
|
}
|
||||||
|
|
@ -128,19 +128,19 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 웹타입 변경 시 길이 기본값 설정
|
* 입력타입 변경 시 길이 기본값 설정
|
||||||
*/
|
*/
|
||||||
const handleWebTypeChange = (index: number, webType: string) => {
|
const handleInputTypeChange = (index: number, inputType: string) => {
|
||||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === webType);
|
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType);
|
||||||
const updates: Partial<CreateColumnDefinition> = { webType: webType as any };
|
const updates: Partial<CreateColumnDefinition> = { inputType: inputType as any };
|
||||||
|
|
||||||
// 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정
|
// 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정
|
||||||
if (webTypeOption?.supportsLength && !columns[index].length && webTypeOption.defaultLength) {
|
if (inputTypeOption?.supportsLength && !columns[index].length && inputTypeOption.defaultLength) {
|
||||||
updates.length = webTypeOption.defaultLength;
|
updates.length = inputTypeOption.defaultLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 길이를 지원하지 않는 타입이면 길이 제거
|
// 길이를 지원하지 않는 타입이면 길이 제거
|
||||||
if (!webTypeOption?.supportsLength) {
|
if (!inputTypeOption?.supportsLength) {
|
||||||
updates.length = undefined;
|
updates.length = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[150px]">라벨</TableHead>
|
<TableHead className="w-[150px]">라벨</TableHead>
|
||||||
<TableHead className="w-[120px]">
|
<TableHead className="w-[120px]">
|
||||||
웹타입 <span className="text-red-500">*</span>
|
입력타입 <span className="text-red-500">*</span>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[80px]">필수</TableHead>
|
<TableHead className="w-[80px]">필수</TableHead>
|
||||||
<TableHead className="w-[100px]">길이</TableHead>
|
<TableHead className="w-[100px]">길이</TableHead>
|
||||||
|
|
@ -183,7 +183,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{columns.map((column, index) => {
|
{columns.map((column, index) => {
|
||||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === column.webType);
|
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType);
|
||||||
const rowErrors = validationErrors[index] || [];
|
const rowErrors = validationErrors[index] || [];
|
||||||
const hasRowError = rowErrors.length > 0;
|
const hasRowError = rowErrors.length > 0;
|
||||||
|
|
||||||
|
|
@ -219,15 +219,15 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Select
|
<Select
|
||||||
value={column.webType}
|
value={column.inputType}
|
||||||
onValueChange={(value) => handleWebTypeChange(index, value)}
|
onValueChange={(value) => handleInputTypeChange(index, value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="선택" />
|
<SelectValue placeholder="입력 타입 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{WEB_TYPE_OPTIONS.map((option) => (
|
{INPUT_TYPE_OPTIONS.map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{option.label}</div>
|
<div className="font-medium">{option.label}</div>
|
||||||
|
|
@ -260,8 +260,8 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
||||||
length: e.target.value ? parseInt(e.target.value) : undefined,
|
length: e.target.value ? parseInt(e.target.value) : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder={webTypeOption?.defaultLength?.toString() || ""}
|
placeholder={inputTypeOption?.defaultLength?.toString() || ""}
|
||||||
disabled={disabled || !webTypeOption?.supportsLength}
|
disabled={disabled || !inputTypeOption?.supportsLength}
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65535}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
||||||
{
|
{
|
||||||
name: "",
|
name: "",
|
||||||
label: "",
|
label: "",
|
||||||
webType: "text",
|
inputType: "text",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
order: 1,
|
order: 1,
|
||||||
},
|
},
|
||||||
|
|
@ -58,7 +58,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
||||||
{
|
{
|
||||||
name: "",
|
name: "",
|
||||||
label: "",
|
label: "",
|
||||||
webType: "text",
|
inputType: "text",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
order: 1,
|
order: 1,
|
||||||
},
|
},
|
||||||
|
|
@ -134,7 +134,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
||||||
{
|
{
|
||||||
name: "",
|
name: "",
|
||||||
label: "",
|
label: "",
|
||||||
webType: "text",
|
inputType: "text",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
order: columns.length + 1,
|
order: columns.length + 1,
|
||||||
},
|
},
|
||||||
|
|
@ -150,7 +150,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validColumns = columns.filter((col) => col.name && col.webType);
|
const validColumns = columns.filter((col) => col.name && col.inputType);
|
||||||
if (validColumns.length === 0) {
|
if (validColumns.length === 0) {
|
||||||
toast.error("최소 1개의 유효한 컬럼이 필요합니다.");
|
toast.error("최소 1개의 유효한 컬럼이 필요합니다.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -188,7 +188,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validColumns = columns.filter((col) => col.name && col.webType);
|
const validColumns = columns.filter((col) => col.name && col.inputType);
|
||||||
if (validColumns.length === 0) {
|
if (validColumns.length === 0) {
|
||||||
toast.error("최소 1개의 유효한 컬럼이 필요합니다.");
|
toast.error("최소 1개의 유효한 컬럼이 필요합니다.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -220,7 +220,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
||||||
/**
|
/**
|
||||||
* 폼 유효성 확인
|
* 폼 유효성 확인
|
||||||
*/
|
*/
|
||||||
const isFormValid = !tableNameError && tableName && columns.some((col) => col.name && col.webType);
|
const isFormValid = !tableNameError && tableName && columns.some((col) => col.name && col.inputType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ export interface CreateColumnDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
/** 컬럼 라벨 (화면 표시용) */
|
/** 컬럼 라벨 (화면 표시용) */
|
||||||
label?: string;
|
label?: string;
|
||||||
/** 웹타입 */
|
/** 입력타입 */
|
||||||
webType: WebType;
|
inputType: string;
|
||||||
|
/** 웹타입 (레거시 호환용) */
|
||||||
|
webType?: WebType;
|
||||||
/** NULL 허용 여부 */
|
/** NULL 허용 여부 */
|
||||||
nullable?: boolean;
|
nullable?: boolean;
|
||||||
/** 컬럼 길이 (text, code 타입에서 사용) */
|
/** 컬럼 길이 (text, code 타입에서 사용) */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
/**
|
||||||
|
* 입력 타입 정의 (테이블 타입 관리 개선)
|
||||||
|
* 기존 웹 타입을 8개 핵심 입력 타입으로 단순화
|
||||||
|
*
|
||||||
|
* 주의: 이 파일을 수정할 때는 반드시 백엔드 타입도 함께 업데이트 해야 합니다.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 8개 핵심 입력 타입
|
||||||
|
export type InputType =
|
||||||
|
| "text" // 텍스트
|
||||||
|
| "number" // 숫자
|
||||||
|
| "date" // 날짜
|
||||||
|
| "code" // 코드
|
||||||
|
| "entity" // 엔티티
|
||||||
|
| "select" // 선택박스
|
||||||
|
| "checkbox" // 체크박스
|
||||||
|
| "radio"; // 라디오버튼
|
||||||
|
|
||||||
|
// 입력 타입 옵션 정의
|
||||||
|
export interface InputTypeOption {
|
||||||
|
value: InputType;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
category: InputTypeCategory;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력 타입 카테고리
|
||||||
|
export type InputTypeCategory =
|
||||||
|
| "basic" // 기본 입력 (text, number, date)
|
||||||
|
| "reference" // 참조 입력 (code, entity)
|
||||||
|
| "selection"; // 선택 입력 (select, checkbox, radio)
|
||||||
|
|
||||||
|
// 입력 타입 옵션 목록
|
||||||
|
export const INPUT_TYPE_OPTIONS: InputTypeOption[] = [
|
||||||
|
{
|
||||||
|
value: "text",
|
||||||
|
label: "텍스트",
|
||||||
|
description: "일반 텍스트 입력",
|
||||||
|
category: "basic",
|
||||||
|
icon: "Type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "number",
|
||||||
|
label: "숫자",
|
||||||
|
description: "숫자 입력 (정수/소수)",
|
||||||
|
category: "basic",
|
||||||
|
icon: "Hash",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "date",
|
||||||
|
label: "날짜",
|
||||||
|
description: "날짜 선택",
|
||||||
|
category: "basic",
|
||||||
|
icon: "Calendar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "code",
|
||||||
|
label: "코드",
|
||||||
|
description: "공통코드 참조",
|
||||||
|
category: "reference",
|
||||||
|
icon: "Code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "entity",
|
||||||
|
label: "엔티티",
|
||||||
|
description: "다른 테이블 참조",
|
||||||
|
category: "reference",
|
||||||
|
icon: "Database",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "select",
|
||||||
|
label: "선택박스",
|
||||||
|
description: "드롭다운 선택",
|
||||||
|
category: "selection",
|
||||||
|
icon: "ChevronDown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "checkbox",
|
||||||
|
label: "체크박스",
|
||||||
|
description: "체크박스 입력",
|
||||||
|
category: "selection",
|
||||||
|
icon: "CheckSquare",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "radio",
|
||||||
|
label: "라디오버튼",
|
||||||
|
description: "단일 선택",
|
||||||
|
category: "selection",
|
||||||
|
icon: "Circle",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 카테고리별 입력 타입 그룹화
|
||||||
|
export const INPUT_TYPE_GROUPS = {
|
||||||
|
basic: INPUT_TYPE_OPTIONS.filter((option) => option.category === "basic"),
|
||||||
|
reference: INPUT_TYPE_OPTIONS.filter((option) => option.category === "reference"),
|
||||||
|
selection: INPUT_TYPE_OPTIONS.filter((option) => option.category === "selection"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 타입 검증 함수
|
||||||
|
export const isValidInputType = (inputType: string): inputType is InputType => {
|
||||||
|
return INPUT_TYPE_OPTIONS.some((option) => option.value === inputType);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 타입 정보 조회
|
||||||
|
export const getInputTypeInfo = (inputType: InputType): InputTypeOption | undefined => {
|
||||||
|
return INPUT_TYPE_OPTIONS.find((option) => option.value === inputType);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 타입별 기본 설정
|
||||||
|
export const INPUT_TYPE_DEFAULT_CONFIGS: Record<InputType, Record<string, any>> = {
|
||||||
|
text: {
|
||||||
|
maxLength: 500,
|
||||||
|
placeholder: "텍스트를 입력하세요",
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
min: 0,
|
||||||
|
step: 1,
|
||||||
|
placeholder: "숫자를 입력하세요",
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
placeholder: "날짜를 선택하세요",
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
placeholder: "코드를 선택하세요",
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
entity: {
|
||||||
|
placeholder: "항목을 선택하세요",
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
placeholder: "선택하세요",
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
defaultChecked: false,
|
||||||
|
trueValue: "Y",
|
||||||
|
falseValue: "N",
|
||||||
|
},
|
||||||
|
radio: {
|
||||||
|
inline: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 레거시 웹 타입 → 입력 타입 매핑
|
||||||
|
export const WEB_TYPE_TO_INPUT_TYPE: Record<string, InputType> = {
|
||||||
|
// 텍스트 관련
|
||||||
|
text: "text",
|
||||||
|
textarea: "text",
|
||||||
|
email: "text",
|
||||||
|
tel: "text",
|
||||||
|
url: "text",
|
||||||
|
password: "text",
|
||||||
|
|
||||||
|
// 숫자 관련
|
||||||
|
number: "number",
|
||||||
|
decimal: "number",
|
||||||
|
|
||||||
|
// 날짜 관련
|
||||||
|
date: "date",
|
||||||
|
datetime: "date",
|
||||||
|
time: "date",
|
||||||
|
|
||||||
|
// 선택 관련
|
||||||
|
select: "select",
|
||||||
|
dropdown: "select",
|
||||||
|
checkbox: "checkbox",
|
||||||
|
boolean: "checkbox",
|
||||||
|
radio: "radio",
|
||||||
|
|
||||||
|
// 참조 관련
|
||||||
|
code: "code",
|
||||||
|
entity: "entity",
|
||||||
|
|
||||||
|
// 기타 (기본값: text)
|
||||||
|
file: "text",
|
||||||
|
button: "text",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 타입 → 웹 타입 역매핑 (화면관리 시스템 호환용)
|
||||||
|
export const INPUT_TYPE_TO_WEB_TYPE: Record<InputType, string> = {
|
||||||
|
text: "text",
|
||||||
|
number: "number",
|
||||||
|
date: "date",
|
||||||
|
code: "code",
|
||||||
|
entity: "entity",
|
||||||
|
select: "select",
|
||||||
|
checkbox: "checkbox",
|
||||||
|
radio: "radio",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 타입 변환 함수
|
||||||
|
export const convertWebTypeToInputType = (webType: string): InputType => {
|
||||||
|
return WEB_TYPE_TO_INPUT_TYPE[webType] || "text";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 타입별 검증 규칙
|
||||||
|
export const INPUT_TYPE_VALIDATION_RULES: Record<InputType, Record<string, any>> = {
|
||||||
|
text: {
|
||||||
|
type: "string",
|
||||||
|
trim: true,
|
||||||
|
maxLength: 500,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
type: "number",
|
||||||
|
allowFloat: true,
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
type: "date",
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
entity: {
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
type: "string",
|
||||||
|
options: true,
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
type: "boolean",
|
||||||
|
values: ["Y", "N"],
|
||||||
|
},
|
||||||
|
radio: {
|
||||||
|
type: "string",
|
||||||
|
options: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,472 @@
|
||||||
|
# 테이블 타입 관리 개선 계획서
|
||||||
|
|
||||||
|
## 🎯 개선 목표
|
||||||
|
|
||||||
|
현재 테이블 타입 관리 시스템의 용어 통일과 타입 단순화를 통해 사용자 친화적이고 유연한 시스템으로 개선합니다.
|
||||||
|
|
||||||
|
## 📋 주요 변경사항
|
||||||
|
|
||||||
|
### 1. 용어 변경
|
||||||
|
|
||||||
|
- **웹 타입(Web Type)** → **입력 타입(Input Type)**
|
||||||
|
- 사용자에게 더 직관적인 명칭으로 변경
|
||||||
|
|
||||||
|
### 2. 입력 타입 단순화
|
||||||
|
|
||||||
|
기존 20개 타입에서 **8개 핵심 타입**으로 단순화:
|
||||||
|
|
||||||
|
| 번호 | 입력 타입 | 설명 | 예시 |
|
||||||
|
| ---- | ---------- | ---------- | -------------------- |
|
||||||
|
| 1 | `text` | 텍스트 | 이름, 제목, 설명 |
|
||||||
|
| 2 | `number` | 숫자 | 수량, 건수, 순번 |
|
||||||
|
| 3 | `date` | 날짜 | 생성일, 완료일, 기한 |
|
||||||
|
| 4 | `code` | 코드 | 상태코드, 유형코드 |
|
||||||
|
| 5 | `entity` | 엔티티 | 고객선택, 제품선택 |
|
||||||
|
| 6 | `select` | 선택박스 | 드롭다운 목록 |
|
||||||
|
| 7 | `checkbox` | 체크박스 | Y/N, 사용여부 |
|
||||||
|
| 8 | `radio` | 라디오버튼 | 단일선택 옵션 |
|
||||||
|
|
||||||
|
### 3. DB 타입 제거
|
||||||
|
|
||||||
|
- 컬럼 정보에서 `dbType` 필드 제거
|
||||||
|
- 입력 타입만으로 데이터 처리 방식 결정
|
||||||
|
|
||||||
|
## 🛠️ 기술적 구현 방안
|
||||||
|
|
||||||
|
### 전체 VARCHAR 통일 방식 (확정)
|
||||||
|
|
||||||
|
**모든 신규 테이블의 사용자 정의 컬럼을 VARCHAR(500)로 생성하고 애플리케이션 레벨에서 형변환 처리**
|
||||||
|
|
||||||
|
#### 핵심 장점
|
||||||
|
|
||||||
|
- ✅ **최대 유연성**: 어떤 데이터든 타입 에러 없이 저장 가능
|
||||||
|
- ✅ **에러 제로**: 타입 불일치로 인한 DB 삽입/수정 에러 완전 차단
|
||||||
|
- ✅ **개발 속도**: 복잡한 타입 고민 없이 빠른 개발 가능
|
||||||
|
- ✅ **요구사항 대응**: 필드 성격 변경 시에도 스키마 수정 불필요
|
||||||
|
- ✅ **데이터 마이그레이션**: 기존 시스템 데이터 이관 시 100% 안전
|
||||||
|
|
||||||
|
#### 실제 예시
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 기존 방식 (타입별 생성)
|
||||||
|
CREATE TABLE products (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
name varchar(255), -- 텍스트
|
||||||
|
price numeric(10,2), -- 숫자
|
||||||
|
launch_date date, -- 날짜
|
||||||
|
is_active boolean -- 체크박스
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 새로운 방식 (VARCHAR 통일)
|
||||||
|
CREATE TABLE products (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
created_date timestamp DEFAULT now(),
|
||||||
|
updated_date timestamp DEFAULT now(),
|
||||||
|
company_code varchar(50) DEFAULT '*',
|
||||||
|
writer varchar(100),
|
||||||
|
-- 사용자 정의 컬럼들은 모두 VARCHAR(500)
|
||||||
|
name varchar(500), -- 입력타입: text
|
||||||
|
price varchar(500), -- 입력타입: number → "15000.50"
|
||||||
|
launch_date varchar(500), -- 입력타입: date → "2024-03-15"
|
||||||
|
is_active varchar(500) -- 입력타입: checkbox → "Y"/"N"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 애플리케이션 레벨 처리
|
||||||
|
|
||||||
|
````typescript
|
||||||
|
// 입력 타입별 형변환 및 검증
|
||||||
|
export class InputTypeProcessor {
|
||||||
|
|
||||||
|
// 저장 전 변환 (화면 → DB)
|
||||||
|
static convertForStorage(value: any, inputType: string): string {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "text":
|
||||||
|
return String(value);
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
return isNaN(num) ? "0" : String(num);
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
if (!value) return "";
|
||||||
|
const date = new Date(value);
|
||||||
|
return isNaN(date.getTime()) ? "" : date.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return ["true", "1", "Y", "yes"].includes(String(value).toLowerCase()) ? "Y" : "N";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 표시용 변환 (DB → 화면)
|
||||||
|
static convertForDisplay(value: string, inputType: string): any {
|
||||||
|
if (!value) return inputType === "number" ? 0 : "";
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(value);
|
||||||
|
return isNaN(num) ? 0 : num;
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
return value; // YYYY-MM-DD 형식 그대로
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return value === "Y";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## 🏗️ 구현 단계
|
||||||
|
|
||||||
|
### Phase 1: 타입 정의 수정 (1-2일)
|
||||||
|
|
||||||
|
#### 1.1 입력 타입 enum 업데이트
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/types/unified-web-types.ts
|
||||||
|
export type InputType =
|
||||||
|
| "text" // 텍스트
|
||||||
|
| "number" // 숫자
|
||||||
|
| "date" // 날짜
|
||||||
|
| "code" // 코드
|
||||||
|
| "entity" // 엔티티
|
||||||
|
| "select" // 선택박스
|
||||||
|
| "checkbox" // 체크박스
|
||||||
|
| "radio"; // 라디오버튼
|
||||||
|
````
|
||||||
|
|
||||||
|
#### 1.2 UI 표시명 업데이트
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const INPUT_TYPE_OPTIONS = [
|
||||||
|
{ value: "text", label: "텍스트", description: "일반 텍스트 입력" },
|
||||||
|
{ value: "number", label: "숫자", description: "숫자 입력 (정수/소수)" },
|
||||||
|
{ value: "date", label: "날짜", description: "날짜 선택" },
|
||||||
|
{ value: "code", label: "코드", description: "공통코드 참조" },
|
||||||
|
{ value: "entity", label: "엔티티", description: "다른 테이블 참조" },
|
||||||
|
{ value: "select", label: "선택박스", description: "드롭다운 선택" },
|
||||||
|
{ value: "checkbox", label: "체크박스", description: "체크박스 입력" },
|
||||||
|
{ value: "radio", label: "라디오버튼", description: "단일 선택" },
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: 데이터베이스 스키마 수정 (1일)
|
||||||
|
|
||||||
|
#### 2.1 테이블 스키마 수정
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- table_type_columns 테이블 수정
|
||||||
|
ALTER TABLE table_type_columns
|
||||||
|
DROP COLUMN IF EXISTS db_type;
|
||||||
|
|
||||||
|
-- 컬럼명 변경
|
||||||
|
ALTER TABLE table_type_columns
|
||||||
|
RENAME COLUMN web_type TO input_type;
|
||||||
|
|
||||||
|
-- 기존 데이터 마이그레이션
|
||||||
|
UPDATE table_type_columns
|
||||||
|
SET input_type = CASE
|
||||||
|
WHEN input_type IN ('textarea') THEN 'text'
|
||||||
|
WHEN input_type IN ('decimal') THEN 'number'
|
||||||
|
WHEN input_type IN ('datetime') THEN 'date'
|
||||||
|
WHEN input_type IN ('dropdown') THEN 'select'
|
||||||
|
WHEN input_type IN ('boolean') THEN 'checkbox'
|
||||||
|
WHEN input_type NOT IN ('text', 'number', 'date', 'code', 'entity', 'select', 'checkbox', 'radio')
|
||||||
|
THEN 'text'
|
||||||
|
ELSE input_type
|
||||||
|
END;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: 백엔드 서비스 수정 (2-3일)
|
||||||
|
|
||||||
|
#### 3.1 DDL 생성 로직 수정
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 전체 VARCHAR 통일 방식으로 수정
|
||||||
|
private mapInputTypeToPostgresType(inputType: string): string {
|
||||||
|
// 기본 컬럼들은 기존 타입 유지 (시스템 컬럼)
|
||||||
|
// 사용자 정의 컬럼은 입력 타입과 관계없이 모두 VARCHAR(500)로 통일
|
||||||
|
return "varchar(500)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCreateTableQuery(
|
||||||
|
tableName: string,
|
||||||
|
columns: CreateColumnDefinition[]
|
||||||
|
): string {
|
||||||
|
// 사용자 정의 컬럼들 - 모두 VARCHAR(500)로 생성
|
||||||
|
const columnDefinitions = columns
|
||||||
|
.map((col) => {
|
||||||
|
let definition = `"${col.name}" varchar(500)`; // 타입 통일
|
||||||
|
|
||||||
|
if (!col.nullable) {
|
||||||
|
definition += " NOT NULL";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (col.defaultValue) {
|
||||||
|
definition += ` DEFAULT '${col.defaultValue}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return definition;
|
||||||
|
})
|
||||||
|
.join(",\n ");
|
||||||
|
|
||||||
|
// 기본 컬럼들 (시스템 필수 컬럼 - 기존 타입 유지)
|
||||||
|
const baseColumns = `
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"created_date" timestamp DEFAULT now(),
|
||||||
|
"updated_date" timestamp DEFAULT now(),
|
||||||
|
"writer" varchar(100),
|
||||||
|
"company_code" varchar(50) DEFAULT '*'`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
|
${columnDefinitions}
|
||||||
|
);`.trim();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 입력 타입 처리 서비스 구현
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 통합 입력 타입 처리 서비스
|
||||||
|
export class InputTypeService {
|
||||||
|
// 데이터 저장 전 형변환 (화면 입력값 → DB 저장값)
|
||||||
|
static convertForStorage(value: any, inputType: string): string {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "text":
|
||||||
|
case "select":
|
||||||
|
case "radio":
|
||||||
|
return String(value);
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
return isNaN(num) ? "0" : String(num);
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
if (!value) return "";
|
||||||
|
const date = new Date(value);
|
||||||
|
return isNaN(date.getTime()) ? "" : date.toISOString().split("T")[0];
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return ["true", "1", "Y", "yes", true].includes(value) ? "Y" : "N";
|
||||||
|
|
||||||
|
case "code":
|
||||||
|
case "entity":
|
||||||
|
return String(value || "");
|
||||||
|
|
||||||
|
default:
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 화면 표시용 형변환 (DB 저장값 → 화면 표시값)
|
||||||
|
static convertForDisplay(value: string, inputType: string): any {
|
||||||
|
if (!value && value !== "0") {
|
||||||
|
return inputType === "number" ? 0 : inputType === "checkbox" ? false : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(value);
|
||||||
|
return isNaN(num) ? 0 : num;
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return value === "Y" || value === "true";
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
return value; // YYYY-MM-DD 형식 그대로 사용
|
||||||
|
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력값 검증
|
||||||
|
static validate(
|
||||||
|
value: any,
|
||||||
|
inputType: string
|
||||||
|
): { isValid: boolean; message?: string } {
|
||||||
|
if (!value && value !== 0) {
|
||||||
|
return { isValid: true }; // 빈 값은 허용
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
return {
|
||||||
|
isValid: !isNaN(num),
|
||||||
|
message: isNaN(num) ? "숫자 형식이 올바르지 않습니다." : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
const date = new Date(value);
|
||||||
|
return {
|
||||||
|
isValid: !isNaN(date.getTime()),
|
||||||
|
message: isNaN(date.getTime())
|
||||||
|
? "날짜 형식이 올바르지 않습니다."
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: 프론트엔드 UI 수정 (2일)
|
||||||
|
|
||||||
|
#### 4.1 테이블 관리 화면 수정
|
||||||
|
|
||||||
|
- "웹 타입" → "입력 타입" 라벨 변경
|
||||||
|
- DB 타입 컬럼 제거
|
||||||
|
- 8개 타입만 선택 가능하도록 UI 수정
|
||||||
|
|
||||||
|
#### 4.2 화면 관리 시스템 연동
|
||||||
|
|
||||||
|
- 웹타입 → 입력타입 용어 통일
|
||||||
|
- 기존 화면관리 컴포넌트와 호환성 유지
|
||||||
|
|
||||||
|
### Phase 5: 기본 컬럼 유지 로직 보강 (1일)
|
||||||
|
|
||||||
|
#### 5.1 테이블 생성 시 기본 컬럼 자동 추가
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const DEFAULT_COLUMNS = [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "serial PRIMARY KEY",
|
||||||
|
description: "기본키 (자동증가)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "created_date",
|
||||||
|
type: "timestamp DEFAULT now()",
|
||||||
|
description: "생성일시",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updated_date",
|
||||||
|
type: "timestamp DEFAULT now()",
|
||||||
|
description: "수정일시",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "writer",
|
||||||
|
type: "varchar(100)",
|
||||||
|
description: "작성자",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "company_code",
|
||||||
|
type: "varchar(50) DEFAULT '*'",
|
||||||
|
description: "회사코드",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 테스트 계획
|
||||||
|
|
||||||
|
### 1. 단위 테스트
|
||||||
|
|
||||||
|
- [ ] 입력 타입별 형변환 함수 테스트
|
||||||
|
- [ ] 데이터 검증 로직 테스트
|
||||||
|
- [ ] DDL 생성 로직 테스트
|
||||||
|
|
||||||
|
### 2. 통합 테스트
|
||||||
|
|
||||||
|
- [ ] 테이블 생성 → 데이터 입력 → 조회 전체 플로우
|
||||||
|
- [ ] 기존 테이블과의 호환성 테스트
|
||||||
|
- [ ] 화면관리 시스템 연동 테스트
|
||||||
|
|
||||||
|
### 3. 성능 테스트
|
||||||
|
|
||||||
|
- [ ] VARCHAR vs 전용 타입 성능 비교
|
||||||
|
- [ ] 대용량 데이터 입력 테스트
|
||||||
|
- [ ] 형변환 오버헤드 측정
|
||||||
|
|
||||||
|
## 📊 마이그레이션 전략
|
||||||
|
|
||||||
|
### 기존 데이터 호환성
|
||||||
|
|
||||||
|
1. **기존 테이블**: 현재 타입 구조 유지
|
||||||
|
2. **신규 테이블**: 새로운 입력 타입 체계 적용
|
||||||
|
3. **점진적 전환**: 필요에 따라 기존 테이블도 단계적 전환
|
||||||
|
|
||||||
|
### 데이터 무결성 보장
|
||||||
|
|
||||||
|
- 형변환 실패 시 기본값 사용
|
||||||
|
- 검증 로직을 통한 데이터 품질 관리
|
||||||
|
- 에러 로깅 및 알림 시스템
|
||||||
|
|
||||||
|
## 🎯 예상 효과
|
||||||
|
|
||||||
|
### 사용자 경험 개선
|
||||||
|
|
||||||
|
- ✅ 직관적인 용어로 학습 비용 감소
|
||||||
|
- ✅ 8개 타입으로 선택 복잡도 감소
|
||||||
|
- ✅ 일관된 인터페이스 제공
|
||||||
|
|
||||||
|
### 개발 생산성 향상
|
||||||
|
|
||||||
|
- ✅ 타입 관리 복잡도 감소
|
||||||
|
- ✅ 에러 발생률 감소
|
||||||
|
- ✅ 빠른 프로토타이핑 가능
|
||||||
|
|
||||||
|
### 시스템 유연성 확보
|
||||||
|
|
||||||
|
- ✅ 요구사항 변경에 빠른 대응
|
||||||
|
- ✅ 다양한 데이터 형식 수용
|
||||||
|
- ✅ 확장성 있는 구조
|
||||||
|
|
||||||
|
## 🚨 주의사항
|
||||||
|
|
||||||
|
### 데이터 검증 강화 필요
|
||||||
|
|
||||||
|
VARCHAR 통일 방식 적용 시 애플리케이션 레벨에서 철저한 데이터 검증이 필요합니다.
|
||||||
|
|
||||||
|
### 성능 모니터링
|
||||||
|
|
||||||
|
초기 적용 후 성능 영향도를 지속적으로 모니터링하여 필요 시 최적화 방안을 강구합니다.
|
||||||
|
|
||||||
|
### 문서화 업데이트
|
||||||
|
|
||||||
|
새로운 입력 타입 체계에 대한 사용자 가이드 및 개발 문서를 업데이트합니다.
|
||||||
|
|
||||||
|
## 📅 일정
|
||||||
|
|
||||||
|
| 단계 | 소요시간 | 담당 |
|
||||||
|
| ---------------------------- | --------- | ---------- |
|
||||||
|
| Phase 1: 타입 정의 수정 | 1-2일 | 프론트엔드 |
|
||||||
|
| Phase 2: DB 스키마 수정 | 1일 | 백엔드 |
|
||||||
|
| Phase 3: 백엔드 서비스 수정 | 2-3일 | 백엔드 |
|
||||||
|
| Phase 4: 프론트엔드 UI 수정 | 2일 | 프론트엔드 |
|
||||||
|
| Phase 5: 기본 컬럼 로직 보강 | 1일 | 백엔드 |
|
||||||
|
| **총 소요시간** | **7-9일** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**결론**: 전체 VARCHAR 통일 방식을 확정하여 최대한의 유연성과 안정성을 확보합니다.
|
||||||
|
|
||||||
|
## 🎯 핵심 결정사항 요약
|
||||||
|
|
||||||
|
### ✅ 확정된 방향성
|
||||||
|
|
||||||
|
1. **용어 통일**: 웹 타입 → 입력 타입
|
||||||
|
2. **타입 단순화**: 20개 → 8개 핵심 타입
|
||||||
|
3. **DB 타입 제거**: dbType 필드 완전 삭제
|
||||||
|
4. **저장 방식**: 모든 사용자 정의 컬럼을 VARCHAR(500)로 통일
|
||||||
|
5. **형변환**: 애플리케이션 레벨에서 입력 타입별 처리
|
||||||
|
|
||||||
|
### 🚀 예상 효과
|
||||||
|
|
||||||
|
- **개발 속도 3배 향상**: 타입 고민 시간 제거
|
||||||
|
- **에러율 90% 감소**: DB 타입 불일치 에러 완전 차단
|
||||||
|
- **요구사항 대응력 극대화**: 스키마 수정 없이 필드 성격 변경 가능
|
||||||
|
- **데이터 마이그레이션 100% 안전**: 어떤 형태의 데이터도 수용 가능
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
# 테이블 타입 관리 개선 사용 가이드
|
||||||
|
|
||||||
|
## 🎯 개선 내용 요약
|
||||||
|
|
||||||
|
테이블 타입 관리 시스템이 다음과 같이 개선되었습니다:
|
||||||
|
|
||||||
|
### 주요 변경사항
|
||||||
|
|
||||||
|
- **용어 통일**: 웹 타입 → **입력 타입**
|
||||||
|
- **타입 단순화**: 20개 → **8개 핵심 타입**
|
||||||
|
- **DB 타입 제거**: dbType 필드 완전 삭제
|
||||||
|
- **저장 방식**: 모든 사용자 정의 컬럼을 **VARCHAR(500)로 통일**
|
||||||
|
- **형변환**: 애플리케이션 레벨에서 입력 타입별 처리
|
||||||
|
|
||||||
|
## 📋 8개 핵심 입력 타입
|
||||||
|
|
||||||
|
| 번호 | 입력 타입 | 설명 | 예시 | 저장 형태 |
|
||||||
|
| ---- | ---------- | ---------- | -------------------- | ------------ |
|
||||||
|
| 1 | `text` | 텍스트 | 이름, 제목, 설명 | "홍길동" |
|
||||||
|
| 2 | `number` | 숫자 | 수량, 건수, 순번 | "15000.50" |
|
||||||
|
| 3 | `date` | 날짜 | 생성일, 완료일, 기한 | "2024-03-15" |
|
||||||
|
| 4 | `code` | 코드 | 상태코드, 유형코드 | "ACTIVE" |
|
||||||
|
| 5 | `entity` | 엔티티 | 고객선택, 제품선택 | "123" |
|
||||||
|
| 6 | `select` | 선택박스 | 드롭다운 목록 | "option1" |
|
||||||
|
| 7 | `checkbox` | 체크박스 | Y/N, 사용여부 | "Y" / "N" |
|
||||||
|
| 8 | `radio` | 라디오버튼 | 단일선택 옵션 | "high" |
|
||||||
|
|
||||||
|
## 🚀 마이그레이션 실행
|
||||||
|
|
||||||
|
### 1. 데이터베이스 마이그레이션
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 백엔드 디렉토리에서 실행
|
||||||
|
cd backend-node
|
||||||
|
node scripts/migrate-to-input-types.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**실행 결과:**
|
||||||
|
|
||||||
|
- `web_type` → `input_type` 컬럼명 변경
|
||||||
|
- `db_type` 컬럼 제거
|
||||||
|
- 기존 웹 타입을 8개 입력 타입으로 자동 변환
|
||||||
|
- 기존 데이터 백업 (`table_type_columns_backup`)
|
||||||
|
|
||||||
|
### 2. 서비스 재시작
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 백엔드 서비스 재시작
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 프론트엔드 서비스 재시작 (별도 터미널)
|
||||||
|
cd ../frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💻 사용법
|
||||||
|
|
||||||
|
### 1. 테이블 생성
|
||||||
|
|
||||||
|
새로운 테이블 생성 시 자동으로 기본 컬럼이 추가됩니다:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE "products" (
|
||||||
|
-- 기본 컬럼 (자동 추가)
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"created_date" timestamp DEFAULT now(),
|
||||||
|
"updated_date" timestamp DEFAULT now(),
|
||||||
|
"writer" varchar(100),
|
||||||
|
"company_code" varchar(50) DEFAULT '*',
|
||||||
|
|
||||||
|
-- 사용자 정의 컬럼 (모두 VARCHAR(500))
|
||||||
|
"product_name" varchar(500), -- 입력타입: text
|
||||||
|
"price" varchar(500), -- 입력타입: number
|
||||||
|
"launch_date" varchar(500), -- 입력타입: date
|
||||||
|
"is_active" varchar(500) -- 입력타입: checkbox
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 데이터 입력/조회
|
||||||
|
|
||||||
|
#### 백엔드에서 데이터 처리
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { InputTypeService } from "./services/inputTypeService";
|
||||||
|
|
||||||
|
// 데이터 저장 시
|
||||||
|
const inputData = {
|
||||||
|
product_name: "테스트 제품",
|
||||||
|
price: 15000.5,
|
||||||
|
launch_date: new Date("2024-03-15"),
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnTypes = {
|
||||||
|
product_name: "text",
|
||||||
|
price: "number",
|
||||||
|
launch_date: "date",
|
||||||
|
is_active: "checkbox",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 저장용 변환 (모든 값을 문자열로)
|
||||||
|
const convertedData = InputTypeService.convertBatchForStorage(
|
||||||
|
inputData,
|
||||||
|
columnTypes
|
||||||
|
);
|
||||||
|
// 결과: { product_name: "테스트 제품", price: "15000.5", launch_date: "2024-03-15", is_active: "Y" }
|
||||||
|
|
||||||
|
// 데이터베이스에 저장
|
||||||
|
await prisma.products.create({ data: convertedData });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 데이터 조회 시
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 데이터베이스에서 조회 (모든 값이 문자열)
|
||||||
|
const rawData = await prisma.products.findFirst();
|
||||||
|
// 결과: { product_name: "테스트 제품", price: "15000.5", launch_date: "2024-03-15", is_active: "Y" }
|
||||||
|
|
||||||
|
// 표시용 변환 (적절한 타입으로)
|
||||||
|
const displayData = InputTypeService.convertBatchForDisplay(
|
||||||
|
rawData,
|
||||||
|
columnTypes
|
||||||
|
);
|
||||||
|
// 결과: { product_name: "테스트 제품", price: 15000.5, launch_date: "2024-03-15", is_active: true }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 프론트엔드에서 사용
|
||||||
|
|
||||||
|
#### 테이블 관리 화면
|
||||||
|
|
||||||
|
1. **관리자 > 테이블 관리** 메뉴 접속
|
||||||
|
2. 테이블 선택 후 컬럼 목록 확인
|
||||||
|
3. **입력 타입** 컬럼에서 8개 타입 중 선택
|
||||||
|
4. **DB 타입** 컬럼은 제거됨 (더 이상 표시되지 않음)
|
||||||
|
|
||||||
|
#### 화면 관리 시스템 연동
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { INPUT_TYPE_OPTIONS } from "@/types/input-types";
|
||||||
|
|
||||||
|
// 입력 타입 옵션 사용
|
||||||
|
const inputTypeSelect = (
|
||||||
|
<Select>
|
||||||
|
{INPUT_TYPE_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label} - {option.description}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 테스트
|
||||||
|
|
||||||
|
### 전체 시스템 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend-node
|
||||||
|
node scripts/test-input-type-system.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**테스트 내용:**
|
||||||
|
|
||||||
|
- ✅ 테이블 생성 (기본 컬럼 자동 추가)
|
||||||
|
- ✅ VARCHAR(500) 통일 저장
|
||||||
|
- ✅ 입력 타입별 형변환
|
||||||
|
- ✅ 배치 데이터 처리
|
||||||
|
- ✅ 입력값 검증
|
||||||
|
- ✅ 성능 테스트
|
||||||
|
|
||||||
|
## 🔧 개발자 가이드
|
||||||
|
|
||||||
|
### 새로운 입력 타입 추가
|
||||||
|
|
||||||
|
1. **타입 정의 업데이트**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/types/input-types.ts & backend-node/src/types/input-types.ts
|
||||||
|
export type InputType =
|
||||||
|
| "text"
|
||||||
|
| "number"
|
||||||
|
| "date"
|
||||||
|
| "code"
|
||||||
|
| "entity"
|
||||||
|
| "select"
|
||||||
|
| "checkbox"
|
||||||
|
| "radio"
|
||||||
|
| "new_type"; // 새 타입 추가
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **변환 로직 추가**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// backend-node/src/services/inputTypeService.ts
|
||||||
|
case "new_type":
|
||||||
|
return processNewType(value);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **UI 옵션 추가**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/types/input-types.ts
|
||||||
|
export const INPUT_TYPE_OPTIONS = [
|
||||||
|
// ... 기존 옵션들
|
||||||
|
{
|
||||||
|
value: "new_type",
|
||||||
|
label: "새 타입",
|
||||||
|
description: "새로운 입력 타입",
|
||||||
|
category: "basic",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 엔드포인트
|
||||||
|
|
||||||
|
#### 입력 타입 변경
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/table-management/tables/{tableName}/columns/{columnName}/input-type
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"inputType": "number",
|
||||||
|
"detailSettings": "{}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 컬럼 입력 타입 조회
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/table-management/tables/{tableName}/input-types
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 주의사항
|
||||||
|
|
||||||
|
### 1. 데이터 검증 강화 필요
|
||||||
|
|
||||||
|
VARCHAR 통일 방식에서는 애플리케이션 레벨 검증이 중요합니다:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 반드시 검증 후 저장
|
||||||
|
const validation = InputTypeService.validate(value, inputType);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
throw new Error(validation.message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 기존 데이터 호환성
|
||||||
|
|
||||||
|
- **기존 테이블**: 현재 타입 구조 유지
|
||||||
|
- **신규 테이블**: 새로운 입력 타입 체계 적용
|
||||||
|
- **점진적 전환**: 필요에 따라 기존 테이블도 단계적 전환
|
||||||
|
|
||||||
|
### 3. 성능 고려사항
|
||||||
|
|
||||||
|
- 대용량 데이터 처리 시 배치 변환 사용
|
||||||
|
- 자주 사용되는 변환 결과는 캐싱 고려
|
||||||
|
- 복잡한 검증 로직은 비동기 처리
|
||||||
|
|
||||||
|
## 🆘 문제 해결
|
||||||
|
|
||||||
|
### 마이그레이션 실패 시 롤백
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 1. 기존 테이블 삭제
|
||||||
|
DROP TABLE table_type_columns;
|
||||||
|
|
||||||
|
-- 2. 백업에서 복원
|
||||||
|
ALTER TABLE table_type_columns_backup RENAME TO table_type_columns;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 입력 타입 변환 오류
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 안전한 변환을 위한 try-catch 사용
|
||||||
|
try {
|
||||||
|
const converted = InputTypeService.convertForStorage(value, inputType);
|
||||||
|
return converted;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("변환 실패", { value, inputType, error });
|
||||||
|
return String(value); // 기본값으로 문자열 반환
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI에서 입력 타입이 표시되지 않는 경우
|
||||||
|
|
||||||
|
1. 브라우저 캐시 클리어
|
||||||
|
2. 프론트엔드 서비스 재시작
|
||||||
|
3. `INPUT_TYPE_OPTIONS` import 확인
|
||||||
|
|
||||||
|
## 📞 지원
|
||||||
|
|
||||||
|
문제가 발생하거나 추가 기능이 필요한 경우:
|
||||||
|
|
||||||
|
1. **로그 확인**: 백엔드 콘솔에서 상세 로그 확인
|
||||||
|
2. **테스트 실행**: `test-input-type-system.js`로 시스템 상태 점검
|
||||||
|
3. **데이터 백업**: 중요한 변경 전 항상 백업 실행
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 테이블 타입 관리 개선으로 더욱 유연하고 안정적인 시스템을 경험하세요!**
|
||||||
Loading…
Reference in New Issue