From e653effac01a92fbea12687eed6cd49ba673a925 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 10:40:21 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/controllers/ddlController.ts | 20 +- .../controllers/tableManagementController.ts | 76 ++- .../src/routes/tableManagementRoutes.ts | 12 +- .../src/services/ddlExecutionService.ts | 224 +++++++-- .../src/services/ddlSafetyValidator.ts | 4 +- backend-node/src/services/inputTypeService.ts | 282 +++++++++++ .../src/services/tableManagementService.ts | 162 +++++- backend-node/src/types/ddl.ts | 6 +- backend-node/src/types/input-types.ts | 115 +++++ backend-node/src/types/tableManagement.ts | 11 +- docker/dev/docker-compose.backend.mac.yml | 4 +- frontend/app/(main)/admin/tableMng/page.tsx | 100 ++-- frontend/components/admin/AddColumnModal.tsx | 48 +- .../admin/ColumnDefinitionTable.tsx | 42 +- .../components/admin/CreateTableModal.tsx | 12 +- frontend/types/ddl.ts | 6 +- frontend/types/input-types.ts | 235 +++++++++ 테이블_타입_관리_개선_계획서.md | 472 ++++++++++++++++++ 테이블_타입_관리_개선_사용_가이드.md | 301 +++++++++++ 19 files changed, 1931 insertions(+), 201 deletions(-) create mode 100644 backend-node/src/services/inputTypeService.ts create mode 100644 backend-node/src/types/input-types.ts create mode 100644 frontend/types/input-types.ts create mode 100644 테이블_타입_관리_개선_계획서.md create mode 100644 테이블_타입_관리_개선_사용_가이드.md diff --git a/backend-node/src/controllers/ddlController.ts b/backend-node/src/controllers/ddlController.ts index 006dceac..3fff2d73 100644 --- a/backend-node/src/controllers/ddlController.ts +++ b/backend-node/src/controllers/ddlController.ts @@ -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 ); diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 682672ee..aac86625 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -443,24 +443,24 @@ export async function updateTableLabel( } /** - * 컬럼 웹 타입 설정 + * 컬럼 입력 타입 설정 */ -export async function updateColumnWebType( +export async function updateColumnInputType( req: AuthenticatedRequest, res: Response ): Promise { 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 = { 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 = { success: true, - message: "컬럼 웹 타입이 성공적으로 설정되었습니다.", + message: "컬럼 입력 타입이 성공적으로 설정되었습니다.", data: null, }; res.status(200).json(response); } catch (error) { - logger.error("컬럼 웹 타입 설정 중 오류 발생:", error); + logger.error("컬럼 입력 타입 설정 중 오류 발생:", error); const response: ApiResponse = { 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 = { 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 { + 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 = { + success: false, + message: "컬럼 웹 타입 설정 중 오류가 발생했습니다.", + error: { + code: "WEB_TYPE_UPDATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} diff --git a/backend-node/src/routes/tableManagementRoutes.ts b/backend-node/src/routes/tableManagementRoutes.ts index 18f99172..c0b35b94 100644 --- a/backend-node/src/routes/tableManagementRoutes.ts +++ b/backend-node/src/routes/tableManagementRoutes.ts @@ -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 diff --git a/backend-node/src/services/ddlExecutionService.ts b/backend-node/src/services/ddlExecutionService.ts index d327bd51..9167a48e 100644 --- a/backend-node/src/services/ddlExecutionService.ts +++ b/backend-node/src/services/ddlExecutionService.ts @@ -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 = { + // 텍스트 관련 + 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"; + } + /** * 권한 검증 (슈퍼관리자 확인) */ diff --git a/backend-node/src/services/ddlSafetyValidator.ts b/backend-node/src/services/ddlSafetyValidator.ts index b7a44435..bacf2308 100644 --- a/backend-node/src/services/ddlSafetyValidator.ts +++ b/backend-node/src/services/ddlSafetyValidator.ts @@ -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 이하여야 합니다.`); diff --git a/backend-node/src/services/inputTypeService.ts b/backend-node/src/services/inputTypeService.ts new file mode 100644 index 00000000..57bf80d4 --- /dev/null +++ b/backend-node/src/services/inputTypeService.ts @@ -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, + columnTypes: Record + ): Record { + const converted: Record = {}; + + 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, + columnTypes: Record + ): Record { + const converted: Record = {}; + + 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, + columnTypes: Record + ): { + isValid: boolean; + errors: Record; + convertedData: Record; + } { + const errors: Record = {}; + const convertedData: Record = {}; + + 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, + }; + } +} diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 1501bd77..daa9f526 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -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 + ): Promise { + 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 { + 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 { switch (webType) { @@ -2363,22 +2479,21 @@ export class TableManagementService { } /** - * 컬럼 웹타입 정보 조회 (화면관리 연동용) + * 컬럼 입력타입 정보 조회 (화면관리 연동용) */ - async getColumnWebTypes(tableName: string): Promise { + async getColumnInputTypes(tableName: string): Promise { try { - logger.info(`컬럼 웹타입 정보 조회: ${tableName}`); + logger.info(`컬럼 입력타입 정보 조회: ${tableName}`); - // table_type_columns에서 웹타입 정보 조회 - const rawWebTypes = await prisma.$queryRaw` + // table_type_columns에서 입력타입 정보 조회 + const rawInputTypes = await prisma.$queryRaw` 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 { + logger.warn( + `레거시 메서드 사용: getColumnWebTypes → getColumnInputTypes 사용 권장` + ); + return this.getColumnInputTypes(tableName); + } + /** * 데이터베이스 연결 상태 확인 */ diff --git a/backend-node/src/types/ddl.ts b/backend-node/src/types/ddl.ts index ec84a983..cb6b79dc 100644 --- a/backend-node/src/types/ddl.ts +++ b/backend-node/src/types/ddl.ts @@ -26,8 +26,10 @@ export interface CreateColumnDefinition { name: string; /** 컬럼 라벨 (화면 표시용) */ label?: string; - /** 웹타입 */ - webType: WebType; + /** 입력타입 */ + inputType?: string; + /** 웹타입 (레거시 호환용) */ + webType?: WebType; /** NULL 허용 여부 */ nullable?: boolean; /** 컬럼 길이 (text, code 타입에서 사용) */ diff --git a/backend-node/src/types/input-types.ts b/backend-node/src/types/input-types.ts new file mode 100644 index 00000000..98e429c1 --- /dev/null +++ b/backend-node/src/types/input-types.ts @@ -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 = { + // 텍스트 관련 + 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"; +}; diff --git a/backend-node/src/types/tableManagement.ts b/backend-node/src/types/tableManagement.ts index dec3ab16..52dca092 100644 --- a/backend-node/src/types/tableManagement.ts +++ b/backend-node/src/types/tableManagement.ts @@ -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; // 코드 값 diff --git a/docker/dev/docker-compose.backend.mac.yml b/docker/dev/docker-compose.backend.mac.yml index 0cb5cc57..3342887c 100644 --- a/docker/dev/docker-compose.backend.mac.yml +++ b/docker/dev/docker-compose.backend.mac.yml @@ -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 diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 298c1186..f09f6bc8 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -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 = { - text: "텍스트", - number: "숫자", - date: "날짜", - code: "코드", - entity: "엔티티", - textarea: "텍스트 영역", - select: "선택박스", - checkbox: "체크박스", - radio: "라디오버튼", - file: "파일", - decimal: "소수", - datetime: "날짜시간", - boolean: "불린", - email: "이메일", - tel: "전화번호", - url: "URL", - dropdown: "드롭다운", - }; + // 입력 타입 옵션 (8개 핵심 타입) + const inputTypeOptions = INPUT_TYPE_OPTIONS.map((option) => ({ + value: option.value, + label: option.label, + description: option.description, + })); - return { - value: option.value, - label: koreanLabels[option.value] || option.value, - description: koreanLabels[option.value] || option.value, - }; - }); - - // 메모이제이션된 웹타입 옵션 - 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() {
컬럼명
라벨
-
DB 타입
-
웹 타입
+
입력 타입
상세 설정
@@ -772,21 +755,16 @@ export default function TableManagementPage() { className="h-7 text-xs" />
-
- - {column.dbType} - -
handleDetailSettingsChange(column.columnName, "code", value)} @@ -814,7 +792,7 @@ export default function TableManagementPage() { )} {/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */} - {column.webType === "entity" && ( + {column.inputType === "entity" && (
{/* 🎯 Entity 타입 설정 - 가로 배치 */}
@@ -949,7 +927,7 @@ export default function TableManagementPage() {
)} {/* 다른 웹 타입인 경우 빈 공간 */} - {column.webType !== "code" && column.webType !== "entity" && ( + {column.inputType !== "code" && column.inputType !== "entity" && (
-
)}
diff --git a/frontend/components/admin/AddColumnModal.tsx b/frontend/components/admin/AddColumnModal.tsx index 763cf5c8..2fd6c33e 100644 --- a/frontend/components/admin/AddColumnModal.tsx +++ b/frontend/components/admin/AddColumnModal.tsx @@ -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({ 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 = { webType: webType as any }; + const handleInputTypeChange = (inputType: string) => { + const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType); + const updates: Partial = { 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 ( @@ -248,14 +248,14 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
- - + - {WEB_TYPE_OPTIONS.map((option) => ( + {INPUT_TYPE_OPTIONS.map((option) => (
{option.label}
@@ -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} />

- {webTypeOption?.supportsLength ? "1-65535 범위에서 설정 가능" : "이 타입은 길이 설정이 불가능합니다"} + {inputTypeOption?.supportsLength ? "1-65535 범위에서 설정 가능" : "이 타입은 길이 설정이 불가능합니다"}

diff --git a/frontend/components/admin/ColumnDefinitionTable.tsx b/frontend/components/admin/ColumnDefinitionTable.tsx index aa9b455b..c58ed39b 100644 --- a/frontend/components/admin/ColumnDefinitionTable.tsx +++ b/frontend/components/admin/ColumnDefinitionTable.tsx @@ -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>({}); @@ -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 = { webType: webType as any }; + const handleInputTypeChange = (index: number, inputType: string) => { + const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType); + const updates: Partial = { 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 라벨 - 웹타입 * + 입력타입 * 필수 길이 @@ -183,7 +183,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C {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 + {INPUT_TYPE_OPTIONS.map((option) => ( + + {option.label} - {option.description} + + ))} + +); +``` + +## 🧪 테스트 + +### 전체 시스템 테스트 + +```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. **데이터 백업**: 중요한 변경 전 항상 백업 실행 + +--- + +**🎉 테이블 타입 관리 개선으로 더욱 유연하고 안정적인 시스템을 경험하세요!**