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(); // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž ํ•„ํ„ฐ ์ ์šฉ (autoFilter๊ฐ€ ์—†๊ฑฐ๋‚˜ enabled๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ false๊ฐ€ ์•„๋‹ˆ๋ฉด ๊ธฐ๋ณธ ์ ์šฉ) let enhancedSearch = { ...search }; const shouldApplyAutoFilter = autoFilter?.enabled !== false; // ๊ธฐ๋ณธ๊ฐ’: true if (shouldApplyAutoFilter && 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}`); } } // ๐Ÿ†• writer ์ปฌ๋Ÿผ ์ž๋™ ์ถ”๊ฐ€ (ํ…Œ์ด๋ธ”์— writer ์ปฌ๋Ÿผ์ด ์žˆ๊ณ  ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ) const userId = req.user?.userId; if (userId && !data.writer) { const hasWriterColumn = await tableManagementService.hasColumn(tableName, "writer"); if (hasWriterColumn) { data.writer = userId; logger.info(`writer ์ž๋™ ์ถ”๊ฐ€ - ${userId}`); } } // ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ 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, // ๋””๋ฒ„๊น…์šฉ }); } } /** * ๋ฒ”์šฉ ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ API * * ๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ์„œ๋ธŒ ํ…Œ์ด๋ธ”(๋“ค)์— ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. * * ์š”์ฒญ ๋ณธ๋ฌธ: * { * mainTable: { tableName: string, primaryKeyColumn: string }, * mainData: Record, * subTables: Array<{ * tableName: string, * linkColumn: { mainField: string, subColumn: string }, * items: Record[], * options?: { * saveMainAsFirst?: boolean, * mainFieldMappings?: Array<{ formField: string, targetColumn: string }>, * mainMarkerColumn?: string, * mainMarkerValue?: any, * subMarkerValue?: any, * deleteExistingBefore?: boolean, * } * }>, * isUpdate?: boolean * } */ export async function multiTableSave( req: AuthenticatedRequest, res: Response ): Promise { const pool = require("../database/db").getPool(); const client = await pool.connect(); try { const { mainTable, mainData, subTables, isUpdate } = req.body; const companyCode = req.user?.companyCode || "*"; logger.info("=== ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ ์‹œ์ž‘ ===", { mainTable, mainDataKeys: Object.keys(mainData || {}), subTablesCount: subTables?.length || 0, isUpdate, companyCode, }); // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ if (!mainTable?.tableName || !mainTable?.primaryKeyColumn) { res.status(400).json({ success: false, message: "๋ฉ”์ธ ํ…Œ์ด๋ธ” ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", }); return; } if (!mainData || Object.keys(mainData).length === 0) { res.status(400).json({ success: false, message: "์ €์žฅํ•  ๋ฉ”์ธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", }); return; } await client.query("BEGIN"); // 1. ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์ €์žฅ const mainTableName = mainTable.tableName; const pkColumn = mainTable.primaryKeyColumn; const pkValue = mainData[pkColumn]; // company_code ์ž๋™ ์ถ”๊ฐ€ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ) if (companyCode !== "*" && !mainData.company_code) { mainData.company_code = companyCode; } let mainResult: any; if (isUpdate && pkValue) { // UPDATE const updateColumns = Object.keys(mainData) .filter(col => col !== pkColumn) .map((col, idx) => `"${col}" = $${idx + 1}`) .join(", "); const updateValues = Object.keys(mainData) .filter(col => col !== pkColumn) .map(col => mainData[col]); // updated_at ์ปฌ๋Ÿผ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ const hasUpdatedAt = await client.query(` SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = 'updated_at' `, [mainTableName]); const updatedAtClause = hasUpdatedAt.rowCount && hasUpdatedAt.rowCount > 0 ? ", updated_at = NOW()" : ""; const updateQuery = ` UPDATE "${mainTableName}" SET ${updateColumns}${updatedAtClause} WHERE "${pkColumn}" = $${updateValues.length + 1} ${companyCode !== "*" ? `AND company_code = $${updateValues.length + 2}` : ""} RETURNING * `; const updateParams = companyCode !== "*" ? [...updateValues, pkValue, companyCode] : [...updateValues, pkValue]; logger.info("๋ฉ”์ธ ํ…Œ์ด๋ธ” UPDATE:", { query: updateQuery, paramsCount: updateParams.length }); mainResult = await client.query(updateQuery, updateParams); } else { // INSERT const columns = Object.keys(mainData).map(col => `"${col}"`).join(", "); const placeholders = Object.keys(mainData).map((_, idx) => `$${idx + 1}`).join(", "); const values = Object.values(mainData); // updated_at ์ปฌ๋Ÿผ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ const hasUpdatedAt = await client.query(` SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = 'updated_at' `, [mainTableName]); const updatedAtClause = hasUpdatedAt.rowCount && hasUpdatedAt.rowCount > 0 ? ", updated_at = NOW()" : ""; const updateSetClause = Object.keys(mainData) .filter(col => col !== pkColumn) .map(col => `"${col}" = EXCLUDED."${col}"`) .join(", "); const insertQuery = ` INSERT INTO "${mainTableName}" (${columns}) VALUES (${placeholders}) ON CONFLICT ("${pkColumn}") DO UPDATE SET ${updateSetClause}${updatedAtClause} RETURNING * `; logger.info("๋ฉ”์ธ ํ…Œ์ด๋ธ” INSERT/UPSERT:", { query: insertQuery, paramsCount: values.length }); mainResult = await client.query(insertQuery, values); } if (mainResult.rowCount === 0) { throw new Error("๋ฉ”์ธ ํ…Œ์ด๋ธ” ์ €์žฅ ์‹คํŒจ"); } const savedMainData = mainResult.rows[0]; const savedPkValue = savedMainData[pkColumn]; logger.info("๋ฉ”์ธ ํ…Œ์ด๋ธ” ์ €์žฅ ์™„๋ฃŒ:", { pkColumn, savedPkValue }); // 2. ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ €์žฅ const subTableResults: any[] = []; for (const subTableConfig of subTables || []) { const { tableName, linkColumn, items, options } = subTableConfig; // saveMainAsFirst๊ฐ€ ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, items๊ฐ€ ๋น„์–ด์žˆ์–ด๋„ ๋ฉ”์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ธŒ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•ด์•ผ ํ•จ const hasSaveMainAsFirst = options?.saveMainAsFirst && options?.mainFieldMappings && options.mainFieldMappings.length > 0; if (!tableName || (!items?.length && !hasSaveMainAsFirst)) { logger.info(`์„œ๋ธŒ ํ…Œ์ด๋ธ” ${tableName} ์Šคํ‚ต: ๋ฐ์ดํ„ฐ ์—†์Œ (saveMainAsFirst: ${hasSaveMainAsFirst})`); continue; } logger.info(`์„œ๋ธŒ ํ…Œ์ด๋ธ” ${tableName} ์ €์žฅ ์‹œ์ž‘:`, { itemsCount: items?.length || 0, linkColumn, options, hasSaveMainAsFirst, }); // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์˜ต์…˜ if (options?.deleteExistingBefore && linkColumn?.subColumn) { const deleteQuery = options?.deleteOnlySubItems && options?.mainMarkerColumn ? `DELETE FROM "${tableName}" WHERE "${linkColumn.subColumn}" = $1 AND "${options.mainMarkerColumn}" = $2` : `DELETE FROM "${tableName}" WHERE "${linkColumn.subColumn}" = $1`; const deleteParams = options?.deleteOnlySubItems && options?.mainMarkerColumn ? [savedPkValue, options.subMarkerValue ?? false] : [savedPkValue]; logger.info(`์„œ๋ธŒ ํ…Œ์ด๋ธ” ${tableName} ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ:`, { deleteQuery, deleteParams }); await client.query(deleteQuery, deleteParams); } // ๋ฉ”์ธ ๋ฐ์ดํ„ฐ๋„ ์„œ๋ธŒ ํ…Œ์ด๋ธ”์— ์ €์žฅ (์˜ต์…˜) // mainFieldMappings๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด ๊ฑด๋„ˆ๋œ€ (ํ•„์ˆ˜ ์ปฌ๋Ÿผ ๋ˆ„๋ฝ ๋ฐฉ์ง€) logger.info(`saveMainAsFirst ์˜ต์…˜ ํ™•์ธ:`, { saveMainAsFirst: options?.saveMainAsFirst, mainFieldMappings: options?.mainFieldMappings, mainFieldMappingsLength: options?.mainFieldMappings?.length, linkColumn, mainDataKeys: Object.keys(mainData), }); if (options?.saveMainAsFirst && options?.mainFieldMappings && options.mainFieldMappings.length > 0 && linkColumn?.subColumn) { const mainSubItem: Record = { [linkColumn.subColumn]: savedPkValue, }; // ๋ฉ”์ธ ํ•„๋“œ ๋งคํ•‘ ์ ์šฉ for (const mapping of options.mainFieldMappings) { if (mapping.formField && mapping.targetColumn) { mainSubItem[mapping.targetColumn] = mainData[mapping.formField]; } } // ๋ฉ”์ธ ๋งˆ์ปค ์„ค์ • if (options.mainMarkerColumn) { mainSubItem[options.mainMarkerColumn] = options.mainMarkerValue ?? true; } // company_code ์ถ”๊ฐ€ if (companyCode !== "*") { mainSubItem.company_code = companyCode; } // ๋จผ์ € ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ (user_id + is_primary ์กฐํ•ฉ) const checkQuery = ` SELECT * FROM "${tableName}" WHERE "${linkColumn.subColumn}" = $1 ${options.mainMarkerColumn ? `AND "${options.mainMarkerColumn}" = $2` : ""} ${companyCode !== "*" ? `AND company_code = $${options.mainMarkerColumn ? 3 : 2}` : ""} LIMIT 1 `; const checkParams: any[] = [savedPkValue]; if (options.mainMarkerColumn) { checkParams.push(options.mainMarkerValue ?? true); } if (companyCode !== "*") { checkParams.push(companyCode); } const existingResult = await client.query(checkQuery, checkParams); if (existingResult.rows.length > 0) { // UPDATE const updateColumns = Object.keys(mainSubItem) .filter(col => col !== linkColumn.subColumn && col !== options.mainMarkerColumn && col !== "company_code") .map((col, idx) => `"${col}" = $${idx + 1}`) .join(", "); const updateValues = Object.keys(mainSubItem) .filter(col => col !== linkColumn.subColumn && col !== options.mainMarkerColumn && col !== "company_code") .map(col => mainSubItem[col]); if (updateColumns) { const updateQuery = ` UPDATE "${tableName}" SET ${updateColumns} WHERE "${linkColumn.subColumn}" = $${updateValues.length + 1} ${options.mainMarkerColumn ? `AND "${options.mainMarkerColumn}" = $${updateValues.length + 2}` : ""} ${companyCode !== "*" ? `AND company_code = $${updateValues.length + (options.mainMarkerColumn ? 3 : 2)}` : ""} RETURNING * `; const updateParams = [...updateValues, savedPkValue]; if (options.mainMarkerColumn) { updateParams.push(options.mainMarkerValue ?? true); } if (companyCode !== "*") { updateParams.push(companyCode); } const updateResult = await client.query(updateQuery, updateParams); subTableResults.push({ tableName, type: "main", data: updateResult.rows[0] }); } else { subTableResults.push({ tableName, type: "main", data: existingResult.rows[0] }); } } else { // INSERT const mainSubColumns = Object.keys(mainSubItem).map(col => `"${col}"`).join(", "); const mainSubPlaceholders = Object.keys(mainSubItem).map((_, idx) => `$${idx + 1}`).join(", "); const mainSubValues = Object.values(mainSubItem); const insertQuery = ` INSERT INTO "${tableName}" (${mainSubColumns}) VALUES (${mainSubPlaceholders}) RETURNING * `; const insertResult = await client.query(insertQuery, mainSubValues); subTableResults.push({ tableName, type: "main", data: insertResult.rows[0] }); } } // ์„œ๋ธŒ ์•„์ดํ…œ๋“ค ์ €์žฅ for (const item of items) { // ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ ๊ฐ’ ์„ค์ • if (linkColumn?.subColumn) { item[linkColumn.subColumn] = savedPkValue; } // company_code ์ถ”๊ฐ€ if (companyCode !== "*" && !item.company_code) { item.company_code = companyCode; } const subColumns = Object.keys(item).map(col => `"${col}"`).join(", "); const subPlaceholders = Object.keys(item).map((_, idx) => `$${idx + 1}`).join(", "); const subValues = Object.values(item); const subInsertQuery = ` INSERT INTO "${tableName}" (${subColumns}) VALUES (${subPlaceholders}) RETURNING * `; logger.info(`์„œ๋ธŒ ํ…Œ์ด๋ธ” ${tableName} ์•„์ดํ…œ ์ €์žฅ:`, { subInsertQuery, subValuesCount: subValues.length }); const subResult = await client.query(subInsertQuery, subValues); subTableResults.push({ tableName, type: "sub", data: subResult.rows[0] }); } logger.info(`์„œ๋ธŒ ํ…Œ์ด๋ธ” ${tableName} ์ €์žฅ ์™„๋ฃŒ`); } await client.query("COMMIT"); logger.info("=== ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ ์™„๋ฃŒ ===", { mainTable: mainTableName, mainPk: savedPkValue, subTableResultsCount: subTableResults.length, }); res.json({ success: true, message: "๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", data: { main: savedMainData, subTables: subTableResults, }, }); } catch (error: any) { await client.query("ROLLBACK"); logger.error("๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ ์‹คํŒจ:", { message: error.message, stack: error.stack, }); res.status(500).json({ success: false, message: error.message || "๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", error: error.message, }); } finally { client.release(); } }