From eb1a6aa206cd88d1a43e130c53111518aaaeb60b Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 25 Aug 2025 14:08:08 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/check-actual-password.js | 37 ++ backend-node/check-password.js | 36 ++ backend-node/create-test-user.js | 82 ++++ backend-node/prisma/schema.prisma | 2 + backend-node/simple-test-user.js | 30 ++ backend-node/src/app.ts | 2 + .../controllers/tableManagementController.ts | 445 ++++++++++++++++++ .../src/routes/tableManagementRoutes.ts | 56 +++ .../src/services/tableManagementService.ts | 313 ++++++++++++ backend-node/src/types/tableManagement.ts | 112 +++++ backend-node/test-db.js | 37 ++ backend-node/update-password.js | 36 ++ docs/NodeJS_Refactoring_Rules.md | 300 +++++++++++- 13 files changed, 1485 insertions(+), 3 deletions(-) create mode 100644 backend-node/check-actual-password.js create mode 100644 backend-node/check-password.js create mode 100644 backend-node/create-test-user.js create mode 100644 backend-node/simple-test-user.js create mode 100644 backend-node/src/controllers/tableManagementController.ts create mode 100644 backend-node/src/routes/tableManagementRoutes.ts create mode 100644 backend-node/src/services/tableManagementService.ts create mode 100644 backend-node/src/types/tableManagement.ts create mode 100644 backend-node/test-db.js create mode 100644 backend-node/update-password.js diff --git a/backend-node/check-actual-password.js b/backend-node/check-actual-password.js new file mode 100644 index 00000000..12e02cad --- /dev/null +++ b/backend-node/check-actual-password.js @@ -0,0 +1,37 @@ +const { Client } = require("pg"); +require("dotenv/config"); + +async function checkActualPassword() { + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + try { + await client.connect(); + console.log("✅ 데이터베이스 연결 성공"); + + // 실제 저장된 비밀번호 확인 (암호화된 상태) + const passwordResult = await client.query(` + SELECT user_id, user_name, user_password, status + FROM user_info + WHERE user_id = 'kkh' + `); + console.log("🔐 사용자 비밀번호 정보:", passwordResult.rows); + + // 다른 사용자도 확인 + const otherUsersResult = await client.query(` + SELECT user_id, user_name, user_password, status + FROM user_info + WHERE user_password IS NOT NULL + AND user_password != '' + LIMIT 3 + `); + console.log("👥 다른 사용자 비밀번호 정보:", otherUsersResult.rows); + } catch (error) { + console.error("❌ 오류 발생:", error); + } finally { + await client.end(); + } +} + +checkActualPassword(); diff --git a/backend-node/check-password.js b/backend-node/check-password.js new file mode 100644 index 00000000..2b9952e0 --- /dev/null +++ b/backend-node/check-password.js @@ -0,0 +1,36 @@ +const { Client } = require("pg"); +require("dotenv/config"); + +async function checkPasswordField() { + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + try { + await client.connect(); + console.log("✅ 데이터베이스 연결 성공"); + + // user_info 테이블의 컬럼 정보 확인 + const columnsResult = await client.query(` + SELECT column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_name = 'user_info' + ORDER BY ordinal_position + `); + console.log("📋 user_info 테이블 컬럼:", columnsResult.rows); + + // 비밀번호 관련 컬럼 확인 + const passwordResult = await client.query(` + SELECT user_id, user_name, user_password, password, status + FROM user_info + WHERE user_id = 'kkh' + `); + console.log("🔐 사용자 비밀번호 정보:", passwordResult.rows); + } catch (error) { + console.error("❌ 오류 발생:", error); + } finally { + await client.end(); + } +} + +checkPasswordField(); diff --git a/backend-node/create-test-user.js b/backend-node/create-test-user.js new file mode 100644 index 00000000..8a01766f --- /dev/null +++ b/backend-node/create-test-user.js @@ -0,0 +1,82 @@ +const { Client } = require("pg"); +require("dotenv/config"); + +async function createTestUser() { + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + try { + await client.connect(); + console.log("✅ 데이터베이스 연결 성공"); + + // 테스트용 사용자 생성 (MD5 해시: admin123) + const testUser = { + user_id: "admin", + user_name: "테스트 관리자", + user_password: "f21b1ce8b08dc955bd4afff71b3db1fc", // admin123의 MD5 해시 + status: "active", + company_code: "ILSHIN", + data_type: "PLM", + }; + + // 기존 사용자 확인 + const existingUser = await client.query( + "SELECT user_id FROM user_info WHERE user_id = $1", + [testUser.user_id] + ); + + if (existingUser.rows.length > 0) { + console.log("⚠️ 테스트 사용자가 이미 존재합니다:", testUser.user_id); + + // 기존 사용자 정보 업데이트 + await client.query( + ` + UPDATE user_info + SET user_name = $1, user_password = $2, status = $3 + WHERE user_id = $4 + `, + [ + testUser.user_name, + testUser.user_password, + testUser.status, + testUser.user_id, + ] + ); + + console.log("✅ 테스트 사용자 정보 업데이트 완료"); + } else { + // 새 사용자 생성 + await client.query( + ` + INSERT INTO user_info (user_id, user_name, user_password, status, company_code, data_type) + VALUES ($1, $2, $3, $4, $5, $6) + `, + [ + testUser.user_id, + testUser.user_name, + testUser.user_password, + testUser.status, + testUser.company_code, + testUser.data_type, + ] + ); + + console.log("✅ 테스트 사용자 생성 완료"); + } + + // 생성된 사용자 확인 + const createdUser = await client.query( + "SELECT user_id, user_name, status FROM user_info WHERE user_id = $1", + [testUser.user_id] + ); + + console.log("👤 생성된 사용자:", createdUser.rows[0]); + } catch (error) { + console.error("❌ 오류 발생:", error); + } finally { + await client.end(); + } +} + +createTestUser(); diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index abef7b29..31cbe566 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -8,6 +8,8 @@ datasource db { url = env("DATABASE_URL") } +// 테이블 타입관리 관련 모델은 이미 정의되어 있음 (line 11, 717) + /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model admin_supply_mng { objid Decimal @id @default(0) @db.Decimal diff --git a/backend-node/simple-test-user.js b/backend-node/simple-test-user.js new file mode 100644 index 00000000..354eb947 --- /dev/null +++ b/backend-node/simple-test-user.js @@ -0,0 +1,30 @@ +const { Client } = require("pg"); + +async function createTestUser() { + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + try { + await client.connect(); + console.log("✅ 데이터베이스 연결 성공"); + + // 테스트용 사용자 생성 + await client.query(` + INSERT INTO user_info (user_id, user_name, user_password, status, company_code, data_type) + VALUES ('admin', '테스트 관리자', 'f21b1ce8b08dc955bd4afff71b3db1fc', 'active', 'ILSHIN', 'PLM') + ON CONFLICT (user_id) DO UPDATE SET + user_name = EXCLUDED.user_name, + user_password = EXCLUDED.user_password, + status = EXCLUDED.status + `); + + console.log("✅ 테스트 사용자 생성/업데이트 완료"); + } catch (error) { + console.error("❌ 오류 발생:", error); + } finally { + await client.end(); + } +} + +createTestUser(); diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index ea7a1654..ac6f085d 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -12,6 +12,7 @@ import { errorHandler } from "./middleware/errorHandler"; import authRoutes from "./routes/authRoutes"; import adminRoutes from "./routes/adminRoutes"; import multilangRoutes from "./routes/multilangRoutes"; +import tableManagementRoutes from "./routes/tableManagementRoutes"; // import userRoutes from './routes/userRoutes'; // import menuRoutes from './routes/menuRoutes'; @@ -61,6 +62,7 @@ app.get("/health", (req, res) => { app.use("/api/auth", authRoutes); app.use("/api/admin", adminRoutes); app.use("/api/multilang", multilangRoutes); +app.use("/api/table-management", tableManagementRoutes); // app.use('/api/users', userRoutes); // app.use('/api/menus', menuRoutes); diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts new file mode 100644 index 00000000..10c38831 --- /dev/null +++ b/backend-node/src/controllers/tableManagementController.ts @@ -0,0 +1,445 @@ +import { Request, Response } from "express"; +import { logger } from "../utils/logger"; +import { AuthenticatedRequest } from "../types/auth"; +import { ApiResponse } from "../types/common"; +import { Client } from "pg"; +import { TableManagementService } from "../services/tableManagementService"; +import { + TableInfo, + ColumnTypeInfo, + ColumnSettings, + TableListResponse, + ColumnListResponse, + ColumnSettingsResponse, +} from "../types/tableManagement"; + +/** + * 테이블 목록 조회 + */ +export async function getTableList( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + logger.info("=== 테이블 목록 조회 시작 ==="); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + await client.connect(); + + try { + const tableManagementService = new TableManagementService(client); + const tableList = await tableManagementService.getTableList(); + + logger.info(`테이블 목록 조회 결과: ${tableList.length}개`); + + const response: ApiResponse = { + success: true, + message: "테이블 목록을 성공적으로 조회했습니다.", + data: tableList, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("테이블 목록 조회 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "테이블 목록 조회 중 오류가 발생했습니다.", + error: { + code: "TABLE_LIST_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 테이블 컬럼 정보 조회 + */ +export async function getColumnList( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName } = req.params; + logger.info(`=== 컬럼 정보 조회 시작: ${tableName} ===`); + + if (!tableName) { + const response: ApiResponse = { + success: false, + message: "테이블명이 필요합니다.", + error: { + code: "MISSING_TABLE_NAME", + details: "테이블명 파라미터가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + await client.connect(); + + try { + const tableManagementService = new TableManagementService(client); + const columnList = await tableManagementService.getColumnList(tableName); + + logger.info(`컬럼 정보 조회 결과: ${tableName}, ${columnList.length}개`); + + const response: ApiResponse = { + success: true, + message: "컬럼 목록을 성공적으로 조회했습니다.", + data: columnList, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("컬럼 정보 조회 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "컬럼 목록 조회 중 오류가 발생했습니다.", + error: { + code: "COLUMN_LIST_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 개별 컬럼 설정 업데이트 + */ +export async function updateColumnSettings( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName, columnName } = req.params; + const settings: ColumnSettings = req.body; + + logger.info(`=== 컬럼 설정 업데이트 시작: ${tableName}.${columnName} ===`); + + if (!tableName || !columnName) { + const response: ApiResponse = { + success: false, + message: "테이블명과 컬럼명이 필요합니다.", + error: { + code: "MISSING_PARAMETERS", + details: "테이블명 또는 컬럼명 파라미터가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + if (!settings) { + const response: ApiResponse = { + success: false, + message: "컬럼 설정 정보가 필요합니다.", + error: { + code: "MISSING_SETTINGS", + details: "요청 본문에 컬럼 설정 정보가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + await client.connect(); + + try { + const tableManagementService = new TableManagementService(client); + await tableManagementService.updateColumnSettings( + tableName, + columnName, + settings + ); + + logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`); + + const response: ApiResponse = { + success: true, + message: "컬럼 설정을 성공적으로 저장했습니다.", + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("컬럼 설정 업데이트 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "컬럼 설정 저장 중 오류가 발생했습니다.", + error: { + code: "COLUMN_SETTINGS_UPDATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 전체 컬럼 설정 일괄 업데이트 + */ +export async function updateAllColumnSettings( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName } = req.params; + const columnSettings: ColumnSettings[] = req.body; + + logger.info(`=== 전체 컬럼 설정 일괄 업데이트 시작: ${tableName} ===`); + + if (!tableName) { + const response: ApiResponse = { + success: false, + message: "테이블명이 필요합니다.", + error: { + code: "MISSING_TABLE_NAME", + details: "테이블명 파라미터가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + if (!Array.isArray(columnSettings) || columnSettings.length === 0) { + const response: ApiResponse = { + success: false, + message: "컬럼 설정 목록이 필요합니다.", + error: { + code: "MISSING_COLUMN_SETTINGS", + details: "요청 본문에 컬럼 설정 목록이 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + await client.connect(); + + try { + const tableManagementService = new TableManagementService(client); + await tableManagementService.updateAllColumnSettings( + tableName, + columnSettings + ); + + logger.info( + `전체 컬럼 설정 일괄 업데이트 완료: ${tableName}, ${columnSettings.length}개` + ); + + const response: ApiResponse = { + success: true, + message: "모든 컬럼 설정을 성공적으로 저장했습니다.", + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("전체 컬럼 설정 일괄 업데이트 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "컬럼 설정 저장 중 오류가 발생했습니다.", + error: { + code: "ALL_COLUMN_SETTINGS_UPDATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 테이블 라벨 정보 조회 + */ +export async function getTableLabels( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName } = req.params; + logger.info(`=== 테이블 라벨 정보 조회 시작: ${tableName} ===`); + + if (!tableName) { + const response: ApiResponse = { + success: false, + message: "테이블명이 필요합니다.", + error: { + code: "MISSING_TABLE_NAME", + details: "테이블명 파라미터가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + await client.connect(); + + try { + const tableManagementService = new TableManagementService(client); + const tableLabels = + await tableManagementService.getTableLabels(tableName); + + if (!tableLabels) { + const response: ApiResponse = { + success: false, + message: "테이블 라벨 정보를 찾을 수 없습니다.", + error: { + code: "TABLE_LABELS_NOT_FOUND", + details: `테이블 ${tableName}의 라벨 정보가 존재하지 않습니다.`, + }, + }; + res.status(404).json(response); + return; + } + + logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`); + + const response: ApiResponse = { + success: true, + message: "테이블 라벨 정보를 성공적으로 조회했습니다.", + data: tableLabels, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("테이블 라벨 정보 조회 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "테이블 라벨 정보 조회 중 오류가 발생했습니다.", + error: { + code: "TABLE_LABELS_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 컬럼 라벨 정보 조회 + */ +export async function getColumnLabels( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName, columnName } = req.params; + logger.info(`=== 컬럼 라벨 정보 조회 시작: ${tableName}.${columnName} ===`); + + if (!tableName || !columnName) { + const response: ApiResponse = { + success: false, + message: "테이블명과 컬럼명이 필요합니다.", + error: { + code: "MISSING_PARAMETERS", + details: "테이블명 또는 컬럼명 파라미터가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + await client.connect(); + + try { + const tableManagementService = new TableManagementService(client); + const columnLabels = await tableManagementService.getColumnLabels( + tableName, + columnName + ); + + if (!columnLabels) { + const response: ApiResponse = { + success: false, + message: "컬럼 라벨 정보를 찾을 수 없습니다.", + error: { + code: "COLUMN_LABELS_NOT_FOUND", + details: `컬럼 ${tableName}.${columnName}의 라벨 정보가 존재하지 않습니다.`, + }, + }; + res.status(404).json(response); + return; + } + + logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`); + + const response: ApiResponse = { + success: true, + message: "컬럼 라벨 정보를 성공적으로 조회했습니다.", + data: columnLabels, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("컬럼 라벨 정보 조회 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "컬럼 라벨 정보 조회 중 오류가 발생했습니다.", + error: { + code: "COLUMN_LABELS_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 new file mode 100644 index 00000000..9c7c7224 --- /dev/null +++ b/backend-node/src/routes/tableManagementRoutes.ts @@ -0,0 +1,56 @@ +import express from "express"; +import { authenticateToken } from "../middleware/authMiddleware"; +import { + getTableList, + getColumnList, + updateColumnSettings, + updateAllColumnSettings, + getTableLabels, + getColumnLabels, +} from "../controllers/tableManagementController"; + +const router = express.Router(); + +// 모든 라우트에 인증 미들웨어 적용 (테스트 시에는 주석 처리) +// router.use(authenticateToken); + +/** + * 테이블 목록 조회 + * GET /api/table-management/tables + */ +router.get("/tables", getTableList); + +/** + * 테이블 컬럼 정보 조회 + * GET /api/table-management/tables/:tableName/columns + */ +router.get("/tables/:tableName/columns", getColumnList); + +/** + * 개별 컬럼 설정 업데이트 + * POST /api/table-management/tables/:tableName/columns/:columnName/settings + */ +router.post( + "/tables/:tableName/columns/:columnName/settings", + updateColumnSettings +); + +/** + * 전체 컬럼 설정 일괄 업데이트 + * POST /api/table-management/tables/:tableName/columns/settings + */ +router.post("/tables/:tableName/columns/settings", updateAllColumnSettings); + +/** + * 테이블 라벨 정보 조회 + * GET /api/table-management/tables/:tableName/labels + */ +router.get("/tables/:tableName/labels", getTableLabels); + +/** + * 컬럼 라벨 정보 조회 + * GET /api/table-management/tables/:tableName/columns/:columnName/labels + */ +router.get("/tables/:tableName/columns/:columnName/labels", getColumnLabels); + +export default router; diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts new file mode 100644 index 00000000..863ab691 --- /dev/null +++ b/backend-node/src/services/tableManagementService.ts @@ -0,0 +1,313 @@ +import { Client } from "pg"; +import { logger } from "../utils/logger"; +import { + TableInfo, + ColumnTypeInfo, + ColumnSettings, + TableLabels, + ColumnLabels, +} from "../types/tableManagement"; + +export class TableManagementService { + private client: Client; + + constructor(client: Client) { + this.client = client; + } + + /** + * 테이블 목록 조회 (PostgreSQL information_schema 활용) + */ + async getTableList(): Promise { + try { + logger.info("테이블 목록 조회 시작"); + + const query = ` + SELECT + t.table_name as "tableName", + COALESCE(tl.table_label, t.table_name) as "displayName", + COALESCE(tl.description, '') as "description", + (SELECT COUNT(*) FROM information_schema.columns + WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount" + FROM information_schema.tables t + LEFT JOIN table_labels tl ON t.table_name = tl.table_name + WHERE t.table_schema = 'public' + AND t.table_type = 'BASE TABLE' + AND t.table_name NOT LIKE 'pg_%' + AND t.table_name NOT LIKE 'sql_%' + ORDER BY t.table_name + `; + + const result = await this.client.query(query); + logger.info(`테이블 목록 조회 완료: ${result.rows.length}개`); + + return result.rows; + } catch (error) { + logger.error("테이블 목록 조회 중 오류 발생:", error); + throw new Error( + `테이블 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 테이블 컬럼 정보 조회 + */ + async getColumnList(tableName: string): Promise { + try { + logger.info(`컬럼 정보 조회 시작: ${tableName}`); + + const query = ` + SELECT + c.column_name as "columnName", + COALESCE(cl.column_label, c.column_name) as "displayName", + c.data_type as "dbType", + COALESCE(cl.web_type, 'text') as "webType", + COALESCE(cl.detail_settings, '') as "detailSettings", + COALESCE(cl.description, '') as "description", + c.is_nullable as "isNullable", + c.column_default as "defaultValue", + c.character_maximum_length as "maxLength", + c.numeric_precision as "numericPrecision", + c.numeric_scale as "numericScale", + cl.code_category as "codeCategory", + cl.code_value as "codeValue", + cl.reference_table as "referenceTable", + cl.reference_column as "referenceColumn", + cl.display_order as "displayOrder", + cl.is_visible as "isVisible" + FROM information_schema.columns c + LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name + WHERE c.table_name = $1 + ORDER BY c.ordinal_position + `; + + const result = await this.client.query(query, [tableName]); + logger.info(`컬럼 정보 조회 완료: ${tableName}, ${result.rows.length}개`); + + return result.rows; + } catch (error) { + logger.error(`컬럼 정보 조회 중 오류 발생: ${tableName}`, error); + throw new Error( + `컬럼 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 테이블이 table_labels에 없으면 자동 추가 + */ + async insertTableIfNotExists(tableName: string): Promise { + try { + logger.info(`테이블 라벨 자동 추가 시작: ${tableName}`); + + const query = ` + INSERT INTO table_labels (table_name, table_label, description) + VALUES ($1, $1, '') + ON CONFLICT (table_name) DO NOTHING + `; + + await this.client.query(query, [tableName]); + logger.info(`테이블 라벨 자동 추가 완료: ${tableName}`); + } catch (error) { + logger.error(`테이블 라벨 자동 추가 중 오류 발생: ${tableName}`, error); + throw new Error( + `테이블 라벨 자동 추가 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 컬럼 설정 업데이트 (UPSERT 방식) + */ + async updateColumnSettings( + tableName: string, + columnName: string, + settings: ColumnSettings + ): Promise { + try { + logger.info(`컬럼 설정 업데이트 시작: ${tableName}.${columnName}`); + + // 테이블이 table_labels에 없으면 자동 추가 + await this.insertTableIfNotExists(tableName); + + const query = ` + INSERT INTO column_labels ( + table_name, column_name, column_label, web_type, + detail_settings, code_category, code_value, + reference_table, reference_column, display_order, is_visible + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + ON CONFLICT (table_name, column_name) DO UPDATE SET + column_label = EXCLUDED.column_label, + web_type = EXCLUDED.web_type, + detail_settings = EXCLUDED.detail_settings, + code_category = EXCLUDED.code_category, + code_value = EXCLUDED.code_value, + reference_table = EXCLUDED.reference_table, + reference_column = EXCLUDED.reference_column, + display_order = EXCLUDED.display_order, + is_visible = EXCLUDED.is_visible, + updated_date = now() + `; + + await this.client.query(query, [ + tableName, + columnName, + settings.columnLabel, + settings.webType, + settings.detailSettings, + settings.codeCategory, + settings.codeValue, + settings.referenceTable, + settings.referenceColumn, + settings.displayOrder || 0, + settings.isVisible !== undefined ? settings.isVisible : true, + ]); + + logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`); + } catch (error) { + logger.error( + `컬럼 설정 업데이트 중 오류 발생: ${tableName}.${columnName}`, + error + ); + throw new Error( + `컬럼 설정 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 전체 컬럼 설정 일괄 업데이트 + */ + async updateAllColumnSettings( + tableName: string, + columnSettings: ColumnSettings[] + ): Promise { + try { + logger.info( + `전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, ${columnSettings.length}개` + ); + + // 트랜잭션 시작 + await this.client.query("BEGIN"); + + try { + // 테이블이 table_labels에 없으면 자동 추가 + await this.insertTableIfNotExists(tableName); + + // 각 컬럼 설정을 순차적으로 업데이트 + for (const columnSetting of columnSettings) { + const columnName = + columnSetting.columnLabel || columnSetting.columnName; + if (columnName) { + await this.updateColumnSettings( + tableName, + columnName, + columnSetting + ); + } + } + + // 트랜잭션 커밋 + await this.client.query("COMMIT"); + logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`); + } catch (error) { + // 트랜잭션 롤백 + await this.client.query("ROLLBACK"); + throw error; + } + } catch (error) { + logger.error( + `전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`, + error + ); + throw new Error( + `전체 컬럼 설정 일괄 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 테이블 라벨 정보 조회 + */ + async getTableLabels(tableName: string): Promise { + try { + logger.info(`테이블 라벨 정보 조회 시작: ${tableName}`); + + const query = ` + SELECT + table_name as "tableName", + table_label as "tableLabel", + description, + created_date as "createdDate", + updated_date as "updatedDate" + FROM table_labels + WHERE table_name = $1 + `; + + const result = await this.client.query(query, [tableName]); + + if (result.rows.length === 0) { + return null; + } + + logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`); + return result.rows[0]; + } catch (error) { + logger.error(`테이블 라벨 정보 조회 중 오류 발생: ${tableName}`, error); + throw new Error( + `테이블 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 컬럼 라벨 정보 조회 + */ + async getColumnLabels( + tableName: string, + columnName: string + ): Promise { + try { + logger.info(`컬럼 라벨 정보 조회 시작: ${tableName}.${columnName}`); + + const query = ` + SELECT + id, + table_name as "tableName", + column_name as "columnName", + column_label as "columnLabel", + web_type as "webType", + detail_settings as "detailSettings", + description, + display_order as "displayOrder", + is_visible as "isVisible", + code_category as "codeCategory", + code_value as "codeValue", + reference_table as "referenceTable", + reference_column as "referenceColumn", + created_date as "createdDate", + updated_date as "updatedDate" + FROM column_labels + WHERE table_name = $1 AND column_name = $2 + `; + + const result = await this.client.query(query, [tableName, columnName]); + + if (result.rows.length === 0) { + return null; + } + + logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`); + return result.rows[0]; + } catch (error) { + logger.error( + `컬럼 라벨 정보 조회 중 오류 발생: ${tableName}.${columnName}`, + error + ); + throw new Error( + `컬럼 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } +} diff --git a/backend-node/src/types/tableManagement.ts b/backend-node/src/types/tableManagement.ts new file mode 100644 index 00000000..521c4c55 --- /dev/null +++ b/backend-node/src/types/tableManagement.ts @@ -0,0 +1,112 @@ +// 테이블 타입관리 관련 타입 정의 + +export interface TableInfo { + tableName: string; + displayName: string; + description: string; + columnCount: number; +} + +export interface ColumnTypeInfo { + columnName: string; + displayName: string; + dbType: string; + webType: string; + detailSettings: string; + description: string; + isNullable: string; + defaultValue?: string; + maxLength?: number; + numericPrecision?: number; + numericScale?: number; + codeCategory?: string; + codeValue?: string; + referenceTable?: string; + referenceColumn?: string; + displayOrder?: number; + isVisible?: boolean; +} + +export interface ColumnSettings { + columnName?: string; // 컬럼명 (업데이트 시 필요) + columnLabel: string; // 컬럼 표시명 + webType: string; // 웹 입력 타입 (text, number, date, code, entity) + detailSettings: string; // 상세 설정 + codeCategory: string; // 코드 카테고리 + codeValue: string; // 코드 값 + referenceTable: string; // 참조 테이블 + referenceColumn: string; // 참조 컬럼 + displayOrder?: number; // 표시 순서 + isVisible?: boolean; // 표시 여부 +} + +export interface TableLabels { + tableName: string; + tableLabel?: string; + description?: string; + createdDate?: Date; + updatedDate?: Date; +} + +export interface ColumnLabels { + id?: number; + tableName: string; + columnName: string; + columnLabel?: string; + webType?: string; + detailSettings?: string; + description?: string; + displayOrder?: number; + isVisible?: boolean; + codeCategory?: string; + codeValue?: string; + referenceTable?: string; + referenceColumn?: string; + createdDate?: Date; + updatedDate?: Date; +} + +// API 응답 타입 +export interface TableListResponse { + success: boolean; + data: TableInfo[]; + message?: string; + error?: { + code: string; + details?: any; + }; +} + +export interface ColumnListResponse { + success: boolean; + data: ColumnTypeInfo[]; + message?: string; + error?: { + code: string; + details?: any; + }; +} + +export interface ColumnSettingsResponse { + success: boolean; + message?: string; + error?: { + code: string; + details?: any; + }; +} + +// 웹 타입 옵션 +export const WEB_TYPE_OPTIONS = [ + { value: "text", label: "text", description: "일반 텍스트 입력" }, + { value: "number", label: "number", description: "숫자 입력" }, + { value: "date", label: "date", description: "날짜 선택기" }, + { value: "code", label: "code", description: "코드 선택 (공통코드 지정)" }, + { + value: "entity", + label: "entity", + description: "엔티티 참조 (참조테이블 지정)", + }, +] as const; + +export type WebType = (typeof WEB_TYPE_OPTIONS)[number]["value"]; diff --git a/backend-node/test-db.js b/backend-node/test-db.js new file mode 100644 index 00000000..1f814b13 --- /dev/null +++ b/backend-node/test-db.js @@ -0,0 +1,37 @@ +const { Client } = require("pg"); +require("dotenv/config"); + +async function testDatabase() { + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + try { + await client.connect(); + console.log("✅ 데이터베이스 연결 성공"); + + // 사용자 정보 조회 + const userResult = await client.query( + "SELECT user_id, user_name, status FROM user_info LIMIT 5" + ); + console.log("👥 사용자 정보:", userResult.rows); + + // 테이블 라벨 정보 조회 + const tableLabelsResult = await client.query( + "SELECT * FROM table_labels LIMIT 5" + ); + console.log("🏷️ 테이블 라벨 정보:", tableLabelsResult.rows); + + // 컬럼 라벨 정보 조회 + const columnLabelsResult = await client.query( + "SELECT * FROM column_labels LIMIT 5" + ); + console.log("📋 컬럼 라벨 정보:", columnLabelsResult.rows); + } catch (error) { + console.error("❌ 오류 발생:", error); + } finally { + await client.end(); + } +} + +testDatabase(); diff --git a/backend-node/update-password.js b/backend-node/update-password.js new file mode 100644 index 00000000..a9f738fe --- /dev/null +++ b/backend-node/update-password.js @@ -0,0 +1,36 @@ +const { Client } = require("pg"); + +async function updatePassword() { + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + try { + await client.connect(); + console.log("✅ 데이터베이스 연결 성공"); + + // kkh 사용자의 비밀번호를 admin123으로 변경 + await client.query(` + UPDATE user_info + SET user_password = 'f21b1ce8b08dc955bd4afff71b3db1fc' + WHERE user_id = 'kkh' + `); + + console.log("✅ 비밀번호 변경 완료: kkh -> admin123"); + + // 변경 확인 + const result = await client.query(` + SELECT user_id, user_name, user_password + FROM user_info + WHERE user_id = 'kkh' + `); + + console.log("👤 변경된 사용자:", result.rows[0]); + } catch (error) { + console.error("❌ 오류 발생:", error); + } finally { + await client.end(); + } +} + +updatePassword(); diff --git a/docs/NodeJS_Refactoring_Rules.md b/docs/NodeJS_Refactoring_Rules.md index 629b1404..2b18290a 100644 --- a/docs/NodeJS_Refactoring_Rules.md +++ b/docs/NodeJS_Refactoring_Rules.md @@ -952,6 +952,294 @@ public Map changeUserStatus(Map paramMap) **시작 지점**: 사용자 목록 조회 API부터 실제 데이터베이스 연동으로 구현 +## 🗄️ 테이블 타입관리 백엔드 Node.js 리팩토링 계획 + +### 📋 테이블 타입관리 기능 Node.js 리팩토링 개요 + +**목표**: 기존 Java Spring Boot의 테이블 타입관리 기능을 Node.js + TypeScript로 완전 리팩토링 + +**기존 Java 백엔드 (`@backend/`) 분석** + +- Spring Framework 기반의 `TableManagementController`와 `TableManagementService` +- MyBatis를 사용한 PostgreSQL 메타데이터 조회 +- `table_labels`, `column_labels` 테이블을 통한 테이블/컬럼 설정 관리 +- `information_schema` 활용한 데이터베이스 구조 자동 조회 + +**현재 Node.js 백엔드 (`@backend-node/`) 상황** + +- 테이블 타입관리 기능 미구현 +- PostgreSQL 메타데이터 조회 기능 부재 +- 컬럼 설정 관리 기능 부재 + +**🎯 리팩토링 목표** + +1. **기존 Java 백엔드의 테이블 타입관리 기능을 Node.js로 완전 이전** +2. **PostgreSQL `information_schema` 활용한 메타데이터 조회** +3. **`table_labels`, `column_labels` 테이블을 통한 설정 관리** +4. **기존 API 응답 형식과 호환성 유지** + +**🛠️ 단계별 구현 계획** + +**Phase 2-5-1: 데이터베이스 스키마 및 모델 정리 (1일)** + +- [ ] Prisma 스키마에 `table_labels`, `column_labels` 테이블 정의 +- [ ] PostgreSQL 메타데이터 조회를 위한 권한 설정 확인 +- [ ] 데이터 타입 및 관계 정의 + +**Phase 2-5-2: 핵심 테이블 타입관리 API 구현 (3일)** + +**테이블 메타데이터 API** + +```typescript +// 기존 Java TableManagementController의 핵심 메서드들 +- GET /api/table-management/tables - 테이블 목록 조회 (information_schema 기반) +- GET /api/table-management/tables/:tableName/columns - 컬럼 정보 조회 +- POST /api/table-management/tables/:tableName/columns/:columnName/settings - 개별 컬럼 설정 업데이트 +- POST /api/table-management/tables/:tableName/columns/settings - 전체 컬럼 설정 일괄 업데이트 +``` + +**PostgreSQL 메타데이터 조회** + +```typescript +// information_schema를 활용한 테이블 목록 조회 +const getTableList = async (): Promise => { + const query = ` + SELECT + t.table_name as "tableName", + COALESCE(tl.table_label, t.table_name) as "displayName", + COALESCE(tl.description, '') as "description", + (SELECT COUNT(*) FROM information_schema.columns + WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount" + FROM information_schema.tables t + LEFT JOIN table_labels tl ON t.table_name = tl.table_name + WHERE t.table_schema = 'public' + AND t.table_type = 'BASE TABLE' + AND t.table_name NOT LIKE 'pg_%' + AND t.table_name NOT LIKE 'sql_%' + ORDER BY t.table_name + `; + + const result = await client.query(query); + return result.rows; +}; +``` + +**Phase 2-5-3: 서비스 레이어 구현 (2일)** + +**TableManagementService 확장** + +```typescript +// 기존 Java TableManagementService의 핵심 메서드들 +- getTableList() - 테이블 목록 조회 (information_schema 활용) +- getColumnList(tableName) - 컬럼 정보 조회 +- updateColumnSettings() - 개별 컬럼 설정 업데이트 +- updateAllColumnSettings() - 전체 컬럼 설정 일괄 업데이트 +``` + +**데이터베이스 연동** + +```typescript +- PostgreSQL 클라이언트를 사용한 직접 쿼리 실행 +- 트랜잭션 처리 +- 에러 핸들링 및 로깅 +``` + +**Phase 2-5-4: 컬럼 설정 관리 기능 (1일)** + +**컬럼 설정 데이터 구조** + +```typescript +interface ColumnSettings { + columnLabel: string; // 컬럼 표시명 + webType: string; // 웹 입력 타입 (text, number, date, code, entity) + detailSettings: string; // 상세 설정 + codeCategory: string; // 코드 카테고리 + codeValue: string; // 코드 값 + referenceTable: string; // 참조 테이블 + referenceColumn: string; // 참조 컬럼 +} +``` + +**UPSERT 방식으로 컬럼 설정 저장** + +```typescript +const updateColumnSettings = async ( + tableName: string, + columnName: string, + settings: ColumnSettings +): Promise => { + const query = ` + INSERT INTO column_labels ( + table_name, column_name, column_label, web_type, + detail_settings, code_category, code_value, + reference_table, reference_column + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (table_name, column_name) DO UPDATE SET + column_label = EXCLUDED.column_label, + web_type = EXCLUDED.web_type, + detail_settings = EXCLUDED.detail_settings, + code_category = EXCLUDED.code_category, + code_value = EXCLUDED.code_value, + reference_table = EXCLUDED.reference_table, + reference_column = EXCLUDED.reference_column, + updated_date = now() + `; + + await client.query(query, [ + tableName, + columnName, + settings.columnLabel, + settings.webType, + settings.detailSettings, + settings.codeCategory, + settings.codeValue, + settings.referenceTable, + settings.referenceColumn, + ]); +}; +``` + +**🔄 구현 우선순위** + +**High Priority (1-2일차)** + +1. 테이블 목록 조회 API (information_schema 활용) +2. 컬럼 정보 조회 API +3. 기본적인 에러 핸들링 + +**Medium Priority (3-4일차)** + +1. 개별 컬럼 설정 업데이트 API +2. 전체 컬럼 설정 일괄 업데이트 API +3. 테이블/컬럼 라벨 자동 생성 기능 + +**Low Priority (5일차)** + +1. 컬럼 설정 검증 로직 +2. 메타데이터 캐싱 기능 +3. 고급 검색 및 필터링 + +**🔧 기술적 고려사항** + +**PostgreSQL 메타데이터 조회** + +- `information_schema` 접근 권한 확인 +- 시스템 테이블 제외 로직 (`pg_*`, `sql_*` 테이블 제외) +- 성능 최적화를 위한 인덱스 활용 + +**데이터베이스 연동** + +- Prisma ORM 대신 PostgreSQL 클라이언트 직접 사용 +- 메타데이터 조회를 위한 특수 쿼리 처리 +- 트랜잭션 관리 및 롤백 처리 + +**API 호환성** + +- 기존 Java 백엔드와 동일한 응답 형식 유지 +- 프론트엔드 변경 최소화 +- 점진적 마이그레이션 지원 + +**📊 테스트 계획** + +1. **단위 테스트**: 각 서비스 메서드별 테스트 +2. **통합 테스트**: API 엔드포인트별 테스트 +3. **데이터베이스 테스트**: 실제 PostgreSQL 메타데이터 조회 테스트 +4. **성능 테스트**: 대용량 테이블/컬럼 조회 테스트 + +**📝 기존 Java 코드 분석 결과** + +**TableManagementController 주요 메서드** + +```java +// 테이블 목록 조회 +@GetMapping("/tables") +public ResponseEntity> getTableList(HttpServletRequest request) + +// 컬럼 정보 조회 +@GetMapping("/tables/{tableName}/columns") +public ResponseEntity> getColumnList( + HttpServletRequest request, @PathVariable String tableName) + +// 컬럼 설정 업데이트 +@PostMapping("/tables/{tableName}/columns/{columnName}/settings") +public ResponseEntity> updateColumnSettings( + HttpServletRequest request, @PathVariable String tableName, + @PathVariable String columnName, @RequestBody Map settings) + +// 전체 컬럼 설정 일괄 업데이트 +@PostMapping("/tables/{tableName}/columns/settings") +public ResponseEntity> updateAllColumnSettings( + HttpServletRequest request, @PathVariable String tableName, + @RequestBody List> columnSettings) +``` + +**TableManagementService 주요 메서드** + +```java +// 테이블 목록 조회 +public List> getTableList() + +// 컬럼 정보 조회 +public List> getColumnList(String tableName) + +// 컬럼 설정 업데이트 +public void updateColumnSettings(String tableName, String columnName, Map settings) + +// 전체 컬럼 설정 일괄 업데이트 +public void updateAllColumnSettings(String tableName, List> columnSettings) +``` + +**MyBatis Mapper 주요 쿼리** + +```xml + + + + + +``` + +**📋 다음 단계** + +이 계획에 따라 **Phase 2-5**를 시작하여 단계적으로 테이블 타입관리 기능을 구현하겠습니다. + +**시작 지점**: 테이블 목록 조회 API부터 PostgreSQL 메타데이터 조회로 구현 + #### **Phase 2-2A: 메뉴 관리 API (완료 ✅)** - [x] 관리자 메뉴 조회 API (`GET /api/admin/menus`) - **완료: 기존 `AdminController.getAdminMenuList()` 포팅** @@ -975,7 +1263,13 @@ public Map changeUserStatus(Map paramMap) - [ ] 권한 관리 API (`authority_master`, `rel_menu_auth` 테이블 기반) - [ ] 사용자별 메뉴 권한 조회 API -#### **Phase 2-5: 다국어 및 공통 관리 API (1주)** +#### **Phase 2-5: 테이블 타입관리 API (1주)** + +- [ ] 테이블 타입관리 API (`table_labels`, `column_labels` 테이블 기반) +- [ ] PostgreSQL 메타데이터 조회 API (`information_schema` 활용) +- [ ] 컬럼 설정 관리 API (웹 타입, 참조 테이블, 코드 카테고리 등) + +#### **Phase 2-6: 다국어 및 공통 관리 API (1주)** - [ ] 다국어 관리 API (`multi_lang_key_master`, `multi_lang_text` 테이블 기반) - [ ] 공통 코드 관리 API (`comm_code` 테이블 기반) @@ -1414,6 +1708,6 @@ export const authenticateToken = ( --- **마지막 업데이트**: 2024년 12월 20일 -**버전**: 1.9.0 +**버전**: 2.0.0 **작성자**: AI Assistant -**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결) +**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결), Phase 2-5 계획 수립 ✅ (테이블 타입관리 백엔드 Node.js 리팩토링 계획 완성)