테이블 추가기능 수정사항
This commit is contained in:
parent
474cc33aee
commit
e653effac0
|
|
@ -42,11 +42,17 @@ export class DDLController {
|
|||
ip: req.ip,
|
||||
});
|
||||
|
||||
// inputType을 webType으로 변환 (레거시 호환성)
|
||||
const processedColumns = columns.map((col) => ({
|
||||
...col,
|
||||
webType: (col.inputType || col.webType || "text") as any,
|
||||
}));
|
||||
|
||||
// DDL 실행 서비스 호출
|
||||
const ddlService = new DDLExecutionService();
|
||||
const result = await ddlService.createTable(
|
||||
tableName,
|
||||
columns,
|
||||
processedColumns,
|
||||
userCompanyCode,
|
||||
userId,
|
||||
description
|
||||
|
|
@ -112,12 +118,12 @@ export class DDLController {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!column || !column.name || !column.webType) {
|
||||
if (!column || !column.name || (!column.inputType && !column.webType)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "INVALID_INPUT",
|
||||
details: "컬럼명과 웹타입이 필요합니다.",
|
||||
details: "컬럼명과 입력타입이 필요합니다.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
|
@ -131,11 +137,17 @@ export class DDLController {
|
|||
ip: req.ip,
|
||||
});
|
||||
|
||||
// inputType을 webType으로 변환 (레거시 호환성)
|
||||
const processedColumn = {
|
||||
...column,
|
||||
webType: (column.inputType || column.webType || "text") as any,
|
||||
};
|
||||
|
||||
// DDL 실행 서비스 호출
|
||||
const ddlService = new DDLExecutionService();
|
||||
const result = await ddlService.addColumn(
|
||||
tableName,
|
||||
column,
|
||||
processedColumn,
|
||||
userCompanyCode,
|
||||
userId
|
||||
);
|
||||
|
|
|
|||
|
|
@ -443,24 +443,24 @@ export async function updateTableLabel(
|
|||
}
|
||||
|
||||
/**
|
||||
* 컬럼 웹 타입 설정
|
||||
* 컬럼 입력 타입 설정
|
||||
*/
|
||||
export async function updateColumnWebType(
|
||||
export async function updateColumnInputType(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const { webType, detailSettings, inputType } = req.body;
|
||||
const { inputType, detailSettings } = req.body;
|
||||
|
||||
logger.info(
|
||||
`=== 컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType} ===`
|
||||
`=== 컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${inputType} ===`
|
||||
);
|
||||
|
||||
if (!tableName || !columnName || !webType) {
|
||||
if (!tableName || !columnName || !inputType) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블명, 컬럼명, 웹 타입이 모두 필요합니다.",
|
||||
message: "테이블명, 컬럼명, 입력 타입이 모두 필요합니다.",
|
||||
error: {
|
||||
code: "MISSING_PARAMETERS",
|
||||
details: "필수 파라미터가 누락되었습니다.",
|
||||
|
|
@ -471,33 +471,32 @@ export async function updateColumnWebType(
|
|||
}
|
||||
|
||||
const tableManagementService = new TableManagementService();
|
||||
await tableManagementService.updateColumnWebType(
|
||||
await tableManagementService.updateColumnInputType(
|
||||
tableName,
|
||||
columnName,
|
||||
webType,
|
||||
detailSettings,
|
||||
inputType
|
||||
inputType,
|
||||
detailSettings
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`컬럼 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}`
|
||||
`컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${inputType}`
|
||||
);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: true,
|
||||
message: "컬럼 웹 타입이 성공적으로 설정되었습니다.",
|
||||
message: "컬럼 입력 타입이 성공적으로 설정되었습니다.",
|
||||
data: null,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("컬럼 웹 타입 설정 중 오류 발생:", error);
|
||||
logger.error("컬럼 입력 타입 설정 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "컬럼 웹 타입 설정 중 오류가 발생했습니다.",
|
||||
message: "컬럼 입력 타입 설정 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "WEB_TYPE_UPDATE_ERROR",
|
||||
code: "INPUT_TYPE_UPDATE_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
|
@ -866,16 +865,17 @@ export async function getColumnWebTypes(
|
|||
}
|
||||
|
||||
const tableManagementService = new TableManagementService();
|
||||
const webTypes = await tableManagementService.getColumnWebTypes(tableName);
|
||||
const inputTypes =
|
||||
await tableManagementService.getColumnInputTypes(tableName);
|
||||
|
||||
logger.info(
|
||||
`컬럼 웹타입 정보 조회 완료: ${tableName}, ${webTypes.length}개 컬럼`
|
||||
`컬럼 입력타입 정보 조회 완료: ${tableName}, ${inputTypes.length}개 컬럼`
|
||||
);
|
||||
|
||||
const response: ApiResponse<ColumnTypeInfo[]> = {
|
||||
success: true,
|
||||
message: "컬럼 웹타입 정보를 성공적으로 조회했습니다.",
|
||||
data: webTypes,
|
||||
message: "컬럼 입력타입 정보를 성공적으로 조회했습니다.",
|
||||
data: inputTypes,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
|
|
@ -1010,3 +1010,41 @@ export async function deleteTableData(
|
|||
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,
|
||||
getColumnLabels,
|
||||
updateColumnWebType,
|
||||
updateColumnInputType,
|
||||
updateTableLabel,
|
||||
getTableData,
|
||||
addTableData,
|
||||
|
|
@ -70,7 +71,7 @@ router.get("/tables/:tableName/labels", getTableLabels);
|
|||
router.get("/tables/:tableName/columns/:columnName/labels", getColumnLabels);
|
||||
|
||||
/**
|
||||
* 컬럼 웹 타입 설정
|
||||
* 컬럼 웹 타입 설정 (레거시 지원)
|
||||
* PUT /api/table-management/tables/:tableName/columns/:columnName/web-type
|
||||
*/
|
||||
router.put(
|
||||
|
|
@ -78,6 +79,15 @@ router.put(
|
|||
updateColumnWebType
|
||||
);
|
||||
|
||||
/**
|
||||
* 컬럼 입력 타입 설정 (새로운 시스템)
|
||||
* PUT /api/table-management/tables/:tableName/columns/:columnName/input-type
|
||||
*/
|
||||
router.put(
|
||||
"/tables/:tableName/columns/:columnName/input-type",
|
||||
updateColumnInputType
|
||||
);
|
||||
|
||||
/**
|
||||
* 개별 컬럼 설정 업데이트 (PUT 방식)
|
||||
* PUT /api/table-management/tables/:tableName/columns/:columnName
|
||||
|
|
|
|||
|
|
@ -342,14 +342,11 @@ export class DDLExecutionService {
|
|||
tableName: string,
|
||||
columns: CreateColumnDefinition[]
|
||||
): string {
|
||||
// 사용자 정의 컬럼들
|
||||
// 사용자 정의 컬럼들 - 모두 VARCHAR(500)로 통일
|
||||
const columnDefinitions = columns
|
||||
.map((col) => {
|
||||
const postgresType = this.mapWebTypeToPostgresType(
|
||||
col.webType,
|
||||
col.length
|
||||
);
|
||||
let definition = `"${col.name}" ${postgresType}`;
|
||||
// 입력 타입과 관계없이 모든 컬럼을 VARCHAR(500)로 생성
|
||||
let definition = `"${col.name}" varchar(500)`;
|
||||
|
||||
if (!col.nullable) {
|
||||
definition += " NOT NULL";
|
||||
|
|
@ -363,13 +360,13 @@ export class DDLExecutionService {
|
|||
})
|
||||
.join(",\n ");
|
||||
|
||||
// 기본 컬럼들 (시스템 필수 컬럼)
|
||||
// 기본 컬럼들 (날짜는 TIMESTAMP, 나머지는 VARCHAR)
|
||||
const baseColumns = `
|
||||
"id" serial PRIMARY KEY,
|
||||
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||
"created_date" timestamp DEFAULT now(),
|
||||
"updated_date" timestamp DEFAULT now(),
|
||||
"writer" varchar(100),
|
||||
"company_code" varchar(50) DEFAULT '*'`;
|
||||
"writer" varchar(500),
|
||||
"company_code" varchar(500)`;
|
||||
|
||||
// 최종 CREATE TABLE 쿼리
|
||||
return `
|
||||
|
|
@ -385,11 +382,8 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
tableName: string,
|
||||
column: CreateColumnDefinition
|
||||
): string {
|
||||
const postgresType = this.mapWebTypeToPostgresType(
|
||||
column.webType,
|
||||
column.length
|
||||
);
|
||||
let definition = `"${column.name}" ${postgresType}`;
|
||||
// 새로 추가되는 컬럼도 VARCHAR(500)로 통일
|
||||
let definition = `"${column.name}" varchar(500)`;
|
||||
|
||||
if (!column.nullable) {
|
||||
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 {
|
||||
const mapping = WEB_TYPE_TO_POSTGRES_MAP[webType];
|
||||
|
||||
if (!mapping) {
|
||||
logger.warn(`알 수 없는 웹타입: ${webType}, text로 대체`);
|
||||
return "text";
|
||||
}
|
||||
|
||||
if (mapping.supportsLength && length && length > 0) {
|
||||
if (mapping.postgresType === "varchar") {
|
||||
return `varchar(${length})`;
|
||||
}
|
||||
}
|
||||
|
||||
return mapping.postgresType;
|
||||
// 레거시 지원을 위해 유지하되, VARCHAR(500)로 통일
|
||||
logger.info(`레거시 웹타입 사용: ${webType} → varchar(500)로 변환`);
|
||||
return "varchar(500)";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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) {
|
||||
await tx.column_labels.upsert({
|
||||
where: {
|
||||
|
|
@ -482,7 +601,7 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
},
|
||||
update: {
|
||||
column_label: column.label || column.name,
|
||||
web_type: column.webType,
|
||||
input_type: this.convertWebTypeToInputType(column.webType || "text"),
|
||||
detail_settings: JSON.stringify(column.detailSettings || {}),
|
||||
description: column.description,
|
||||
display_order: column.order || 0,
|
||||
|
|
@ -493,7 +612,7 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
table_name: tableName,
|
||||
column_name: 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 || {}),
|
||||
description: column.description,
|
||||
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 (
|
||||
!["text", "code", "email", "tel", "select", "radio"].includes(
|
||||
column.webType
|
||||
column.webType || "text"
|
||||
)
|
||||
) {
|
||||
warnings.push(
|
||||
`${prefix}${column.webType} 타입에서는 길이 설정이 무시됩니다.`
|
||||
`${prefix}${column.webType || "text"} 타입에서는 길이 설정이 무시됩니다.`
|
||||
);
|
||||
} else if (column.length <= 0 || column.length > 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: {
|
||||
column_label: settings.columnLabel,
|
||||
web_type: settings.webType,
|
||||
input_type: settings.inputType,
|
||||
detail_settings: settings.detailSettings,
|
||||
code_category: settings.codeCategory,
|
||||
code_value: settings.codeValue,
|
||||
|
|
@ -345,7 +345,7 @@ export class TableManagementService {
|
|||
table_name: tableName,
|
||||
column_name: columnName,
|
||||
column_label: settings.columnLabel,
|
||||
web_type: settings.webType,
|
||||
input_type: settings.inputType,
|
||||
detail_settings: settings.detailSettings,
|
||||
code_category: settings.codeCategory,
|
||||
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> {
|
||||
switch (webType) {
|
||||
|
|
@ -2363,22 +2479,21 @@ export class TableManagementService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 컬럼 웹타입 정보 조회 (화면관리 연동용)
|
||||
* 컬럼 입력타입 정보 조회 (화면관리 연동용)
|
||||
*/
|
||||
async getColumnWebTypes(tableName: string): Promise<ColumnTypeInfo[]> {
|
||||
async getColumnInputTypes(tableName: string): Promise<ColumnTypeInfo[]> {
|
||||
try {
|
||||
logger.info(`컬럼 웹타입 정보 조회: ${tableName}`);
|
||||
logger.info(`컬럼 입력타입 정보 조회: ${tableName}`);
|
||||
|
||||
// table_type_columns에서 웹타입 정보 조회
|
||||
const rawWebTypes = await prisma.$queryRaw<any[]>`
|
||||
// table_type_columns에서 입력타입 정보 조회
|
||||
const rawInputTypes = await prisma.$queryRaw<any[]>`
|
||||
SELECT
|
||||
ttc.column_name as "columnName",
|
||||
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",
|
||||
ttc.is_nullable as "isNullable",
|
||||
ic.data_type as "dataType",
|
||||
ic.udt_name as "dbType"
|
||||
ic.data_type as "dataType"
|
||||
FROM table_type_columns ttc
|
||||
LEFT JOIN information_schema.columns ic
|
||||
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
|
||||
`;
|
||||
|
||||
const webTypes: ColumnTypeInfo[] = rawWebTypes.map((col) => ({
|
||||
const inputTypes: ColumnTypeInfo[] = rawInputTypes.map((col) => ({
|
||||
tableName: tableName,
|
||||
columnName: col.columnName,
|
||||
displayName: col.displayName,
|
||||
dataType: col.dataType || "text",
|
||||
dbType: col.dbType || "text",
|
||||
webType: col.webType,
|
||||
inputType: "direct",
|
||||
dataType: col.dataType || "varchar",
|
||||
inputType: col.inputType,
|
||||
detailSettings: col.detailSettings,
|
||||
description: "", // 필수 필드 추가
|
||||
isNullable: col.isNullable,
|
||||
|
|
@ -2403,15 +2516,26 @@ export class TableManagementService {
|
|||
}));
|
||||
|
||||
logger.info(
|
||||
`컬럼 웹타입 정보 조회 완료: ${tableName}, ${webTypes.length}개 컬럼`
|
||||
`컬럼 입력타입 정보 조회 완료: ${tableName}, ${inputTypes.length}개 컬럼`
|
||||
);
|
||||
return webTypes;
|
||||
return inputTypes;
|
||||
} catch (error) {
|
||||
logger.error(`컬럼 웹타입 정보 조회 실패: ${tableName}`, error);
|
||||
logger.error(`컬럼 입력타입 정보 조회 실패: ${tableName}`, 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;
|
||||
/** 컬럼 라벨 (화면 표시용) */
|
||||
label?: string;
|
||||
/** 웹타입 */
|
||||
webType: WebType;
|
||||
/** 입력타입 */
|
||||
inputType?: string;
|
||||
/** 웹타입 (레거시 호환용) */
|
||||
webType?: WebType;
|
||||
/** NULL 허용 여부 */
|
||||
nullable?: boolean;
|
||||
/** 컬럼 길이 (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 {
|
||||
tableName?: string;
|
||||
columnName: string;
|
||||
displayName: string;
|
||||
dataType: string; // 추가: 데이터 타입 (dbType과 동일하지만 별도 필드)
|
||||
dbType: string;
|
||||
webType: string;
|
||||
inputType?: "direct" | "auto";
|
||||
dataType: string; // DB 데이터 타입 (varchar, integer 등)
|
||||
inputType: string; // 입력 타입 (text, number, date, code, entity, select, checkbox, radio)
|
||||
detailSettings: string;
|
||||
description: string;
|
||||
isNullable: string;
|
||||
isPrimaryKey: boolean; // 추가: 기본키 여부
|
||||
isPrimaryKey: boolean;
|
||||
defaultValue?: string;
|
||||
maxLength?: number;
|
||||
numericPrecision?: number;
|
||||
|
|
@ -34,7 +33,7 @@ export interface ColumnTypeInfo {
|
|||
export interface ColumnSettings {
|
||||
columnName?: string; // 컬럼명 (업데이트 시 필요)
|
||||
columnLabel: string; // 컬럼 표시명
|
||||
webType: string; // 웹 입력 타입 (text, number, date, code, entity)
|
||||
inputType: string; // 입력 타입 (text, number, date, code, entity, select, checkbox, radio)
|
||||
detailSettings: string; // 상세 설정
|
||||
codeCategory: string; // 코드 카테고리
|
||||
codeValue: string; // 코드 값
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ services:
|
|||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=8080
|
||||
- DATABASE_URL=postgresql://postgres:postgres@postgres-erp:5432/ilshin
|
||||
- JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024
|
||||
- DATABASE_URL=postgresql://postgres:ph0909!!@39.117.244.52:11132/plm
|
||||
- JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||
- JWT_EXPIRES_IN=24h
|
||||
- CORS_ORIGIN=http://localhost:9771
|
||||
- CORS_CREDENTIALS=true
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
|||
import { toast } from "sonner";
|
||||
import { useMultiLang } from "@/hooks/useMultiLang";
|
||||
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 { commonCodeApi } from "@/lib/api/commonCode";
|
||||
import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin";
|
||||
|
|
@ -31,8 +32,7 @@ interface TableInfo {
|
|||
interface ColumnTypeInfo {
|
||||
columnName: string;
|
||||
displayName: string;
|
||||
dbType: string;
|
||||
webType: string;
|
||||
inputType: string; // webType → inputType 변경
|
||||
detailSettings: string;
|
||||
description: string;
|
||||
isNullable: string;
|
||||
|
|
@ -148,38 +148,15 @@ export default function TableManagementPage() {
|
|||
[], // 의존성 배열에서 referenceTableColumns 제거
|
||||
);
|
||||
|
||||
// 웹 타입 옵션 (한글 직접 표시)
|
||||
const webTypeOptions = WEB_TYPE_OPTIONS_WITH_KEYS.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 {
|
||||
// 입력 타입 옵션 (8개 핵심 타입)
|
||||
const inputTypeOptions = INPUT_TYPE_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: koreanLabels[option.value] || option.value,
|
||||
description: koreanLabels[option.value] || option.value,
|
||||
};
|
||||
});
|
||||
label: option.label,
|
||||
description: option.description,
|
||||
}));
|
||||
|
||||
// 메모이제이션된 웹타입 옵션
|
||||
const memoizedWebTypeOptions = useMemo(() => webTypeOptions, [uiTexts]);
|
||||
// 메모이제이션된 입력타입 옵션
|
||||
const memoizedInputTypeOptions = useMemo(() => inputTypeOptions, []);
|
||||
|
||||
// 참조 테이블 옵션 (실제 테이블 목록에서 가져옴)
|
||||
const referenceTableOptions = [
|
||||
|
|
@ -254,14 +231,21 @@ export default function TableManagementPage() {
|
|||
// 응답 상태 확인
|
||||
if (response.data.success) {
|
||||
const data = response.data.data;
|
||||
|
||||
// 컬럼 데이터에 기본값 설정
|
||||
const processedColumns = (data.columns || data).map((col: any) => ({
|
||||
...col,
|
||||
inputType: col.inputType || "text", // 기본값: text
|
||||
}));
|
||||
|
||||
if (page === 1) {
|
||||
setColumns(data.columns || data);
|
||||
setOriginalColumns(data.columns || data);
|
||||
setColumns(processedColumns);
|
||||
setOriginalColumns(processedColumns);
|
||||
} 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("컬럼 정보를 성공적으로 로드했습니다.");
|
||||
} else {
|
||||
toast.error(response.data.message || "컬럼 정보 로드에 실패했습니다.");
|
||||
|
|
@ -291,24 +275,24 @@ export default function TableManagementPage() {
|
|||
[loadColumnTypes, pageSize, tables],
|
||||
);
|
||||
|
||||
// 웹 타입 변경
|
||||
const handleWebTypeChange = useCallback(
|
||||
(columnName: string, newWebType: string) => {
|
||||
// 입력 타입 변경
|
||||
const handleInputTypeChange = useCallback(
|
||||
(columnName: string, newInputType: string) => {
|
||||
setColumns((prev) =>
|
||||
prev.map((col) => {
|
||||
if (col.columnName === columnName) {
|
||||
const webTypeOption = memoizedWebTypeOptions.find((option) => option.value === newWebType);
|
||||
const inputTypeOption = memoizedInputTypeOptions.find((option) => option.value === newInputType);
|
||||
return {
|
||||
...col,
|
||||
webType: newWebType,
|
||||
detailSettings: webTypeOption?.description || col.detailSettings,
|
||||
inputType: newInputType,
|
||||
detailSettings: inputTypeOption?.description || col.detailSettings,
|
||||
};
|
||||
}
|
||||
return col;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[memoizedWebTypeOptions],
|
||||
[memoizedInputTypeOptions],
|
||||
);
|
||||
|
||||
// 상세 설정 변경 (코드/엔티티 타입용)
|
||||
|
|
@ -418,7 +402,7 @@ export default function TableManagementPage() {
|
|||
const columnSetting = {
|
||||
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
||||
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
||||
webType: column.webType || "text",
|
||||
inputType: column.inputType || "text",
|
||||
detailSettings: column.detailSettings || "",
|
||||
codeCategory: column.codeCategory || "",
|
||||
codeValue: column.codeValue || "",
|
||||
|
|
@ -473,7 +457,7 @@ export default function TableManagementPage() {
|
|||
const columnSettings = columns.map((column) => ({
|
||||
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
||||
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
||||
webType: column.webType || "text",
|
||||
inputType: column.inputType || "text",
|
||||
detailSettings: column.detailSettings || "",
|
||||
description: column.description || "",
|
||||
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="w-40 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>
|
||||
|
|
@ -772,21 +755,16 @@ export default function TableManagementPage() {
|
|||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-40 px-4">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{column.dbType}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="w-48 px-4">
|
||||
<Select
|
||||
value={column.webType}
|
||||
onValueChange={(value) => handleWebTypeChange(column.columnName, value)}
|
||||
value={column.inputType || "text"}
|
||||
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
<SelectValue placeholder="입력 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{memoizedWebTypeOptions.map((option) => (
|
||||
{memoizedInputTypeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
|
|
@ -796,7 +774,7 @@ export default function TableManagementPage() {
|
|||
</div>
|
||||
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
|
||||
{/* 웹 타입이 'code'인 경우 공통코드 선택 */}
|
||||
{column.webType === "code" && (
|
||||
{column.inputType === "code" && (
|
||||
<Select
|
||||
value={column.codeCategory || "none"}
|
||||
onValueChange={(value) => handleDetailSettingsChange(column.columnName, "code", value)}
|
||||
|
|
@ -814,7 +792,7 @@ export default function TableManagementPage() {
|
|||
</Select>
|
||||
)}
|
||||
{/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */}
|
||||
{column.webType === "entity" && (
|
||||
{column.inputType === "entity" && (
|
||||
<div className="space-y-1">
|
||||
{/* 🎯 Entity 타입 설정 - 가로 배치 */}
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-2">
|
||||
|
|
@ -949,7 +927,7 @@ export default function TableManagementPage() {
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -20,17 +20,17 @@ import { ddlApi } from "../../lib/api/ddl";
|
|||
import {
|
||||
AddColumnModalProps,
|
||||
CreateColumnDefinition,
|
||||
WEB_TYPE_OPTIONS,
|
||||
VALIDATION_RULES,
|
||||
RESERVED_WORDS,
|
||||
RESERVED_COLUMNS,
|
||||
} from "../../types/ddl";
|
||||
import { INPUT_TYPE_OPTIONS } from "../../types/input-types";
|
||||
|
||||
export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddColumnModalProps) {
|
||||
const [column, setColumn] = useState<CreateColumnDefinition>({
|
||||
name: "",
|
||||
label: "",
|
||||
webType: "text",
|
||||
inputType: "text",
|
||||
nullable: true,
|
||||
order: 0,
|
||||
});
|
||||
|
|
@ -44,7 +44,7 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
|||
setColumn({
|
||||
name: "",
|
||||
label: "",
|
||||
webType: "text",
|
||||
inputType: "text",
|
||||
nullable: true,
|
||||
order: 0,
|
||||
});
|
||||
|
|
@ -114,14 +114,14 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
|||
}
|
||||
}
|
||||
|
||||
// 웹타입 검증
|
||||
if (!columnData.webType) {
|
||||
errors.push("웹타입을 선택해주세요.");
|
||||
// 입력타입 검증
|
||||
if (!columnData.inputType) {
|
||||
errors.push("입력타입을 선택해주세요.");
|
||||
}
|
||||
|
||||
// 길이 검증 (길이를 지원하는 타입인 경우)
|
||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === columnData.webType);
|
||||
if (webTypeOption?.supportsLength && columnData.length !== undefined) {
|
||||
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === columnData.inputType);
|
||||
if (inputTypeOption?.supportsLength && columnData.length !== undefined) {
|
||||
if (
|
||||
columnData.length < VALIDATION_RULES.columnLength.min ||
|
||||
columnData.length > VALIDATION_RULES.columnLength.max
|
||||
|
|
@ -135,19 +135,19 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
|||
};
|
||||
|
||||
/**
|
||||
* 웹타입 변경 처리
|
||||
* 입력타입 변경 처리
|
||||
*/
|
||||
const handleWebTypeChange = (webType: string) => {
|
||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === webType);
|
||||
const updates: Partial<CreateColumnDefinition> = { webType: webType as any };
|
||||
const handleInputTypeChange = (inputType: string) => {
|
||||
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType);
|
||||
const updates: Partial<CreateColumnDefinition> = { inputType: inputType as any };
|
||||
|
||||
// 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정
|
||||
if (webTypeOption?.supportsLength && !column.length && webTypeOption.defaultLength) {
|
||||
updates.length = webTypeOption.defaultLength;
|
||||
if (inputTypeOption?.supportsLength && !column.length && inputTypeOption.defaultLength) {
|
||||
updates.length = inputTypeOption.defaultLength;
|
||||
}
|
||||
|
||||
// 길이를 지원하지 않는 타입이면 길이 제거
|
||||
if (!webTypeOption?.supportsLength) {
|
||||
if (!inputTypeOption?.supportsLength) {
|
||||
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 (
|
||||
<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="space-y-2">
|
||||
<Label>
|
||||
웹타입 <span className="text-red-500">*</span>
|
||||
입력타입 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={column.webType} onValueChange={handleWebTypeChange} disabled={loading}>
|
||||
<Select value={column.inputType} onValueChange={handleInputTypeChange} disabled={loading}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="웹타입 선택" />
|
||||
<SelectValue placeholder="입력타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{WEB_TYPE_OPTIONS.map((option) => (
|
||||
{INPUT_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<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,
|
||||
})
|
||||
}
|
||||
placeholder={webTypeOption?.defaultLength?.toString() || ""}
|
||||
disabled={loading || !webTypeOption?.supportsLength}
|
||||
placeholder={inputTypeOption?.defaultLength?.toString() || ""}
|
||||
disabled={loading || !inputTypeOption?.supportsLength}
|
||||
min={1}
|
||||
max={65535}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{webTypeOption?.supportsLength ? "1-65535 범위에서 설정 가능" : "이 타입은 길이 설정이 불가능합니다"}
|
||||
{inputTypeOption?.supportsLength ? "1-65535 범위에서 설정 가능" : "이 타입은 길이 설정이 불가능합니다"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
|
|||
import {
|
||||
CreateColumnDefinition,
|
||||
ColumnDefinitionTableProps,
|
||||
WEB_TYPE_OPTIONS,
|
||||
VALIDATION_RULES,
|
||||
RESERVED_WORDS,
|
||||
RESERVED_COLUMNS,
|
||||
} from "../../types/ddl";
|
||||
import { INPUT_TYPE_OPTIONS } from "../../types/input-types";
|
||||
|
||||
export function ColumnDefinitionTable({ columns, onChange, disabled = false }: ColumnDefinitionTableProps) {
|
||||
const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({});
|
||||
|
|
@ -105,14 +105,14 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
|||
}
|
||||
}
|
||||
|
||||
// 웹타입 검증
|
||||
if (!column.webType) {
|
||||
errors.push("웹타입을 선택해주세요");
|
||||
// 입력타입 검증
|
||||
if (!column.inputType) {
|
||||
errors.push("입력타입을 선택해주세요");
|
||||
}
|
||||
|
||||
// 길이 검증 (길이를 지원하는 타입인 경우)
|
||||
const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === column.webType);
|
||||
if (webTypeOption?.supportsLength && column.length !== undefined) {
|
||||
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType);
|
||||
if (inputTypeOption?.supportsLength && column.length !== undefined) {
|
||||
if (column.length < VALIDATION_RULES.columnLength.min || column.length > VALIDATION_RULES.columnLength.max) {
|
||||
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 webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === webType);
|
||||
const updates: Partial<CreateColumnDefinition> = { webType: webType as any };
|
||||
const handleInputTypeChange = (index: number, inputType: string) => {
|
||||
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType);
|
||||
const updates: Partial<CreateColumnDefinition> = { inputType: inputType as any };
|
||||
|
||||
// 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정
|
||||
if (webTypeOption?.supportsLength && !columns[index].length && webTypeOption.defaultLength) {
|
||||
updates.length = webTypeOption.defaultLength;
|
||||
if (inputTypeOption?.supportsLength && !columns[index].length && inputTypeOption.defaultLength) {
|
||||
updates.length = inputTypeOption.defaultLength;
|
||||
}
|
||||
|
||||
// 길이를 지원하지 않는 타입이면 길이 제거
|
||||
if (!webTypeOption?.supportsLength) {
|
||||
if (!inputTypeOption?.supportsLength) {
|
||||
updates.length = undefined;
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
|||
</TableHead>
|
||||
<TableHead className="w-[150px]">라벨</TableHead>
|
||||
<TableHead className="w-[120px]">
|
||||
웹타입 <span className="text-red-500">*</span>
|
||||
입력타입 <span className="text-red-500">*</span>
|
||||
</TableHead>
|
||||
<TableHead className="w-[80px]">필수</TableHead>
|
||||
<TableHead className="w-[100px]">길이</TableHead>
|
||||
|
|
@ -183,7 +183,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{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 hasRowError = rowErrors.length > 0;
|
||||
|
||||
|
|
@ -219,15 +219,15 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
|||
|
||||
<TableCell>
|
||||
<Select
|
||||
value={column.webType}
|
||||
onValueChange={(value) => handleWebTypeChange(index, value)}
|
||||
value={column.inputType}
|
||||
onValueChange={(value) => handleInputTypeChange(index, value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택" />
|
||||
<SelectValue placeholder="입력 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{WEB_TYPE_OPTIONS.map((option) => (
|
||||
{INPUT_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<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,
|
||||
})
|
||||
}
|
||||
placeholder={webTypeOption?.defaultLength?.toString() || ""}
|
||||
disabled={disabled || !webTypeOption?.supportsLength}
|
||||
placeholder={inputTypeOption?.defaultLength?.toString() || ""}
|
||||
disabled={disabled || !inputTypeOption?.supportsLength}
|
||||
min={1}
|
||||
max={65535}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
|||
{
|
||||
name: "",
|
||||
label: "",
|
||||
webType: "text",
|
||||
inputType: "text",
|
||||
nullable: true,
|
||||
order: 1,
|
||||
},
|
||||
|
|
@ -58,7 +58,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
|||
{
|
||||
name: "",
|
||||
label: "",
|
||||
webType: "text",
|
||||
inputType: "text",
|
||||
nullable: true,
|
||||
order: 1,
|
||||
},
|
||||
|
|
@ -134,7 +134,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
|||
{
|
||||
name: "",
|
||||
label: "",
|
||||
webType: "text",
|
||||
inputType: "text",
|
||||
nullable: true,
|
||||
order: columns.length + 1,
|
||||
},
|
||||
|
|
@ -150,7 +150,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
|||
return;
|
||||
}
|
||||
|
||||
const validColumns = columns.filter((col) => col.name && col.webType);
|
||||
const validColumns = columns.filter((col) => col.name && col.inputType);
|
||||
if (validColumns.length === 0) {
|
||||
toast.error("최소 1개의 유효한 컬럼이 필요합니다.");
|
||||
return;
|
||||
|
|
@ -188,7 +188,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
|||
return;
|
||||
}
|
||||
|
||||
const validColumns = columns.filter((col) => col.name && col.webType);
|
||||
const validColumns = columns.filter((col) => col.name && col.inputType);
|
||||
if (validColumns.length === 0) {
|
||||
toast.error("최소 1개의 유효한 컬럼이 필요합니다.");
|
||||
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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ export interface CreateColumnDefinition {
|
|||
name: string;
|
||||
/** 컬럼 라벨 (화면 표시용) */
|
||||
label?: string;
|
||||
/** 웹타입 */
|
||||
webType: WebType;
|
||||
/** 입력타입 */
|
||||
inputType: string;
|
||||
/** 웹타입 (레거시 호환용) */
|
||||
webType?: WebType;
|
||||
/** NULL 허용 여부 */
|
||||
nullable?: boolean;
|
||||
/** 컬럼 길이 (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