import { Request, Response } from "express"; import { Client } from "pg"; import { logger } from "../utils/logger"; import { AuthenticatedRequest } from "../types/auth"; import { ApiResponse } from "../types/common"; import { TableManagementService } from "../services/tableManagementService"; import { TableInfo, ColumnTypeInfo, ColumnSettings, TableListResponse, ColumnListResponse, ColumnSettingsResponse, } from "../types/tableManagement"; import { query } from "../database/db"; // πŸ†• query ν•¨μˆ˜ import /** * ν…Œμ΄λΈ” λͺ©λ‘ 쑰회 */ export async function getTableList( req: AuthenticatedRequest, res: Response ): Promise { try { logger.info("=== ν…Œμ΄λΈ” λͺ©λ‘ 쑰회 μ‹œμž‘ ==="); const tableManagementService = new TableManagementService(); const tableList = await tableManagementService.getTableList(); logger.info(`ν…Œμ΄λΈ” λͺ©λ‘ 쑰회 κ²°κ³Ό: ${tableList.length}개`); const response: ApiResponse = { success: true, message: "ν…Œμ΄λΈ” λͺ©λ‘μ„ μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: tableList, }; res.status(200).json(response); } 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; const { page = 1, size = 50 } = req.query; // πŸ”₯ νšŒμ‚¬ μ½”λ“œ μΆ”μΆœ (JWTμ—μ„œ λ˜λŠ” DBμ—μ„œ 쑰회) let companyCode = req.user?.companyCode; if (!companyCode && req.user?.userId) { // JWT에 μ—†μœΌλ©΄ DBμ—μ„œ 쑰회 const { query } = require("../database/db"); const userResult = await query( `SELECT company_code FROM user_info WHERE user_id = $1`, [req.user.userId] ); companyCode = userResult[0]?.company_code; logger.info( `DBμ—μ„œ νšŒμ‚¬ μ½”λ“œ 쑰회 (컬럼 λͺ©λ‘): ${req.user.userId} β†’ ${companyCode}` ); } logger.info( `=== 컬럼 정보 쑰회 μ‹œμž‘: ${tableName} (page: ${page}, size: ${size}), company: ${companyCode} ===` ); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); const result = await tableManagementService.getColumnList( tableName, parseInt(page as string), parseInt(size as string), companyCode // πŸ”₯ νšŒμ‚¬ μ½”λ“œ 전달 ); logger.info( `컬럼 정보 쑰회 κ²°κ³Ό: ${tableName}, ${result.columns.length}/${result.total}개 (${result.page}/${result.totalPages} νŽ˜μ΄μ§€)` ); const response: ApiResponse = { success: true, message: "컬럼 λͺ©λ‘μ„ μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: result, }; res.status(200).json(response); } 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; // πŸ”₯ νšŒμ‚¬ μ½”λ“œ μΆ”μΆœ (JWTμ—μ„œ λ˜λŠ” DBμ—μ„œ 쑰회) let companyCode = req.user?.companyCode; if (!companyCode && req.user?.userId) { // JWT에 μ—†μœΌλ©΄ DBμ—μ„œ 쑰회 const { query } = require("../database/db"); const userResult = await query( `SELECT company_code FROM user_info WHERE user_id = $1`, [req.user.userId] ); companyCode = userResult[0]?.company_code; logger.info(`DBμ—μ„œ νšŒμ‚¬ μ½”λ“œ 쑰회: ${req.user.userId} β†’ ${companyCode}`); } logger.info( `=== 컬럼 μ„€μ • μ—…λ°μ΄νŠΈ μ‹œμž‘: ${tableName}.${columnName}, company: ${companyCode} ===` ); 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; } if (!companyCode) { logger.error(`νšŒμ‚¬ μ½”λ“œ λˆ„λ½: ${tableName}.${columnName}`, { user: req.user, hasUser: !!req.user, userId: req.user?.userId, companyCodeFromJWT: req.user?.companyCode, }); const response: ApiResponse = { success: false, message: "νšŒμ‚¬ μ½”λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.", error: { code: "MISSING_COMPANY_CODE", details: "μ‚¬μš©μž μ •λ³΄μ—μ„œ νšŒμ‚¬ μ½”λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. κ΄€λ¦¬μžμ—κ²Œ λ¬Έμ˜ν•˜μ„Έμš”.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); await tableManagementService.updateColumnSettings( tableName, columnName, settings, companyCode // πŸ”₯ νšŒμ‚¬ μ½”λ“œ 전달 ); logger.info( `컬럼 μ„€μ • μ—…λ°μ΄νŠΈ μ™„λ£Œ: ${tableName}.${columnName}, company: ${companyCode}` ); const response: ApiResponse = { success: true, message: "컬럼 섀정을 μ„±κ³΅μ μœΌλ‘œ μ €μž₯ν–ˆμŠ΅λ‹ˆλ‹€.", }; res.status(200).json(response); } 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; // πŸ”₯ νšŒμ‚¬ μ½”λ“œ μΆ”μΆœ (JWTμ—μ„œ λ˜λŠ” DBμ—μ„œ 쑰회) let companyCode = req.user?.companyCode; if (!companyCode && req.user?.userId) { // JWT에 μ—†μœΌλ©΄ DBμ—μ„œ 쑰회 const { query } = require("../database/db"); const userResult = await query( `SELECT company_code FROM user_info WHERE user_id = $1`, [req.user.userId] ); companyCode = userResult[0]?.company_code; logger.info(`DBμ—μ„œ νšŒμ‚¬ μ½”λ“œ 쑰회: ${req.user.userId} β†’ ${companyCode}`); } // πŸ” 디버깅: μ‚¬μš©μž 정보 좜λ ₯ logger.info(`[DEBUG] req.user:`, JSON.stringify(req.user, null, 2)); logger.info(`[DEBUG] req.user?.companyCode: ${req.user?.companyCode}`); logger.info(`[DEBUG] req.user?.userId: ${req.user?.userId}`); logger.info(`[DEBUG] companyCode μ΅œμ’…κ°’: ${companyCode}`); logger.info( `=== 전체 컬럼 μ„€μ • 일괄 μ—…λ°μ΄νŠΈ μ‹œμž‘: ${tableName}, company: ${companyCode} ===` ); 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; } if (!companyCode) { logger.error(`νšŒμ‚¬ μ½”λ“œ λˆ„λ½ (일괄 μ—…λ°μ΄νŠΈ): ${tableName}`, { user: req.user, hasUser: !!req.user, userId: req.user?.userId, companyCodeFromJWT: req.user?.companyCode, settingsCount: columnSettings.length, }); const response: ApiResponse = { success: false, message: "νšŒμ‚¬ μ½”λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.", error: { code: "MISSING_COMPANY_CODE", details: "μ‚¬μš©μž μ •λ³΄μ—μ„œ νšŒμ‚¬ μ½”λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. κ΄€λ¦¬μžμ—κ²Œ λ¬Έμ˜ν•˜μ„Έμš”.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); await tableManagementService.updateAllColumnSettings( tableName, columnSettings, companyCode // πŸ”₯ νšŒμ‚¬ μ½”λ“œ 전달 ); logger.info( `전체 컬럼 μ„€μ • 일괄 μ—…λ°μ΄νŠΈ μ™„λ£Œ: ${tableName}, ${columnSettings.length}개, company: ${companyCode}` ); const response: ApiResponse = { success: true, message: "λͺ¨λ“  컬럼 섀정을 μ„±κ³΅μ μœΌλ‘œ μ €μž₯ν–ˆμŠ΅λ‹ˆλ‹€.", }; res.status(200).json(response); } 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; } const tableManagementService = new TableManagementService(); const tableLabels = await tableManagementService.getTableLabels(tableName); if (!tableLabels) { // 라벨이 μ—†μœΌλ©΄ 빈 객체λ₯Ό μ„±κ³΅μœΌλ‘œ λ°˜ν™˜ (404 μ—λŸ¬ λŒ€μ‹ ) const response: ApiResponse<{}> = { success: true, message: "ν…Œμ΄λΈ” 라벨 정보λ₯Ό μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: {}, }; res.status(200).json(response); return; } logger.info(`ν…Œμ΄λΈ” 라벨 정보 쑰회 μ™„λ£Œ: ${tableName}`); const response: ApiResponse = { success: true, message: "ν…Œμ΄λΈ” 라벨 정보λ₯Ό μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: tableLabels, }; res.status(200).json(response); } 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; } const tableManagementService = new TableManagementService(); const columnLabels = await tableManagementService.getColumnLabels( tableName, columnName ); if (!columnLabels) { // 라벨이 μ—†μœΌλ©΄ 빈 객체λ₯Ό μ„±κ³΅μœΌλ‘œ λ°˜ν™˜ (404 μ—λŸ¬ λŒ€μ‹ ) const response: ApiResponse<{}> = { success: true, message: "컬럼 라벨 정보λ₯Ό μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: {}, }; res.status(200).json(response); return; } logger.info(`컬럼 라벨 정보 쑰회 μ™„λ£Œ: ${tableName}.${columnName}`); const response: ApiResponse = { success: true, message: "컬럼 라벨 정보λ₯Ό μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: columnLabels, }; res.status(200).json(response); } 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); } } /** * ν…Œμ΄λΈ” 라벨 μ„€μ • */ export async function updateTableLabel( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const { displayName, description } = req.body; logger.info(`=== ν…Œμ΄λΈ” 라벨 μ„€μ • μ‹œμž‘: ${tableName} ===`); logger.info(`ν‘œμ‹œλͺ…: ${displayName}, μ„€λͺ…: ${description}`); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); await tableManagementService.updateTableLabel( tableName, displayName, description ); logger.info(`ν…Œμ΄λΈ” 라벨 μ„€μ • μ™„λ£Œ: ${tableName}`); const response: ApiResponse = { success: true, message: "ν…Œμ΄λΈ” 라벨이 μ„±κ³΅μ μœΌλ‘œ μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", data: null, }; res.status(200).json(response); } catch (error) { logger.error("ν…Œμ΄λΈ” 라벨 μ„€μ • 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ” 라벨 μ„€μ • 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "TABLE_LABEL_UPDATE_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * 컬럼 μž…λ ₯ νƒ€μž… μ„€μ • */ export async function updateColumnInputType( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName, columnName } = req.params; const { inputType, detailSettings } = req.body; // πŸ”₯ νšŒμ‚¬ μ½”λ“œ μΆ”μΆœ (JWTμ—μ„œ λ˜λŠ” DBμ—μ„œ 쑰회) let companyCode = req.user?.companyCode; if (!companyCode && req.user?.userId) { // JWT에 μ—†μœΌλ©΄ DBμ—μ„œ 쑰회 const { query } = require("../database/db"); const userResult = await query( `SELECT company_code FROM user_info WHERE user_id = $1`, [req.user.userId] ); companyCode = userResult[0]?.company_code; logger.info(`DBμ—μ„œ νšŒμ‚¬ μ½”λ“œ 쑰회: ${req.user.userId} β†’ ${companyCode}`); } logger.info( `=== 컬럼 μž…λ ₯ νƒ€μž… μ„€μ • μ‹œμž‘: ${tableName}.${columnName} = ${inputType}, company: ${companyCode} ===` ); if (!tableName || !columnName || !inputType) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…, 컬럼λͺ…, μž…λ ₯ νƒ€μž…μ΄ λͺ¨λ‘ ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_PARAMETERS", details: "ν•„μˆ˜ νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } if (!companyCode) { logger.error(`νšŒμ‚¬ μ½”λ“œ λˆ„λ½ (μž…λ ₯ νƒ€μž…): ${tableName}.${columnName}`, { user: req.user, hasUser: !!req.user, userId: req.user?.userId, companyCodeFromJWT: req.user?.companyCode, inputType, }); const response: ApiResponse = { success: false, message: "νšŒμ‚¬ μ½”λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.", error: { code: "MISSING_COMPANY_CODE", details: "μ‚¬μš©μž μ •λ³΄μ—μ„œ νšŒμ‚¬ μ½”λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. κ΄€λ¦¬μžμ—κ²Œ λ¬Έμ˜ν•˜μ„Έμš”.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); await tableManagementService.updateColumnInputType( tableName, columnName, inputType, companyCode, detailSettings ); logger.info( `컬럼 μž…λ ₯ νƒ€μž… μ„€μ • μ™„λ£Œ: ${tableName}.${columnName} = ${inputType}, company: ${companyCode}` ); const response: ApiResponse = { success: true, message: "컬럼 μž…λ ₯ νƒ€μž…μ΄ μ„±κ³΅μ μœΌλ‘œ μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", data: null, }; res.status(200).json(response); } catch (error) { logger.error("컬럼 μž…λ ₯ νƒ€μž… μ„€μ • 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "컬럼 μž…λ ₯ νƒ€μž… μ„€μ • 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "INPUT_TYPE_UPDATE_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * 단일 λ ˆμ½”λ“œ 쑰회 (μžλ™ μž…λ ₯용) */ export async function getTableRecord( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const { filterColumn, filterValue, displayColumn } = req.body; logger.info(`=== 단일 λ ˆμ½”λ“œ 쑰회 μ‹œμž‘: ${tableName} ===`); logger.info(`ν•„ν„°: ${filterColumn} = ${filterValue}`); logger.info(`ν‘œμ‹œ 컬럼: ${displayColumn}`); if (!tableName || !filterColumn || !filterValue || !displayColumn) { const response: ApiResponse = { success: false, message: "ν•„μˆ˜ νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", error: { code: "MISSING_PARAMETERS", details: "tableName, filterColumn, filterValue, displayColumn이 ν•„μš”ν•©λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); // 단일 λ ˆμ½”λ“œ 쑰회 (WHERE filterColumn = filterValue) const result = await tableManagementService.getTableData(tableName, { page: 1, size: 1, search: { [filterColumn]: filterValue, }, }); if (!result.data || result.data.length === 0) { const response: ApiResponse = { success: false, message: "데이터λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.", error: { code: "NOT_FOUND", details: `${filterColumn} = ${filterValue}에 ν•΄λ‹Ήν•˜λŠ” 데이터가 μ—†μŠ΅λ‹ˆλ‹€.`, }, }; res.status(404).json(response); return; } const record = result.data[0]; const displayValue = record[displayColumn]; logger.info(`λ ˆμ½”λ“œ 쑰회 μ™„λ£Œ: ${displayColumn} = ${displayValue}`); const response: ApiResponse<{ value: any; record: any }> = { success: true, message: "λ ˆμ½”λ“œλ₯Ό μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: { value: displayValue, record: record, }, }; res.status(200).json(response); } catch (error) { logger.error("λ ˆμ½”λ“œ 쑰회 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "λ ˆμ½”λ“œ 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "RECORD_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * ν…Œμ΄λΈ” 데이터 쑰회 (νŽ˜μ΄μ§• + 검색 + 필터링) */ export async function getTableData( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const { page = 1, size = 10, search = {}, sortBy, sortOrder = "asc", autoFilter, // πŸ†• μžλ™ ν•„ν„° μ„€μ • μΆ”κ°€ (μ»΄ν¬λ„ŒνŠΈμ—μ„œ 직접 전달) dataFilter, // πŸ†• 컬럼 κ°’ 기반 데이터 필터링 } = req.body; logger.info(`=== ν…Œμ΄λΈ” 데이터 쑰회 μ‹œμž‘: ${tableName} ===`); logger.info(`νŽ˜μ΄μ§•: page=${page}, size=${size}`); logger.info(`검색 쑰건:`, search); logger.info(`μ •λ ¬: ${sortBy} ${sortOrder}`); logger.info(`μžλ™ ν•„ν„°:`, autoFilter); // πŸ†• logger.info(`데이터 ν•„ν„°:`, dataFilter); // πŸ†• if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); // πŸ†• ν˜„μž¬ μ‚¬μš©μž ν•„ν„° 적용 let enhancedSearch = { ...search }; if (autoFilter?.enabled && req.user) { const filterColumn = autoFilter.filterColumn || "company_code"; const userField = autoFilter.userField || "companyCode"; const userValue = (req.user as any)[userField]; if (userValue) { enhancedSearch[filterColumn] = userValue; logger.info("πŸ” ν˜„μž¬ μ‚¬μš©μž ν•„ν„° 적용:", { filterColumn, userField, userValue, tableName, }); } else { logger.warn("⚠️ μ‚¬μš©μž 정보 ν•„λ“œ κ°’ μ—†μŒ:", { userField, user: req.user, }); } } // 데이터 쑰회 const result = await tableManagementService.getTableData(tableName, { page: parseInt(page), size: parseInt(size), search: enhancedSearch, // πŸ†• ν•„ν„°κ°€ 적용된 search μ‚¬μš© sortBy, sortOrder, dataFilter, // πŸ†• 데이터 ν•„ν„° 전달 }); logger.info( `ν…Œμ΄λΈ” 데이터 쑰회 μ™„λ£Œ: ${tableName}, 총 ${result.total}건, νŽ˜μ΄μ§€ ${result.page}/${result.totalPages}` ); const response: ApiResponse = { success: true, message: "ν…Œμ΄λΈ” 데이터λ₯Ό μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: result, }; res.status(200).json(response); } catch (error) { logger.error("ν…Œμ΄λΈ” 데이터 쑰회 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ” 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "TABLE_DATA_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * ν…Œμ΄λΈ” 데이터 μΆ”κ°€ */ export async function addTableData( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const data = req.body; logger.info(`=== ν…Œμ΄λΈ” 데이터 μΆ”κ°€ μ‹œμž‘: ${tableName} ===`); logger.info(`μΆ”κ°€ν•  데이터:`, data); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } if (!data || Object.keys(data).length === 0) { const response: ApiResponse = { success: false, message: "μΆ”κ°€ν•  데이터가 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_DATA", details: "μš”μ²­ 본문에 데이터가 μ—†μŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); // πŸ†• λ©€ν‹°ν…Œλ„Œμ‹œ: company_code μžλ™ μΆ”κ°€ (ν…Œμ΄λΈ”μ— company_code 컬럼이 μžˆλŠ” 경우) const companyCode = req.user?.companyCode; if (companyCode && !data.company_code) { // ν…Œμ΄λΈ”μ— company_code 컬럼이 μžˆλŠ”μ§€ 확인 const hasCompanyCodeColumn = await tableManagementService.hasColumn(tableName, "company_code"); if (hasCompanyCodeColumn) { data.company_code = companyCode; logger.info(`πŸ”’ λ©€ν‹°ν…Œλ„Œμ‹œ: company_code μžλ™ μΆ”κ°€ - ${companyCode}`); } } // 데이터 μΆ”κ°€ await tableManagementService.addTableData(tableName, data); logger.info(`ν…Œμ΄λΈ” 데이터 μΆ”κ°€ μ™„λ£Œ: ${tableName}`); const response: ApiResponse = { success: true, message: "ν…Œμ΄λΈ” 데이터λ₯Ό μ„±κ³΅μ μœΌλ‘œ μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.", }; res.status(201).json(response); } catch (error) { logger.error("ν…Œμ΄λΈ” 데이터 μΆ”κ°€ 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ” 데이터 μΆ”κ°€ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "TABLE_ADD_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * ν…Œμ΄λΈ” 데이터 μˆ˜μ • */ export async function editTableData( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const { originalData, updatedData } = req.body; logger.info(`=== ν…Œμ΄λΈ” 데이터 μˆ˜μ • μ‹œμž‘: ${tableName} ===`); logger.info(`원본 데이터:`, originalData); logger.info(`μˆ˜μ •ν•  데이터:`, updatedData); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "INVALID_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ…이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } if (!originalData || !updatedData) { const response: ApiResponse = { success: false, message: "원본 데이터와 μˆ˜μ •ν•  데이터가 λͺ¨λ‘ ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "INVALID_DATA", details: "originalData와 updatedDataκ°€ λͺ¨λ‘ μ œκ³΅λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } if (Object.keys(updatedData).length === 0) { const response: ApiResponse = { success: false, message: "μˆ˜μ •ν•  데이터가 μ—†μŠ΅λ‹ˆλ‹€.", error: { code: "INVALID_DATA", details: "μˆ˜μ •ν•  데이터가 λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); // 데이터 μˆ˜μ • await tableManagementService.editTableData( tableName, originalData, updatedData ); logger.info(`ν…Œμ΄λΈ” 데이터 μˆ˜μ • μ™„λ£Œ: ${tableName}`); const response: ApiResponse = { success: true, message: "ν…Œμ΄λΈ” 데이터λ₯Ό μ„±κ³΅μ μœΌλ‘œ μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.", }; res.status(200).json(response); } catch (error) { logger.error("ν…Œμ΄λΈ” 데이터 μˆ˜μ • 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ” 데이터 μˆ˜μ • 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "TABLE_EDIT_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * ν…Œμ΄λΈ” μŠ€ν‚€λ§ˆ 정보 쑰회 (컬럼 쑴재 μ—¬λΆ€ κ²€μ¦μš©) */ export async function getTableSchema( 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; } const tableManagementService = new TableManagementService(); const schema = await tableManagementService.getTableSchema(tableName); logger.info( `ν…Œμ΄λΈ” μŠ€ν‚€λ§ˆ 정보 쑰회 μ™„λ£Œ: ${tableName}, ${schema.length}개 컬럼` ); const response: ApiResponse = { success: true, message: "ν…Œμ΄λΈ” μŠ€ν‚€λ§ˆ 정보λ₯Ό μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: schema, }; res.status(200).json(response); } catch (error) { logger.error("ν…Œμ΄λΈ” μŠ€ν‚€λ§ˆ 정보 쑰회 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ” μŠ€ν‚€λ§ˆ 정보 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "TABLE_SCHEMA_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * ν…Œμ΄λΈ” 쑴재 μ—¬λΆ€ 확인 */ export async function checkTableExists( 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; } const tableManagementService = new TableManagementService(); const exists = await tableManagementService.checkTableExists(tableName); logger.info(`ν…Œμ΄λΈ” 쑴재 μ—¬λΆ€ 확인 μ™„λ£Œ: ${tableName} = ${exists}`); const response: ApiResponse<{ exists: boolean }> = { success: true, message: "ν…Œμ΄λΈ” 쑴재 μ—¬λΆ€λ₯Ό ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.", data: { exists }, }; res.status(200).json(response); } catch (error) { logger.error("ν…Œμ΄λΈ” 쑴재 μ—¬λΆ€ 확인 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ” 쑴재 μ—¬λΆ€ 확인 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "TABLE_EXISTS_CHECK_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * 컬럼 μ›Ήνƒ€μž… 정보 쑰회 (화면관리 μ—°λ™μš©) */ export async function getColumnWebTypes( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; // πŸ”₯ νšŒμ‚¬ μ½”λ“œ μΆ”μΆœ (JWTμ—μ„œ λ˜λŠ” DBμ—μ„œ 쑰회) let companyCode = req.user?.companyCode; if (!companyCode && req.user?.userId) { // JWT에 μ—†μœΌλ©΄ DBμ—μ„œ 쑰회 const { query } = require("../database/db"); const userResult = await query( `SELECT company_code FROM user_info WHERE user_id = $1`, [req.user.userId] ); companyCode = userResult[0]?.company_code; logger.info( `DBμ—μ„œ νšŒμ‚¬ μ½”λ“œ 쑰회 (쑰회): ${req.user.userId} β†’ ${companyCode}` ); } logger.info( `=== 컬럼 μ›Ήνƒ€μž… 정보 쑰회 μ‹œμž‘: ${tableName}, company: ${companyCode} ===` ); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } if (!companyCode) { logger.error(`νšŒμ‚¬ μ½”λ“œ λˆ„λ½ (쑰회): ${tableName}`, { user: req.user, hasUser: !!req.user, userId: req.user?.userId, companyCodeFromJWT: req.user?.companyCode, }); const response: ApiResponse = { success: false, message: "νšŒμ‚¬ μ½”λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.", error: { code: "MISSING_COMPANY_CODE", details: "μ‚¬μš©μž μ •λ³΄μ—μ„œ νšŒμ‚¬ μ½”λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. κ΄€λ¦¬μžμ—κ²Œ λ¬Έμ˜ν•˜μ„Έμš”.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); const inputTypes = await tableManagementService.getColumnInputTypes( tableName, companyCode ); logger.info( `컬럼 μž…λ ₯νƒ€μž… 정보 쑰회 μ™„λ£Œ: ${tableName}, company: ${companyCode}, ${inputTypes.length}개 컬럼` ); const response: ApiResponse = { success: true, message: "컬럼 μž…λ ₯νƒ€μž… 정보λ₯Ό μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: inputTypes, }; res.status(200).json(response); } catch (error) { logger.error("컬럼 μ›Ήνƒ€μž… 정보 쑰회 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "컬럼 μ›Ήνƒ€μž… 정보 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "COLUMN_WEB_TYPES_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μƒνƒœ 확인 */ export async function checkDatabaseConnection( req: AuthenticatedRequest, res: Response ): Promise { try { logger.info("=== λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μƒνƒœ 확인 μ‹œμž‘ ==="); const tableManagementService = new TableManagementService(); const connectionStatus = await tableManagementService.checkDatabaseConnection(); logger.info( `λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μƒνƒœ: ${connectionStatus.connected ? "연결됨" : "μ—°κ²° μ•ˆλ¨"}` ); const response: ApiResponse<{ connected: boolean; message: string }> = { success: true, message: "λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μƒνƒœλ₯Ό ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.", data: connectionStatus, }; res.status(200).json(response); } catch (error) { logger.error("λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μƒνƒœ 확인 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μƒνƒœ 확인 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "DATABASE_CONNECTION_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * ν…Œμ΄λΈ” 데이터 μ‚­μ œ */ export async function deleteTableData( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const data = req.body; logger.info(`=== ν…Œμ΄λΈ” 데이터 μ‚­μ œ μ‹œμž‘: ${tableName} ===`); logger.info(`μ‚­μ œν•  데이터:`, data); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } if (!data || (Array.isArray(data) && data.length === 0)) { const response: ApiResponse = { success: false, message: "μ‚­μ œν•  데이터가 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_DATA", details: "μš”μ²­ 본문에 μ‚­μ œν•  데이터가 μ—†μŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); // 데이터 μ‚­μ œ const deletedCount = await tableManagementService.deleteTableData( tableName, data ); logger.info( `ν…Œμ΄λΈ” 데이터 μ‚­μ œ μ™„λ£Œ: ${tableName}, ${deletedCount}건 μ‚­μ œ` ); const response: ApiResponse<{ deletedCount: number }> = { success: true, message: `ν…Œμ΄λΈ” 데이터λ₯Ό μ„±κ³΅μ μœΌλ‘œ μ‚­μ œν–ˆμŠ΅λ‹ˆλ‹€. (${deletedCount}건)`, data: { deletedCount }, }; res.status(200).json(response); } catch (error) { logger.error("ν…Œμ΄λΈ” 데이터 μ‚­μ œ 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ” 데이터 μ‚­μ œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "TABLE_DELETE_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; 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); } } // ======================================== // 🎯 ν…Œμ΄λΈ” 둜그 μ‹œμŠ€ν…œ API // ======================================== /** * 둜그 ν…Œμ΄λΈ” 생성 */ export async function createLogTable( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const { pkColumn } = req.body; const userId = req.user?.userId; logger.info(`=== 둜그 ν…Œμ΄λΈ” 생성 μ‹œμž‘: ${tableName} ===`); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } if (!pkColumn || !pkColumn.columnName || !pkColumn.dataType) { const response: ApiResponse = { success: false, message: "PK 컬럼 정보가 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_PK_COLUMN", details: "PK 컬럼λͺ…κ³Ό 데이터 νƒ€μž…μ΄ ν•„μš”ν•©λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); await tableManagementService.createLogTable(tableName, pkColumn, userId); logger.info(`둜그 ν…Œμ΄λΈ” 생성 μ™„λ£Œ: ${tableName}_log`); const response: ApiResponse = { success: true, message: "둜그 ν…Œμ΄λΈ”μ΄ μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }; res.status(200).json(response); } catch (error) { logger.error("둜그 ν…Œμ΄λΈ” 생성 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "둜그 ν…Œμ΄λΈ” 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "LOG_TABLE_CREATE_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * 둜그 μ„€μ • 쑰회 */ export async function getLogConfig( 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; } const tableManagementService = new TableManagementService(); const logConfig = await tableManagementService.getLogConfig(tableName); const response: ApiResponse = { success: true, message: "둜그 섀정을 μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: logConfig, }; res.status(200).json(response); } catch (error) { logger.error("둜그 μ„€μ • 쑰회 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "둜그 μ„€μ • 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "LOG_CONFIG_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * 둜그 데이터 쑰회 */ export async function getLogData( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const { page = 1, size = 20, operationType, startDate, endDate, changedBy, originalId, } = req.query; logger.info(`=== 둜그 데이터 쑰회: ${tableName} ===`); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); const result = await tableManagementService.getLogData(tableName, { page: parseInt(page as string), size: parseInt(size as string), operationType: operationType as string, startDate: startDate as string, endDate: endDate as string, changedBy: changedBy as string, originalId: originalId as string, }); logger.info(`둜그 데이터 쑰회 μ™„λ£Œ: ${tableName}_log, ${result.total}건`); const response: ApiResponse = { success: true, message: "둜그 데이터λ₯Ό μ‘°νšŒν–ˆμŠ΅λ‹ˆλ‹€.", data: result, }; res.status(200).json(response); } catch (error) { logger.error("둜그 데이터 쑰회 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "둜그 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "LOG_DATA_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * 둜그 ν…Œμ΄λΈ” ν™œμ„±ν™”/λΉ„ν™œμ„±ν™” */ export async function toggleLogTable( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.params; const { isActive } = req.body; logger.info( `=== 둜그 ν…Œμ΄λΈ” ν† κΈ€: ${tableName}, isActive: ${isActive} ===` ); if (!tableName) { const response: ApiResponse = { success: false, message: "ν…Œμ΄λΈ”λͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_TABLE_NAME", details: "ν…Œμ΄λΈ”λͺ… νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } if (isActive === undefined || isActive === null) { const response: ApiResponse = { success: false, message: "isActive 값이 ν•„μš”ν•©λ‹ˆλ‹€.", error: { code: "MISSING_IS_ACTIVE", details: "isActive νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", }, }; res.status(400).json(response); return; } const tableManagementService = new TableManagementService(); await tableManagementService.toggleLogTable( tableName, isActive === "Y" || isActive === true ); logger.info(`둜그 ν…Œμ΄λΈ” ν† κΈ€ μ™„λ£Œ: ${tableName}, isActive: ${isActive}`); const response: ApiResponse = { success: true, message: `둜그 κΈ°λŠ₯이 ${isActive ? "ν™œμ„±ν™”" : "λΉ„ν™œμ„±ν™”"}λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`, }; res.status(200).json(response); } catch (error) { logger.error("둜그 ν…Œμ΄λΈ” ν† κΈ€ 쀑 였λ₯˜ λ°œμƒ:", error); const response: ApiResponse = { success: false, message: "둜그 ν…Œμ΄λΈ” ν† κΈ€ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", error: { code: "LOG_TOGGLE_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * λ©”λ‰΄μ˜ μƒμœ„ 메뉴듀이 μ„€μ •ν•œ λͺ¨λ“  μΉ΄ν…Œκ³ λ¦¬ νƒ€μž… 컬럼 쑰회 (계측 ꡬ쑰 상속) * * @route GET /api/table-management/menu/:menuObjid/category-columns * @description ν˜„μž¬ 메뉴와 μƒμœ„ λ©”λ‰΄λ“€μ—μ„œ μ„€μ •ν•œ category_column_mapping의 λͺ¨λ“  μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회 * * μ˜ˆμ‹œ: * - 2레벨 메뉴 "고객사관리"μ—μ„œ discount_type, rounding_type μ„€μ • * - 3레벨 메뉴 "고객등둝", "고객쑰회" λ“±μ—μ„œλ„ λ™μΌν•˜κ²Œ λ³΄μž„ (상속) */ export async function getCategoryColumnsByMenu( req: AuthenticatedRequest, res: Response ): Promise { try { const { menuObjid } = req.params; const companyCode = req.user?.companyCode; logger.info("πŸ“₯ 메뉴별 μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회 μš”μ²­", { menuObjid, companyCode }); if (!menuObjid) { res.status(400).json({ success: false, message: "메뉴 OBJIDκ°€ ν•„μš”ν•©λ‹ˆλ‹€.", }); return; } const { getPool } = await import("../database/db"); const pool = getPool(); // 1. category_column_mapping ν…Œμ΄λΈ” 쑴재 μ—¬λΆ€ 확인 const tableExistsResult = await pool.query(` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_name = 'category_column_mapping' ) as table_exists `); const mappingTableExists = tableExistsResult.rows[0]?.table_exists === true; let columnsResult; if (mappingTableExists) { // πŸ†• category_column_mapping을 μ‚¬μš©ν•œ 계측 ꡬ쑰 기반 쑰회 logger.info("πŸ” category_column_mapping 기반 μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회 (계측 ꡬ쑰 상속)", { menuObjid, companyCode }); // ν˜„μž¬ 메뉴와 λͺ¨λ“  μƒμœ„ λ©”λ‰΄μ˜ objid 쑰회 (μž¬κ·€) const ancestorMenuQuery = ` WITH RECURSIVE menu_hierarchy AS ( -- ν˜„μž¬ 메뉴 SELECT objid, parent_obj_id, menu_type, menu_name_kor FROM menu_info WHERE objid = $1 UNION ALL -- λΆ€λͺ¨ 메뉴 μž¬κ·€ 쑰회 SELECT m.objid, m.parent_obj_id, m.menu_type, m.menu_name_kor FROM menu_info m INNER JOIN menu_hierarchy mh ON m.objid = mh.parent_obj_id WHERE m.parent_obj_id != 0 -- μ΅œμƒμœ„ 메뉴(parent_obj_id=0) μ œμ™Έ ) SELECT ARRAY_AGG(objid) as menu_objids, ARRAY_AGG(menu_name_kor) as menu_names FROM menu_hierarchy `; const ancestorMenuResult = await pool.query(ancestorMenuQuery, [parseInt(menuObjid)]); const ancestorMenuObjids = ancestorMenuResult.rows[0]?.menu_objids || [parseInt(menuObjid)]; const ancestorMenuNames = ancestorMenuResult.rows[0]?.menu_names || []; logger.info("βœ… μƒμœ„ 메뉴 계측 쑰회 μ™„λ£Œ", { ancestorMenuObjids, ancestorMenuNames, hierarchyDepth: ancestorMenuObjids.length }); // μƒμœ„ 메뉴듀에 μ„€μ •λœ λͺ¨λ“  μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회 (ν…Œμ΄λΈ” 필터링 제거) const columnsQuery = ` SELECT DISTINCT ttc.table_name AS "tableName", COALESCE( tl.table_label, initcap(replace(ttc.table_name, '_', ' ')) ) AS "tableLabel", ccm.logical_column_name AS "columnName", COALESCE( cl.column_label, initcap(replace(ccm.logical_column_name, '_', ' ')) ) AS "columnLabel", ttc.input_type AS "inputType", ccm.menu_objid AS "definedAtMenuObjid" FROM category_column_mapping ccm INNER JOIN table_type_columns ttc ON ccm.table_name = ttc.table_name AND ccm.physical_column_name = ttc.column_name LEFT JOIN column_labels cl ON ttc.table_name = cl.table_name AND ttc.column_name = cl.column_name LEFT JOIN table_labels tl ON ttc.table_name = tl.table_name WHERE ccm.company_code = $1 AND ccm.menu_objid = ANY($2) AND ttc.input_type = 'category' ORDER BY ttc.table_name, ccm.logical_column_name `; columnsResult = await pool.query(columnsQuery, [companyCode, ancestorMenuObjids]); logger.info("βœ… category_column_mapping 기반 쑰회 μ™„λ£Œ (계측 ꡬ쑰 상속)", { rowCount: columnsResult.rows.length, columns: columnsResult.rows.map((r: any) => `${r.tableName}.${r.columnName}`) }); } else { // πŸ”„ λ ˆκ±°μ‹œ 방식: ν˜•μ œ λ©”λ‰΄λ“€μ˜ ν…Œμ΄λΈ”μ—μ„œ λͺ¨λ“  μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회 logger.info("πŸ” λ ˆκ±°μ‹œ 방식: ν˜•μ œ 메뉴 ν…Œμ΄λΈ” 기반 μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회", { menuObjid, companyCode }); // ν˜•μ œ 메뉴 쑰회 const { getSiblingMenuObjids } = await import("../services/menuService"); const siblingObjids = await getSiblingMenuObjids(parseInt(menuObjid)); // ν˜•μ œ 메뉴듀이 μ‚¬μš©ν•˜λŠ” ν…Œμ΄λΈ” 쑰회 const tablesQuery = ` SELECT DISTINCT sd.table_name FROM screen_menu_assignments sma INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id WHERE sma.menu_objid = ANY($1) AND sma.company_code = $2 AND sd.table_name IS NOT NULL `; const tablesResult = await pool.query(tablesQuery, [siblingObjids, companyCode]); const tableNames = tablesResult.rows.map((row: any) => row.table_name); logger.info("βœ… ν˜•μ œ 메뉴 ν…Œμ΄λΈ” 쑰회 μ™„λ£Œ", { tableNames, count: tableNames.length }); if (tableNames.length === 0) { res.json({ success: true, data: [], message: "ν˜•μ œ 메뉴에 μ—°κ²°λœ ν…Œμ΄λΈ”μ΄ μ—†μŠ΅λ‹ˆλ‹€.", }); return; } const columnsQuery = ` SELECT ttc.table_name AS "tableName", COALESCE( tl.table_label, initcap(replace(ttc.table_name, '_', ' ')) ) AS "tableLabel", ttc.column_name AS "columnName", COALESCE( cl.column_label, initcap(replace(ttc.column_name, '_', ' ')) ) AS "columnLabel", ttc.input_type AS "inputType" FROM table_type_columns ttc LEFT JOIN column_labels cl ON ttc.table_name = cl.table_name AND ttc.column_name = cl.column_name LEFT JOIN table_labels tl ON ttc.table_name = tl.table_name WHERE ttc.table_name = ANY($1) AND ttc.company_code = $2 AND ttc.input_type = 'category' ORDER BY ttc.table_name, ttc.column_name `; columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]); logger.info("βœ… λ ˆκ±°μ‹œ 방식 쑰회 μ™„λ£Œ", { rowCount: columnsResult.rows.length }); } logger.info("βœ… μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회 μ™„λ£Œ", { columnCount: columnsResult.rows.length }); res.json({ success: true, data: columnsResult.rows, message: "μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회 성곡", }); } catch (error: any) { logger.error("❌ 메뉴별 μΉ΄ν…Œκ³ λ¦¬ 컬럼 쑰회 μ‹€νŒ¨"); logger.error("μ—λŸ¬ λ©”μ‹œμ§€:", error.message); logger.error("μ—λŸ¬ μŠ€νƒ:", error.stack); logger.error("μ—λŸ¬ 전체:", error); res.status(500).json({ success: false, message: "μΉ΄ν…Œκ³ λ¦¬ 컬럼 μ‘°νšŒμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.", error: error.message, stack: error.stack, // λ””λ²„κΉ…μš© }); } }