2025-08-25 14:08:08 +09:00
import { Request , Response } from "express" ;
2025-09-01 15:22:47 +09:00
import { Client } from "pg" ;
2025-08-25 14:08:08 +09:00
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" ;
2025-11-04 14:33:39 +09:00
import { query } from "../database/db" ; // 🆕 query 함수 import
2025-08-25 14:08:08 +09:00
/ * *
* 테 이 블 목 록 조 회
* /
export async function getTableList (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
logger . info ( "=== 테이블 목록 조회 시작 ===" ) ;
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService ( ) ;
const tableList = await tableManagementService . getTableList ( ) ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
logger . info ( ` 테이블 목록 조회 결과: ${ tableList . length } 개 ` ) ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
const response : ApiResponse < TableInfo [ ] > = {
success : true ,
message : "테이블 목록을 성공적으로 조회했습니다." ,
data : tableList ,
} ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
res . status ( 200 ) . json ( response ) ;
2025-08-25 14:08:08 +09:00
} catch ( error ) {
logger . error ( "테이블 목록 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName } = req . params ;
2025-09-08 14:20:01 +09:00
const { page = 1 , size = 50 } = req . query ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req . user ? . companyCode ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
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 ;
2025-11-07 14:27:07 +09:00
logger . info (
` DB에서 회사 코드 조회 (컬럼 목록): ${ req . user . userId } → ${ companyCode } `
) ;
2025-11-06 17:01:13 +09:00
}
2025-09-08 14:20:01 +09:00
logger . info (
2025-11-06 17:01:13 +09:00
` === 컬럼 정보 조회 시작: ${ tableName } (page: ${ page } , size: ${ size } ), company: ${ companyCode } === `
2025-09-08 14:20:01 +09:00
) ;
2025-08-25 14:08:08 +09:00
if ( ! tableName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명이 필요합니다." ,
error : {
code : "MISSING_TABLE_NAME" ,
details : "테이블명 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService ( ) ;
2026-01-15 15:17:52 +09:00
2025-12-19 15:44:38 +09:00
// 🔥 캐시 버스팅: _t 파라미터가 있으면 캐시 무시
const bustCache = ! ! req . query . _t ;
2026-01-15 15:17:52 +09:00
2025-09-08 14:20:01 +09:00
const result = await tableManagementService . getColumnList (
tableName ,
parseInt ( page as string ) ,
2025-11-06 17:01:13 +09:00
parseInt ( size as string ) ,
2025-12-19 15:44:38 +09:00
companyCode , // 🔥 회사 코드 전달
bustCache // 🔥 캐시 버스팅 옵션
2025-09-08 14:20:01 +09:00
) ;
2025-08-25 14:08:08 +09:00
2025-09-08 14:20:01 +09:00
logger . info (
` 컬럼 정보 조회 결과: ${ tableName } , ${ result . columns . length } / ${ result . total } 개 ( ${ result . page } / ${ result . totalPages } 페이지) `
) ;
2025-08-25 14:08:08 +09:00
2025-09-08 14:20:01 +09:00
const response : ApiResponse < typeof result > = {
2025-09-01 11:00:38 +09:00
success : true ,
message : "컬럼 목록을 성공적으로 조회했습니다." ,
2025-09-08 14:20:01 +09:00
data : result ,
2025-09-01 11:00:38 +09:00
} ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
res . status ( 200 ) . json ( response ) ;
2025-08-25 14:08:08 +09:00
} catch ( error ) {
logger . error ( "컬럼 정보 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName , columnName } = req . params ;
const settings : ColumnSettings = req . body ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req . user ? . companyCode ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
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 } ` ) ;
}
2025-08-25 14:08:08 +09:00
2025-11-07 14:27:07 +09:00
logger . info (
` === 컬럼 설정 업데이트 시작: ${ tableName } . ${ columnName } , company: ${ companyCode } === `
) ;
2025-08-25 14:08:08 +09:00
if ( ! tableName || ! columnName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명과 컬럼명이 필요합니다." ,
error : {
code : "MISSING_PARAMETERS" ,
details : "테이블명 또는 컬럼명 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
if ( ! settings ) {
const response : ApiResponse < null > = {
success : false ,
message : "컬럼 설정 정보가 필요합니다." ,
error : {
code : "MISSING_SETTINGS" ,
details : "요청 본문에 컬럼 설정 정보가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-11-06 17:01:13 +09:00
if ( ! companyCode ) {
logger . error ( ` 회사 코드 누락: ${ tableName } . ${ columnName } ` , {
user : req.user ,
hasUser : ! ! req . user ,
userId : req.user?.userId ,
companyCodeFromJWT : req.user?.companyCode ,
} ) ;
const response : ApiResponse < null > = {
success : false ,
message : "회사 코드를 찾을 수 없습니다." ,
error : {
code : "MISSING_COMPANY_CODE" ,
2025-11-07 14:27:07 +09:00
details :
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요." ,
2025-11-06 17:01:13 +09:00
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService ( ) ;
await tableManagementService . updateColumnSettings (
tableName ,
columnName ,
2025-11-06 17:01:13 +09:00
settings ,
companyCode // 🔥 회사 코드 전달
2025-09-01 11:00:38 +09:00
) ;
2025-08-25 14:08:08 +09:00
2025-11-07 14:27:07 +09:00
logger . info (
` 컬럼 설정 업데이트 완료: ${ tableName } . ${ columnName } , company: ${ companyCode } `
) ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
const response : ApiResponse < null > = {
success : true ,
message : "컬럼 설정을 성공적으로 저장했습니다." ,
} ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
res . status ( 200 ) . json ( response ) ;
2025-08-25 14:08:08 +09:00
} catch ( error ) {
logger . error ( "컬럼 설정 업데이트 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName } = req . params ;
const columnSettings : ColumnSettings [ ] = req . body ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req . user ? . companyCode ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
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 } ` ) ;
}
2025-08-25 14:08:08 +09:00
2025-11-06 17:01:13 +09:00
// 🔍 디버깅: 사용자 정보 출력
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 } ` ) ;
2025-11-07 14:27:07 +09:00
logger . info (
` === 전체 컬럼 설정 일괄 업데이트 시작: ${ tableName } , company: ${ companyCode } === `
) ;
2025-08-25 14:08:08 +09:00
if ( ! tableName ) {
const response : ApiResponse < null > = {
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 < null > = {
success : false ,
message : "컬럼 설정 목록이 필요합니다." ,
error : {
code : "MISSING_COLUMN_SETTINGS" ,
details : "요청 본문에 컬럼 설정 목록이 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-11-06 17:01:13 +09:00
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 < null > = {
success : false ,
message : "회사 코드를 찾을 수 없습니다." ,
error : {
code : "MISSING_COMPANY_CODE" ,
2025-11-07 14:27:07 +09:00
details :
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요." ,
2025-11-06 17:01:13 +09:00
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService ( ) ;
await tableManagementService . updateAllColumnSettings (
tableName ,
2025-11-06 17:01:13 +09:00
columnSettings ,
companyCode // 🔥 회사 코드 전달
2025-09-01 11:00:38 +09:00
) ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
logger . info (
2025-11-06 17:01:13 +09:00
` 전체 컬럼 설정 일괄 업데이트 완료: ${ tableName } , ${ columnSettings . length } 개, company: ${ companyCode } `
2025-09-01 11:00:38 +09:00
) ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
const response : ApiResponse < null > = {
success : true ,
message : "모든 컬럼 설정을 성공적으로 저장했습니다." ,
} ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
res . status ( 200 ) . json ( response ) ;
2025-08-25 14:08:08 +09:00
} catch ( error ) {
logger . error ( "전체 컬럼 설정 일괄 업데이트 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName } = req . params ;
logger . info ( ` === 테이블 라벨 정보 조회 시작: ${ tableName } === ` ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명이 필요합니다." ,
error : {
code : "MISSING_TABLE_NAME" ,
details : "테이블명 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService ( ) ;
const tableLabels = await tableManagementService . getTableLabels ( tableName ) ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
if ( ! tableLabels ) {
2025-09-19 11:00:47 +09:00
// 라벨이 없으면 빈 객체를 성공으로 반환 (404 에러 대신)
const response : ApiResponse < { } > = {
success : true ,
message : "테이블 라벨 정보를 조회했습니다." ,
data : { } ,
2025-09-01 11:00:38 +09:00
} ;
2025-09-19 11:00:47 +09:00
res . status ( 200 ) . json ( response ) ;
2025-09-01 11:00:38 +09:00
return ;
2025-08-25 14:08:08 +09:00
}
2025-09-01 11:00:38 +09:00
logger . info ( ` 테이블 라벨 정보 조회 완료: ${ tableName } ` ) ;
const response : ApiResponse < any > = {
success : true ,
message : "테이블 라벨 정보를 성공적으로 조회했습니다." ,
data : tableLabels ,
} ;
res . status ( 200 ) . json ( response ) ;
2025-08-25 14:08:08 +09:00
} catch ( error ) {
logger . error ( "테이블 라벨 정보 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName , columnName } = req . params ;
logger . info ( ` === 컬럼 라벨 정보 조회 시작: ${ tableName } . ${ columnName } === ` ) ;
if ( ! tableName || ! columnName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명과 컬럼명이 필요합니다." ,
error : {
code : "MISSING_PARAMETERS" ,
details : "테이블명 또는 컬럼명 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService ( ) ;
const columnLabels = await tableManagementService . getColumnLabels (
tableName ,
columnName
) ;
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
if ( ! columnLabels ) {
2025-09-19 11:00:47 +09:00
// 라벨이 없으면 빈 객체를 성공으로 반환 (404 에러 대신)
const response : ApiResponse < { } > = {
success : true ,
message : "컬럼 라벨 정보를 조회했습니다." ,
data : { } ,
2025-09-01 11:00:38 +09:00
} ;
2025-09-19 11:00:47 +09:00
res . status ( 200 ) . json ( response ) ;
2025-09-01 11:00:38 +09:00
return ;
2025-08-25 14:08:08 +09:00
}
2025-09-01 11:00:38 +09:00
logger . info ( ` 컬럼 라벨 정보 조회 완료: ${ tableName } . ${ columnName } ` ) ;
const response : ApiResponse < any > = {
success : true ,
message : "컬럼 라벨 정보를 성공적으로 조회했습니다." ,
data : columnLabels ,
} ;
res . status ( 200 ) . json ( response ) ;
2025-08-25 14:08:08 +09:00
} catch ( error ) {
logger . error ( "컬럼 라벨 정보 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
success : false ,
message : "컬럼 라벨 정보 조회 중 오류가 발생했습니다." ,
error : {
code : "COLUMN_LABELS_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-09-01 11:48:12 +09:00
2025-09-08 14:20:01 +09:00
/ * *
* 테 이 블 라 벨 설 정
* /
export async function updateTableLabel (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
const { displayName , description } = req . body ;
logger . info ( ` === 테이블 라벨 설정 시작: ${ tableName } === ` ) ;
logger . info ( ` 표시명: ${ displayName } , 설명: ${ description } ` ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
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 < null > = {
success : true ,
message : "테이블 라벨이 성공적으로 설정되었습니다." ,
data : null ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "테이블 라벨 설정 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
success : false ,
message : "테이블 라벨 설정 중 오류가 발생했습니다." ,
error : {
code : "TABLE_LABEL_UPDATE_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-09-01 11:48:12 +09:00
/ * *
2025-09-23 10:40:21 +09:00
* 컬 럼 입 력 타 입 설 정
2025-09-01 11:48:12 +09:00
* /
2025-09-23 10:40:21 +09:00
export async function updateColumnInputType (
2025-09-01 11:48:12 +09:00
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName , columnName } = req . params ;
2026-02-02 12:07:37 +09:00
let { inputType , detailSettings } = req . body ;
// 🔥 "direct" 또는 "auto"는 프론트엔드의 입력 방식 구분값이므로
// DB의 input_type(웹타입)으로 저장하면 안 됨 - "text"로 변환
if ( inputType === "direct" || inputType === "auto" ) {
logger . warn (
` 잘못된 inputType 값 감지: ${ inputType } → 'text'로 변환 ( ${ tableName } . ${ columnName } ) `
) ;
inputType = "text" ;
}
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req . user ? . companyCode ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
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 } ` ) ;
}
2025-09-01 11:48:12 +09:00
logger . info (
2025-11-06 17:01:13 +09:00
` === 컬럼 입력 타입 설정 시작: ${ tableName } . ${ columnName } = ${ inputType } , company: ${ companyCode } === `
2025-09-01 11:48:12 +09:00
) ;
2025-09-23 10:40:21 +09:00
if ( ! tableName || ! columnName || ! inputType ) {
2025-09-01 11:48:12 +09:00
const response : ApiResponse < null > = {
success : false ,
2025-09-23 10:40:21 +09:00
message : "테이블명, 컬럼명, 입력 타입이 모두 필요합니다." ,
2025-09-01 11:48:12 +09:00
error : {
code : "MISSING_PARAMETERS" ,
details : "필수 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-11-06 17:01:13 +09:00
if ( ! companyCode ) {
logger . error ( ` 회사 코드 누락 (입력 타입): ${ tableName } . ${ columnName } ` , {
user : req.user ,
hasUser : ! ! req . user ,
userId : req.user?.userId ,
companyCodeFromJWT : req.user?.companyCode ,
inputType ,
} ) ;
const response : ApiResponse < null > = {
success : false ,
message : "회사 코드를 찾을 수 없습니다." ,
error : {
code : "MISSING_COMPANY_CODE" ,
2025-11-07 14:27:07 +09:00
details :
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요." ,
2025-11-06 17:01:13 +09:00
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-09-01 15:22:47 +09:00
const tableManagementService = new TableManagementService ( ) ;
2025-09-23 10:40:21 +09:00
await tableManagementService . updateColumnInputType (
2025-09-01 15:22:47 +09:00
tableName ,
columnName ,
2025-09-23 10:40:21 +09:00
inputType ,
2025-11-06 17:01:13 +09:00
companyCode ,
2025-09-23 10:40:21 +09:00
detailSettings
2025-09-01 15:22:47 +09:00
) ;
2025-09-01 11:48:12 +09:00
2025-09-01 15:22:47 +09:00
logger . info (
2025-11-06 17:01:13 +09:00
` 컬럼 입력 타입 설정 완료: ${ tableName } . ${ columnName } = ${ inputType } , company: ${ companyCode } `
2025-09-01 15:22:47 +09:00
) ;
2025-09-01 11:48:12 +09:00
2025-09-01 15:22:47 +09:00
const response : ApiResponse < null > = {
success : true ,
2025-09-23 10:40:21 +09:00
message : "컬럼 입력 타입이 성공적으로 설정되었습니다." ,
2025-09-01 15:22:47 +09:00
data : null ,
} ;
2025-09-01 11:48:12 +09:00
2025-09-01 15:22:47 +09:00
res . status ( 200 ) . json ( response ) ;
2025-09-01 11:48:12 +09:00
} catch ( error ) {
2025-09-23 10:40:21 +09:00
logger . error ( "컬럼 입력 타입 설정 중 오류 발생:" , error ) ;
2025-09-01 11:48:12 +09:00
const response : ApiResponse < null > = {
success : false ,
2025-09-23 10:40:21 +09:00
message : "컬럼 입력 타입 설정 중 오류가 발생했습니다." ,
2025-09-01 11:48:12 +09:00
error : {
2025-09-23 10:40:21 +09:00
code : "INPUT_TYPE_UPDATE_ERROR" ,
2025-09-01 11:48:12 +09:00
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-09-03 15:23:12 +09:00
/ * *
2025-11-04 14:33:39 +09:00
* 단 일 레 코 드 조 회 ( 자 동 입 력 용 )
* /
export async function getTableRecord (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
const { filterColumn , filterValue , displayColumn } = req . body ;
logger . info ( ` === 단일 레코드 조회 시작: ${ tableName } === ` ) ;
logger . info ( ` 필터: ${ filterColumn } = ${ filterValue } ` ) ;
logger . info ( ` 표시 컬럼: ${ displayColumn } ` ) ;
2026-02-02 12:01:39 +09:00
if ( ! tableName || ! filterColumn || ! filterValue ) {
2025-11-04 14:33:39 +09:00
const response : ApiResponse < null > = {
success : false ,
message : "필수 파라미터가 누락되었습니다." ,
error : {
code : "MISSING_PARAMETERS" ,
details :
2026-02-02 12:01:39 +09:00
"tableName, filterColumn, filterValue가 필요합니다. displayColumn은 선택적입니다." ,
2025-11-04 14:33:39 +09:00
} ,
} ;
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 < null > = {
success : false ,
message : "데이터를 찾을 수 없습니다." ,
error : {
code : "NOT_FOUND" ,
details : ` ${ filterColumn } = ${ filterValue } 에 해당하는 데이터가 없습니다. ` ,
} ,
} ;
res . status ( 404 ) . json ( response ) ;
return ;
}
const record = result . data [ 0 ] ;
2026-02-02 12:01:39 +09:00
// displayColumn이 "*"이거나 없으면 전체 레코드 반환
const displayValue = displayColumn && displayColumn !== "*"
? record [ displayColumn ]
: record ;
2025-11-04 14:33:39 +09:00
2026-02-02 12:01:39 +09:00
logger . info ( ` 레코드 조회 완료: ${ displayColumn || "*" } = ${ typeof displayValue === 'object' ? '[전체 레코드]' : displayValue } ` ) ;
2025-11-04 14:33:39 +09:00
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 < null > = {
success : false ,
message : "레코드 조회 중 오류가 발생했습니다." ,
error : {
code : "RECORD_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
/ * *
* 테 이 블 데 이 터 조 회 ( 페 이 징 + 검 색 + 필 터 링 )
2025-09-03 15:23:12 +09:00
* /
export async function getTableData (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
2025-09-03 16:38:10 +09:00
const {
page = 1 ,
size = 10 ,
search = { } ,
sortBy ,
sortOrder = "asc" ,
2025-11-04 14:33:39 +09:00
autoFilter , // 🆕 자동 필터 설정 추가 (컴포넌트에서 직접 전달)
2025-11-13 17:06:41 +09:00
dataFilter , // 🆕 컬럼 값 기반 데이터 필터링
2025-09-03 16:38:10 +09:00
} = req . body ;
2025-09-03 15:23:12 +09:00
logger . info ( ` === 테이블 데이터 조회 시작: ${ tableName } === ` ) ;
logger . info ( ` 페이징: page= ${ page } , size= ${ size } ` ) ;
logger . info ( ` 검색 조건: ` , search ) ;
logger . info ( ` 정렬: ${ sortBy } ${ sortOrder } ` ) ;
2025-11-04 14:33:39 +09:00
logger . info ( ` 자동 필터: ` , autoFilter ) ; // 🆕
2025-11-13 17:06:41 +09:00
logger . info ( ` 데이터 필터: ` , dataFilter ) ; // 🆕
2025-09-03 15:23:12 +09:00
if ( ! tableName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명이 필요합니다." ,
error : {
code : "MISSING_TABLE_NAME" ,
details : "테이블명 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
const tableManagementService = new TableManagementService ( ) ;
2025-09-03 16:38:10 +09:00
2025-12-17 16:38:12 +09:00
// 🆕 현재 사용자 필터 적용 (autoFilter가 없거나 enabled가 명시적으로 false가 아니면 기본 적용)
2025-11-04 14:33:39 +09:00
let enhancedSearch = { . . . search } ;
2025-12-17 16:38:12 +09:00
const shouldApplyAutoFilter = autoFilter ? . enabled !== false ; // 기본값: true
if ( shouldApplyAutoFilter && req . user ) {
const filterColumn = autoFilter ? . filterColumn || "company_code" ;
const userField = autoFilter ? . userField || "companyCode" ;
2025-11-04 14:33:39 +09:00
const userValue = ( req . user as any ) [ userField ] ;
2026-01-13 13:28:50 +09:00
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 허용)
let finalCompanyCode = userValue ;
if ( autoFilter ? . companyCodeOverride && userValue === "*" ) {
// 최고 관리자만 다른 회사 코드로 오버라이드 가능
finalCompanyCode = autoFilter . companyCodeOverride ;
logger . info ( "🔓 최고 관리자 회사 코드 오버라이드:" , {
originalCompanyCode : userValue ,
overrideCompanyCode : autoFilter.companyCodeOverride ,
tableName ,
} ) ;
}
if ( finalCompanyCode ) {
enhancedSearch [ filterColumn ] = finalCompanyCode ;
2025-11-04 14:33:39 +09:00
logger . info ( "🔍 현재 사용자 필터 적용:" , {
filterColumn ,
userField ,
2026-01-13 13:28:50 +09:00
userValue : finalCompanyCode ,
2025-11-04 14:33:39 +09:00
tableName ,
} ) ;
} else {
logger . warn ( "⚠️ 사용자 정보 필드 값 없음:" , {
userField ,
user : req.user ,
} ) ;
}
}
2025-09-03 15:23:12 +09:00
// 데이터 조회
2025-09-03 16:38:10 +09:00
const result = await tableManagementService . getTableData ( tableName , {
page : parseInt ( page ) ,
size : parseInt ( size ) ,
2025-11-04 14:33:39 +09:00
search : enhancedSearch , // 🆕 필터가 적용된 search 사용
2025-09-03 16:38:10 +09:00
sortBy ,
sortOrder ,
2025-11-13 17:06:41 +09:00
dataFilter , // 🆕 데이터 필터 전달
2025-09-03 16:38:10 +09:00
} ) ;
2025-09-03 15:23:12 +09:00
2025-09-03 16:38:10 +09:00
logger . info (
` 테이블 데이터 조회 완료: ${ tableName } , 총 ${ result . total } 건, 페이지 ${ result . page } / ${ result . totalPages } `
) ;
2025-09-03 15:23:12 +09:00
const response : ApiResponse < any > = {
success : true ,
message : "테이블 데이터를 성공적으로 조회했습니다." ,
data : result ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "테이블 데이터 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
success : false ,
message : "테이블 데이터 조회 중 오류가 발생했습니다." ,
error : {
code : "TABLE_DATA_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-09-03 16:38:10 +09:00
/ * *
* 테 이 블 데 이 터 추 가
* /
export async function addTableData (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
const data = req . body ;
logger . info ( ` === 테이블 데이터 추가 시작: ${ tableName } === ` ) ;
logger . info ( ` 추가할 데이터: ` , data ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
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 < null > = {
success : false ,
message : "추가할 데이터가 필요합니다." ,
error : {
code : "MISSING_DATA" ,
details : "요청 본문에 데이터가 없습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
const tableManagementService = new TableManagementService ( ) ;
2025-12-02 14:02:47 +09:00
// 🆕 멀티테넌시: company_code 자동 추가 (테이블에 company_code 컬럼이 있는 경우)
const companyCode = req . user ? . companyCode ;
if ( companyCode && ! data . company_code ) {
// 테이블에 company_code 컬럼이 있는지 확인
2026-01-15 17:36:38 +09:00
const hasCompanyCodeColumn = await tableManagementService . hasColumn ( tableName , "company_code" ) ;
2025-12-02 14:02:47 +09:00
if ( hasCompanyCodeColumn ) {
data . company_code = companyCode ;
2025-12-17 14:30:29 +09:00
logger . info ( ` 멀티테넌시: company_code 자동 추가 - ${ companyCode } ` ) ;
}
}
// 🆕 writer 컬럼 자동 추가 (테이블에 writer 컬럼이 있고 값이 없는 경우)
const userId = req . user ? . userId ;
if ( userId && ! data . writer ) {
2026-01-15 17:36:38 +09:00
const hasWriterColumn = await tableManagementService . hasColumn ( tableName , "writer" ) ;
2025-12-17 14:30:29 +09:00
if ( hasWriterColumn ) {
data . writer = userId ;
logger . info ( ` writer 자동 추가 - ${ userId } ` ) ;
2025-12-02 14:02:47 +09:00
}
}
2026-02-24 18:40:36 +09:00
// 회사별 NOT NULL 소프트 제약조건 검증
const notNullViolations = await tableManagementService . validateNotNullConstraints (
tableName ,
data ,
companyCode || "*"
) ;
if ( notNullViolations . length > 0 ) {
res . status ( 400 ) . json ( {
success : false ,
message : ` 필수 항목이 비어있습니다: ${ notNullViolations . join ( ", " ) } ` ,
error : {
code : "NOT_NULL_VIOLATION" ,
details : notNullViolations ,
} ,
} ) ;
return ;
}
2026-02-25 14:42:42 +09:00
// 회사별 UNIQUE 소프트 제약조건 검증
const uniqueViolations = await tableManagementService . validateUniqueConstraints (
tableName ,
data ,
companyCode || "*"
) ;
if ( uniqueViolations . length > 0 ) {
res . status ( 400 ) . json ( {
success : false ,
message : ` 중복된 값이 존재합니다: ${ uniqueViolations . join ( ", " ) } ` ,
error : {
code : "UNIQUE_VIOLATION" ,
details : uniqueViolations ,
} ,
} ) ;
return ;
}
2025-09-03 16:38:10 +09:00
// 데이터 추가
2026-02-25 13:59:51 +09:00
const result = await tableManagementService . addTableData ( tableName , data ) ;
2025-09-03 16:38:10 +09:00
2026-02-25 13:59:51 +09:00
logger . info ( ` 테이블 데이터 추가 완료: ${ tableName } , id: ${ result . insertedId } ` ) ;
2025-09-03 16:38:10 +09:00
2026-02-25 13:59:51 +09:00
const response : ApiResponse < { id : string | null } > = {
2025-09-03 16:38:10 +09:00
success : true ,
2026-01-15 17:36:38 +09:00
message : "테이블 데이터를 성공적으로 추가했습니다." ,
2026-02-25 13:59:51 +09:00
data : { id : result.insertedId } ,
2025-09-03 16:38:10 +09:00
} ;
res . status ( 201 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "테이블 데이터 추가 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName } = req . params ;
const { originalData , updatedData } = req . body ;
logger . info ( ` === 테이블 데이터 수정 시작: ${ tableName } === ` ) ;
logger . info ( ` 원본 데이터: ` , originalData ) ;
logger . info ( ` 수정할 데이터: ` , updatedData ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명이 필요합니다." ,
error : {
code : "INVALID_TABLE_NAME" ,
details : "테이블명이 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
if ( ! originalData || ! updatedData ) {
const response : ApiResponse < null > = {
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 < null > = {
success : false ,
message : "수정할 데이터가 없습니다." ,
error : {
code : "INVALID_DATA" ,
details : "수정할 데이터가 비어있습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
const tableManagementService = new TableManagementService ( ) ;
2026-02-24 18:40:36 +09:00
const companyCode = req . user ? . companyCode || "*" ;
// 회사별 NOT NULL 소프트 제약조건 검증 (수정 데이터 대상)
const notNullViolations = await tableManagementService . validateNotNullConstraints (
tableName ,
updatedData ,
companyCode
) ;
if ( notNullViolations . length > 0 ) {
res . status ( 400 ) . json ( {
success : false ,
message : ` 필수 항목이 비어있습니다: ${ notNullViolations . join ( ", " ) } ` ,
error : {
code : "NOT_NULL_VIOLATION" ,
details : notNullViolations ,
} ,
} ) ;
return ;
}
2025-09-03 16:38:10 +09:00
2026-02-25 14:42:42 +09:00
// 회사별 UNIQUE 소프트 제약조건 검증 (수정 시 자기 자신 제외)
const excludeId = originalData ? . id ? String ( originalData . id ) : undefined ;
const uniqueViolations = await tableManagementService . validateUniqueConstraints (
tableName ,
updatedData ,
companyCode ,
excludeId
) ;
if ( uniqueViolations . length > 0 ) {
res . status ( 400 ) . json ( {
success : false ,
message : ` 중복된 값이 존재합니다: ${ uniqueViolations . join ( ", " ) } ` ,
error : {
code : "UNIQUE_VIOLATION" ,
details : uniqueViolations ,
} ,
} ) ;
return ;
}
2025-09-03 16:38:10 +09:00
// 데이터 수정
await tableManagementService . editTableData (
tableName ,
originalData ,
updatedData
) ;
logger . info ( ` 테이블 데이터 수정 완료: ${ tableName } ` ) ;
const response : ApiResponse < null > = {
success : true ,
message : "테이블 데이터를 성공적으로 수정했습니다." ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "테이블 데이터 수정 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
success : false ,
message : "테이블 데이터 수정 중 오류가 발생했습니다." ,
error : {
code : "TABLE_EDIT_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-09-19 18:43:55 +09:00
/ * *
* 테 이 블 스 키 마 정 보 조 회 ( 컬 럼 존 재 여 부 검 증 용 )
* /
export async function getTableSchema (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
logger . info ( ` === 테이블 스키마 정보 조회 시작: ${ tableName } === ` ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
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 < ColumnTypeInfo [ ] > = {
success : true ,
message : "테이블 스키마 정보를 성공적으로 조회했습니다." ,
data : schema ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "테이블 스키마 정보 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName } = req . params ;
logger . info ( ` === 테이블 존재 여부 확인 시작: ${ tableName } === ` ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
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 < null > = {
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 < void > {
try {
const { tableName } = req . params ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req . user ? . companyCode ;
2025-11-07 14:27:07 +09:00
2025-11-06 17:01:13 +09:00
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 ;
2025-11-07 14:27:07 +09:00
logger . info (
` DB에서 회사 코드 조회 (조회): ${ req . user . userId } → ${ companyCode } `
) ;
2025-11-06 17:01:13 +09:00
}
logger . info (
` === 컬럼 웹타입 정보 조회 시작: ${ tableName } , company: ${ companyCode } === `
) ;
2025-09-19 18:43:55 +09:00
if ( ! tableName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명이 필요합니다." ,
error : {
code : "MISSING_TABLE_NAME" ,
details : "테이블명 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-11-06 17:01:13 +09:00
if ( ! companyCode ) {
logger . error ( ` 회사 코드 누락 (조회): ${ tableName } ` , {
user : req.user ,
hasUser : ! ! req . user ,
userId : req.user?.userId ,
companyCodeFromJWT : req.user?.companyCode ,
} ) ;
const response : ApiResponse < null > = {
success : false ,
message : "회사 코드를 찾을 수 없습니다." ,
error : {
code : "MISSING_COMPANY_CODE" ,
2025-11-07 14:27:07 +09:00
details :
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요." ,
2025-11-06 17:01:13 +09:00
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2025-09-19 18:43:55 +09:00
const tableManagementService = new TableManagementService ( ) ;
2025-11-06 17:01:13 +09:00
const inputTypes = await tableManagementService . getColumnInputTypes (
tableName ,
companyCode
) ;
2025-09-19 18:43:55 +09:00
logger . info (
2025-11-06 17:01:13 +09:00
` 컬럼 입력타입 정보 조회 완료: ${ tableName } , company: ${ companyCode } , ${ inputTypes . length } 개 컬럼 `
2025-09-19 18:43:55 +09:00
) ;
const response : ApiResponse < ColumnTypeInfo [ ] > = {
success : true ,
2025-09-23 10:40:21 +09:00
message : "컬럼 입력타입 정보를 성공적으로 조회했습니다." ,
data : inputTypes ,
2025-09-19 18:43:55 +09:00
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "컬럼 웹타입 정보 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
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 < null > = {
success : false ,
message : "데이터베이스 연결 상태 확인 중 오류가 발생했습니다." ,
error : {
code : "DATABASE_CONNECTION_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-09-03 16:38:10 +09:00
/ * *
* 테 이 블 데 이 터 삭 제
* /
export async function deleteTableData (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
const data = req . body ;
logger . info ( ` === 테이블 데이터 삭제 시작: ${ tableName } === ` ) ;
logger . info ( ` 삭제할 데이터: ` , data ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
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 < null > = {
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 < null > = {
success : false ,
message : "테이블 데이터 삭제 중 오류가 발생했습니다." ,
error : {
code : "TABLE_DELETE_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-09-23 10:40:21 +09:00
/ * *
* 컬 럼 웹 타 입 설 정 ( 레 거 시 지 원 )
* @deprecated updateColumnInputType 사 용 권 장
* /
export async function updateColumnWebType (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName , columnName } = req . params ;
const { webType , detailSettings , inputType } = req . body ;
logger . warn (
` 레거시 API 사용: updateColumnWebType → updateColumnInputType 사용 권장 `
) ;
2026-02-02 12:07:37 +09:00
// 🔥 inputType이 "direct" 또는 "auto"이면 무시하고 webType 사용
// "direct"/"auto"는 프론트엔드의 입력 방식(직접입력/자동입력) 구분값이지
// DB에 저장할 웹 타입(text, number, date 등)이 아님
let convertedInputType = webType || "text" ;
if ( inputType && inputType !== "direct" && inputType !== "auto" ) {
convertedInputType = inputType ;
}
logger . info (
` 웹타입 변환: webType= ${ webType } , inputType= ${ inputType } → ${ convertedInputType } `
) ;
2025-09-23 10:40:21 +09:00
// 새로운 메서드 호출
req . body = { inputType : convertedInputType , detailSettings } ;
await updateColumnInputType ( req , res ) ;
} catch ( error ) {
logger . error ( "레거시 컬럼 웹 타입 설정 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
success : false ,
message : "컬럼 웹 타입 설정 중 오류가 발생했습니다." ,
error : {
code : "WEB_TYPE_UPDATE_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-10-21 15:08:41 +09:00
// ========================================
// 🎯 테이블 로그 시스템 API
// ========================================
/ * *
* 로 그 테 이 블 생 성
* /
export async function createLogTable (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
const { pkColumn } = req . body ;
const userId = req . user ? . userId ;
logger . info ( ` === 로그 테이블 생성 시작: ${ tableName } === ` ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명이 필요합니다." ,
error : {
code : "MISSING_TABLE_NAME" ,
details : "테이블명 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
if ( ! pkColumn || ! pkColumn . columnName || ! pkColumn . dataType ) {
const response : ApiResponse < null > = {
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 < null > = {
success : true ,
message : "로그 테이블이 성공적으로 생성되었습니다." ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "로그 테이블 생성 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName } = req . params ;
logger . info ( ` === 로그 설정 조회: ${ tableName } === ` ) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
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 < typeof logConfig > = {
success : true ,
message : "로그 설정을 조회했습니다." ,
data : logConfig ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "로그 설정 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
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 < null > = {
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 ,
} ) ;
2025-11-04 14:33:39 +09:00
logger . info ( ` 로그 데이터 조회 완료: ${ tableName } _log, ${ result . total } 건 ` ) ;
2025-10-21 15:08:41 +09:00
const response : ApiResponse < typeof result > = {
success : true ,
message : "로그 데이터를 조회했습니다." ,
data : result ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "로그 데이터 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
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 < void > {
try {
const { tableName } = req . params ;
const { isActive } = req . body ;
2025-11-04 14:33:39 +09:00
logger . info (
` === 로그 테이블 토글: ${ tableName } , isActive: ${ isActive } === `
) ;
2025-10-21 15:08:41 +09:00
if ( ! tableName ) {
const response : ApiResponse < null > = {
success : false ,
message : "테이블명이 필요합니다." ,
error : {
code : "MISSING_TABLE_NAME" ,
details : "테이블명 파라미터가 누락되었습니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
if ( isActive === undefined || isActive === null ) {
const response : ApiResponse < null > = {
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
) ;
2025-11-04 14:33:39 +09:00
logger . info ( ` 로그 테이블 토글 완료: ${ tableName } , isActive: ${ isActive } ` ) ;
2025-10-21 15:08:41 +09:00
const response : ApiResponse < null > = {
success : true ,
message : ` 로그 기능이 ${ isActive ? "활성화" : "비활성화" } 되었습니다. ` ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "로그 테이블 토글 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
success : false ,
message : "로그 테이블 토글 중 오류가 발생했습니다." ,
error : {
code : "LOG_TOGGLE_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2025-11-11 14:44:22 +09:00
2026-01-26 16:32:20 +09:00
/ * *
* 회 사 별 카 테 고 리 컬 럼 조 회 ( 메 뉴 종 속 없 음 )
*
* @route GET / api / table - management / category - columns
* @description table_type_columns에서 회 사 코 드 기 준 으 로 input_type = 'category' 인 컬 럼 을 조 회
* /
export async function getCategoryColumnsByCompany (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const companyCode = req . user ? . companyCode ;
logger . info ( "📥 회사별 카테고리 컬럼 조회 요청" , { companyCode } ) ;
if ( ! companyCode ) {
logger . error ( "❌ 회사 코드가 없습니다" , { user : req.user } ) ;
res . status ( 400 ) . json ( {
success : false ,
message : "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요." ,
} ) ;
return ;
}
const { getPool } = await import ( "../database/db" ) ;
const pool = getPool ( ) ;
let columnsResult ;
// 최고 관리자인 경우 company_code = '*'인 카테고리 컬럼 조회
if ( companyCode === "*" ) {
const columnsQuery = `
SELECT DISTINCT
ttc . table_name AS "tableName" ,
COALESCE (
tl . table_label ,
initcap ( replace ( ttc . table_name , '_' , ' ' ) )
) AS "tableLabel" ,
ttc . column_name AS "columnName" ,
COALESCE (
2026-01-28 11:24:25 +09:00
ttc . column_label ,
2026-01-26 16:32:20 +09:00
initcap ( replace ( ttc . column_name , '_' , ' ' ) )
) AS "columnLabel" ,
ttc . input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN table_labels tl
ON ttc . table_name = tl . table_name
WHERE ttc . input_type = 'category'
AND ttc . company_code = '*'
ORDER BY ttc . table_name , ttc . column_name
` ;
columnsResult = await pool . query ( columnsQuery ) ;
logger . info ( "✅ 최고 관리자: 전체 카테고리 컬럼 조회 완료" , {
rowCount : columnsResult.rows.length
} ) ;
} else {
// 일반 회사: 해당 회사의 카테고리 컬럼만 조회
const columnsQuery = `
SELECT DISTINCT
ttc . table_name AS "tableName" ,
COALESCE (
tl . table_label ,
initcap ( replace ( ttc . table_name , '_' , ' ' ) )
) AS "tableLabel" ,
ttc . column_name AS "columnName" ,
COALESCE (
2026-01-28 11:24:25 +09:00
ttc . column_label ,
2026-01-26 16:32:20 +09:00
initcap ( replace ( ttc . column_name , '_' , ' ' ) )
) AS "columnLabel" ,
ttc . input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN table_labels tl
ON ttc . table_name = tl . table_name
WHERE ttc . input_type = 'category'
AND ttc . company_code = $1
ORDER BY ttc . table_name , ttc . column_name
` ;
columnsResult = await pool . query ( columnsQuery , [ companyCode ] ) ;
logger . info ( "✅ 회사별 카테고리 컬럼 조회 완료" , {
companyCode ,
rowCount : columnsResult.rows.length
} ) ;
}
res . json ( {
success : true ,
data : columnsResult.rows ,
message : "카테고리 컬럼 조회 성공" ,
} ) ;
} catch ( error : any ) {
logger . error ( "❌ 회사별 카테고리 컬럼 조회 실패" , { error : error.message } ) ;
res . status ( 500 ) . json ( {
success : false ,
message : "카테고리 컬럼 조회 중 오류가 발생했습니다." ,
error : error.message ,
} ) ;
}
}
2025-11-11 14:44:22 +09:00
/ * *
2025-11-18 16:12:47 +09:00
* 메 뉴 의 상 위 메 뉴 들 이 설 정 한 모 든 카 테 고 리 타 입 컬 럼 조 회 ( 계 층 구 조 상 속 )
2026-01-15 17:36:38 +09:00
*
2025-11-11 14:44:22 +09:00
* @route GET / api / table - management / menu / : menuObjid / category - columns
2025-11-18 16:12:47 +09:00
* @description 현 재 메 뉴 와 상 위 메 뉴 들 에 서 설 정 한 category_column_mapping의 모 든 카 테 고 리 컬 럼 조 회
2026-01-15 17:36:38 +09:00
*
2025-11-18 16:12:47 +09:00
* 예 시 :
* - 2 레 벨 메 뉴 "고객사관리" 에 서 discount_type , rounding_type 설 정
* - 3 레 벨 메 뉴 "고객등록" , "고객조회" 등 에 서 도 동 일 하 게 보 임 ( 상 속 )
2025-11-11 14:44:22 +09:00
* /
export async function getCategoryColumnsByMenu (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { menuObjid } = req . params ;
const companyCode = req . user ? . companyCode ;
2026-01-15 17:36:38 +09:00
logger . info ( "📥 메뉴별 카테고리 컬럼 조회 요청" , { menuObjid , companyCode } ) ;
2025-11-11 14:44:22 +09:00
if ( ! menuObjid ) {
2025-11-12 15:18:32 +09:00
res . status ( 400 ) . json ( {
2025-11-11 14:44:22 +09:00
success : false ,
message : "메뉴 OBJID가 필요합니다." ,
} ) ;
2025-11-12 15:18:32 +09:00
return ;
2025-11-11 14:44:22 +09:00
}
2026-01-26 16:32:20 +09:00
if ( ! companyCode ) {
logger . error ( "❌ 회사 코드가 없습니다" , { menuObjid , user : req.user } ) ;
res . status ( 400 ) . json ( {
success : false ,
message : "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요." ,
} ) ;
return ;
}
2025-11-11 14:44:22 +09:00
const { getPool } = await import ( "../database/db" ) ;
const pool = getPool ( ) ;
2026-01-26 16:32:20 +09:00
// 🆕 table_type_columns에서 직접 input_type = 'category'인 컬럼들을 조회
// category_column_mapping 대신 table_type_columns 기준으로 조회
logger . info ( "🔍 table_type_columns 기반 카테고리 컬럼 조회" , { menuObjid , companyCode } ) ;
2025-11-13 14:41:24 +09:00
let columnsResult ;
2026-01-26 16:32:20 +09:00
// 최고 관리자인 경우 모든 회사의 카테고리 컬럼 조회
if ( companyCode === "*" ) {
2025-11-13 14:41:24 +09:00
const columnsQuery = `
SELECT DISTINCT
ttc . table_name AS "tableName" ,
COALESCE (
tl . table_label ,
initcap ( replace ( ttc . table_name , '_' , ' ' ) )
) AS "tableLabel" ,
2026-01-26 16:32:20 +09:00
ttc . column_name AS "columnName" ,
2025-11-13 14:41:24 +09:00
COALESCE (
2026-01-28 11:24:25 +09:00
ttc . column_label ,
2026-01-26 16:32:20 +09:00
initcap ( replace ( ttc . column_name , '_' , ' ' ) )
2025-11-13 14:41:24 +09:00
) AS "columnLabel" ,
2026-01-26 16:32:20 +09:00
ttc . input_type AS "inputType"
FROM table_type_columns ttc
2025-11-13 14:41:24 +09:00
LEFT JOIN table_labels tl
ON ttc . table_name = tl . table_name
2026-01-26 16:32:20 +09:00
WHERE ttc . input_type = 'category'
AND ttc . company_code = '*'
ORDER BY ttc . table_name , ttc . column_name
2025-11-13 14:41:24 +09:00
` ;
2026-01-15 17:36:38 +09:00
2026-01-26 16:32:20 +09:00
columnsResult = await pool . query ( columnsQuery ) ;
logger . info ( "✅ 최고 관리자: 전체 카테고리 컬럼 조회 완료" , {
rowCount : columnsResult.rows.length
2026-01-15 17:36:38 +09:00
} ) ;
2025-11-13 14:41:24 +09:00
} else {
2026-01-26 16:32:20 +09:00
// 일반 회사: 해당 회사의 카테고리 컬럼만 조회
2025-11-13 14:41:24 +09:00
const columnsQuery = `
2026-01-26 16:32:20 +09:00
SELECT DISTINCT
2025-11-13 14:41:24 +09:00
ttc . table_name AS "tableName" ,
COALESCE (
tl . table_label ,
initcap ( replace ( ttc . table_name , '_' , ' ' ) )
) AS "tableLabel" ,
ttc . column_name AS "columnName" ,
COALESCE (
2026-01-28 11:24:25 +09:00
ttc . column_label ,
2025-11-13 14:41:24 +09:00
initcap ( replace ( ttc . column_name , '_' , ' ' ) )
) AS "columnLabel" ,
ttc . input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN table_labels tl
ON ttc . table_name = tl . table_name
2026-01-26 16:32:20 +09:00
WHERE ttc . input_type = 'category'
AND ttc . company_code = $1
2025-11-13 14:41:24 +09:00
ORDER BY ttc . table_name , ttc . column_name
` ;
2026-01-15 17:36:38 +09:00
2026-01-26 16:32:20 +09:00
columnsResult = await pool . query ( columnsQuery , [ companyCode ] ) ;
logger . info ( "✅ 회사별 카테고리 컬럼 조회 완료" , {
companyCode ,
rowCount : columnsResult.rows.length
} ) ;
2025-11-13 14:41:24 +09:00
}
2026-01-15 17:36:38 +09:00
logger . info ( "✅ 카테고리 컬럼 조회 완료" , {
columnCount : columnsResult.rows.length
2025-11-11 14:44:22 +09:00
} ) ;
res . json ( {
success : true ,
data : columnsResult.rows ,
message : "카테고리 컬럼 조회 성공" ,
} ) ;
} catch ( error : any ) {
2025-11-11 14:48:42 +09:00
logger . error ( "❌ 메뉴별 카테고리 컬럼 조회 실패" ) ;
logger . error ( "에러 메시지:" , error . message ) ;
logger . error ( "에러 스택:" , error . stack ) ;
logger . error ( "에러 전체:" , error ) ;
2025-11-11 14:44:22 +09:00
res . status ( 500 ) . json ( {
success : false ,
message : "카테고리 컬럼 조회에 실패했습니다." ,
error : error.message ,
2025-11-11 14:48:42 +09:00
stack : error.stack , // 디버깅용
2025-11-11 14:44:22 +09:00
} ) ;
}
}
2025-12-08 17:54:11 +09:00
/ * *
* 범 용 다 중 테 이 블 저 장 API
2026-01-15 17:36:38 +09:00
*
2025-12-08 17:54:11 +09:00
* 메 인 테 이 블 과 서 브 테 이 블 ( 들 ) 에 트 랜 잭 션 으 로 데 이 터 를 저 장 합 니 다 .
2026-01-15 17:36:38 +09:00
*
2025-12-08 17:54:11 +09:00
* 요 청 본 문 :
* {
* mainTable : { tableName : string , primaryKeyColumn : string } ,
* mainData : Record < string , any > ,
* subTables : Array < {
* tableName : string ,
* linkColumn : { mainField : string , subColumn : string } ,
* items : Record < string , any > [ ] ,
* 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 < void > {
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 ;
2026-01-15 17:36:38 +09:00
2025-12-08 17:54:11 +09:00
if ( isUpdate && pkValue ) {
// UPDATE
const updateColumns = Object . keys ( mainData )
2026-01-15 17:36:38 +09:00
. filter ( col = > col !== pkColumn )
2025-12-08 17:54:11 +09:00
. map ( ( col , idx ) = > ` " ${ col } " = $ ${ idx + 1 } ` )
. join ( ", " ) ;
const updateValues = Object . keys ( mainData )
2026-01-15 17:36:38 +09:00
. filter ( col = > col !== pkColumn )
. map ( col = > mainData [ col ] ) ;
2025-12-08 17:54:11 +09:00
// updated_at 컬럼 존재 여부 확인
2026-01-15 17:36:38 +09:00
const hasUpdatedAt = await client . query ( `
2025-12-08 17:54:11 +09:00
SELECT 1 FROM information_schema . columns
WHERE table_name = $1 AND column_name = 'updated_at'
2026-01-15 17:36:38 +09:00
` , [mainTableName]);
const updatedAtClause = hasUpdatedAt . rowCount && hasUpdatedAt . rowCount > 0 ? ", updated_at = NOW()" : "" ;
2025-12-08 17:54:11 +09:00
const updateQuery = `
UPDATE "${mainTableName}"
SET $ { updateColumns } $ { updatedAtClause }
WHERE "${pkColumn}" = $ $ { updateValues . length + 1 }
$ { companyCode !== "*" ? ` AND company_code = $ ${ updateValues . length + 2 } ` : "" }
RETURNING *
` ;
2026-01-15 17:36:38 +09:00
const updateParams = companyCode !== "*"
? [ . . . updateValues , pkValue , companyCode ]
: [ . . . updateValues , pkValue ] ;
logger . info ( "메인 테이블 UPDATE:" , { query : updateQuery , paramsCount : updateParams.length } ) ;
2025-12-08 17:54:11 +09:00
mainResult = await client . query ( updateQuery , updateParams ) ;
} else {
// INSERT
2026-01-15 17:36:38 +09:00
const columns = Object . keys ( mainData ) . map ( col = > ` " ${ col } " ` ) . join ( ", " ) ;
const placeholders = Object . keys ( mainData ) . map ( ( _ , idx ) = > ` $ ${ idx + 1 } ` ) . join ( ", " ) ;
2025-12-08 17:54:11 +09:00
const values = Object . values ( mainData ) ;
// updated_at 컬럼 존재 여부 확인
2026-01-15 17:36:38 +09:00
const hasUpdatedAt = await client . query ( `
2025-12-08 17:54:11 +09:00
SELECT 1 FROM information_schema . columns
WHERE table_name = $1 AND column_name = 'updated_at'
2026-01-15 17:36:38 +09:00
` , [mainTableName]);
const updatedAtClause = hasUpdatedAt . rowCount && hasUpdatedAt . rowCount > 0 ? ", updated_at = NOW()" : "" ;
2025-12-08 17:54:11 +09:00
const updateSetClause = Object . keys ( mainData )
2026-01-15 17:36:38 +09:00
. filter ( col = > col !== pkColumn )
. map ( col = > ` " ${ col } " = EXCLUDED." ${ col } " ` )
2025-12-08 17:54:11 +09:00
. join ( ", " ) ;
const insertQuery = `
INSERT INTO "${mainTableName}" ( $ { columns } )
VALUES ( $ { placeholders } )
ON CONFLICT ( "${pkColumn}" ) DO UPDATE SET
$ { updateSetClause } $ { updatedAtClause }
RETURNING *
` ;
2026-01-15 17:36:38 +09:00
logger . info ( "메인 테이블 INSERT/UPSERT:" , { query : insertQuery , paramsCount : values.length } ) ;
2025-12-08 17:54:11 +09:00
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 ;
2025-12-28 19:32:13 +09:00
// saveMainAsFirst가 활성화된 경우, items가 비어있어도 메인 데이터를 서브 테이블에 저장해야 함
2026-01-15 17:36:38 +09:00
const hasSaveMainAsFirst = options ? . saveMainAsFirst &&
options ? . mainFieldMappings &&
options . mainFieldMappings . length > 0 ;
2025-12-28 19:32:13 +09:00
if ( ! tableName || ( ! items ? . length && ! hasSaveMainAsFirst ) ) {
2026-01-15 17:36:38 +09:00
logger . info ( ` 서브 테이블 ${ tableName } 스킵: 데이터 없음 (saveMainAsFirst: ${ hasSaveMainAsFirst } ) ` ) ;
2025-12-08 17:54:11 +09:00
continue ;
}
logger . info ( ` 서브 테이블 ${ tableName } 저장 시작: ` , {
2025-12-28 19:32:13 +09:00
itemsCount : items?.length || 0 ,
2025-12-08 17:54:11 +09:00
linkColumn ,
options ,
2025-12-28 19:32:13 +09:00
hasSaveMainAsFirst ,
2025-12-08 17:54:11 +09:00
} ) ;
// 기존 데이터 삭제 옵션
if ( options ? . deleteExistingBefore && linkColumn ? . subColumn ) {
2026-01-15 17:36:38 +09:00
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 } ) ;
2025-12-08 17:54:11 +09:00
await client . query ( deleteQuery , deleteParams ) ;
}
// 메인 데이터도 서브 테이블에 저장 (옵션)
2025-12-28 19:32:13 +09:00
// mainFieldMappings가 비어 있으면 건너뜀 (필수 컬럼 누락 방지)
logger . info ( ` saveMainAsFirst 옵션 확인: ` , {
saveMainAsFirst : options?.saveMainAsFirst ,
mainFieldMappings : options?.mainFieldMappings ,
mainFieldMappingsLength : options?.mainFieldMappings?.length ,
linkColumn ,
mainDataKeys : Object.keys ( mainData ) ,
} ) ;
2026-01-15 17:36:38 +09:00
if ( options ? . saveMainAsFirst && options ? . mainFieldMappings && options . mainFieldMappings . length > 0 && linkColumn ? . subColumn ) {
2025-12-08 17:54:11 +09:00
const mainSubItem : Record < string , any > = {
[ linkColumn . subColumn ] : savedPkValue ,
} ;
// 메인 필드 매핑 적용
for ( const mapping of options . mainFieldMappings ) {
if ( mapping . formField && mapping . targetColumn ) {
mainSubItem [ mapping . targetColumn ] = mainData [ mapping . formField ] ;
}
}
// 메인 마커 설정
if ( options . mainMarkerColumn ) {
2026-01-15 17:36:38 +09:00
mainSubItem [ options . mainMarkerColumn ] = options . mainMarkerValue ? ? true ;
2025-12-08 17:54:11 +09:00
}
// company_code 추가
if ( companyCode !== "*" ) {
mainSubItem . company_code = companyCode ;
}
2025-12-08 18:23:28 +09:00
// 먼저 기존 데이터 존재 여부 확인 (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
2025-12-08 17:54:11 +09:00
` ;
2025-12-08 18:23:28 +09:00
const checkParams : any [ ] = [ savedPkValue ] ;
if ( options . mainMarkerColumn ) {
checkParams . push ( options . mainMarkerValue ? ? true ) ;
}
if ( companyCode !== "*" ) {
checkParams . push ( companyCode ) ;
}
2026-01-15 17:36:38 +09:00
2025-12-08 18:23:28 +09:00
const existingResult = await client . query ( checkQuery , checkParams ) ;
2026-01-15 17:36:38 +09:00
2025-12-08 18:23:28 +09:00
if ( existingResult . rows . length > 0 ) {
// UPDATE
const updateColumns = Object . keys ( mainSubItem )
2026-01-15 17:36:38 +09:00
. filter ( col = > col !== linkColumn . subColumn && col !== options . mainMarkerColumn && col !== "company_code" )
2025-12-08 18:23:28 +09:00
. map ( ( col , idx ) = > ` " ${ col } " = $ ${ idx + 1 } ` )
. join ( ", " ) ;
2026-01-15 17:36:38 +09:00
2025-12-08 18:23:28 +09:00
const updateValues = Object . keys ( mainSubItem )
2026-01-15 17:36:38 +09:00
. filter ( col = > col !== linkColumn . subColumn && col !== options . mainMarkerColumn && col !== "company_code" )
. map ( col = > mainSubItem [ col ] ) ;
2025-12-08 18:23:28 +09:00
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 ) ;
2026-01-15 17:36:38 +09:00
subTableResults . push ( { tableName , type : "main" , data : updateResult.rows [ 0 ] } ) ;
2025-12-08 18:23:28 +09:00
} else {
2026-01-15 17:36:38 +09:00
subTableResults . push ( { tableName , type : "main" , data : existingResult.rows [ 0 ] } ) ;
2025-12-08 18:23:28 +09:00
}
} else {
// INSERT
2026-01-15 17:36:38 +09:00
const mainSubColumns = Object . keys ( mainSubItem ) . map ( col = > ` " ${ col } " ` ) . join ( ", " ) ;
const mainSubPlaceholders = Object . keys ( mainSubItem ) . map ( ( _ , idx ) = > ` $ ${ idx + 1 } ` ) . join ( ", " ) ;
2025-12-08 18:23:28 +09:00
const mainSubValues = Object . values ( mainSubItem ) ;
const insertQuery = `
2025-12-08 17:54:11 +09:00
INSERT INTO "${tableName}" ( $ { mainSubColumns } )
VALUES ( $ { mainSubPlaceholders } )
RETURNING *
` ;
2025-12-08 18:23:28 +09:00
const insertResult = await client . query ( insertQuery , mainSubValues ) ;
2026-01-15 17:36:38 +09:00
subTableResults . push ( { tableName , type : "main" , data : insertResult.rows [ 0 ] } ) ;
2025-12-08 17:54:11 +09:00
}
}
// 서브 아이템들 저장
for ( const item of items ) {
// 연결 컬럼 값 설정
if ( linkColumn ? . subColumn ) {
item [ linkColumn . subColumn ] = savedPkValue ;
}
// company_code 추가
if ( companyCode !== "*" && ! item . company_code ) {
item . company_code = companyCode ;
}
2026-01-15 17:36:38 +09:00
const subColumns = Object . keys ( item ) . map ( col = > ` " ${ col } " ` ) . join ( ", " ) ;
const subPlaceholders = Object . keys ( item ) . map ( ( _ , idx ) = > ` $ ${ idx + 1 } ` ) . join ( ", " ) ;
2025-12-08 17:54:11 +09:00
const subValues = Object . values ( item ) ;
const subInsertQuery = `
INSERT INTO "${tableName}" ( $ { subColumns } )
VALUES ( $ { subPlaceholders } )
RETURNING *
` ;
2026-01-15 17:36:38 +09:00
logger . info ( ` 서브 테이블 ${ tableName } 아이템 저장: ` , { subInsertQuery , subValuesCount : subValues.length } ) ;
2025-12-08 17:54:11 +09:00
const subResult = await client . query ( subInsertQuery , subValues ) ;
2026-01-15 17:36:38 +09:00
subTableResults . push ( { tableName , type : "sub" , data : subResult.rows [ 0 ] } ) ;
2025-12-08 17:54:11 +09:00
}
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 ( ) ;
}
}
2025-12-10 17:13:39 +09:00
2026-01-08 15:56:06 +09:00
/ * *
2026-01-15 17:36:38 +09:00
* 두 테 이 블 간 엔 티 티 관 계 조 회
2026-01-28 11:24:25 +09:00
* table_type_columns의 entity / category 타 입 설 정 을 기 반 으 로 두 테 이 블 간 의 관 계 를 조 회
2026-01-08 15:56:06 +09:00
* /
export async function getTableEntityRelations (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { leftTable , rightTable } = req . query ;
if ( ! leftTable || ! rightTable ) {
2026-01-15 17:36:38 +09:00
res . status ( 400 ) . json ( {
2026-01-08 15:56:06 +09:00
success : false ,
message : "leftTable과 rightTable 파라미터가 필요합니다." ,
2026-01-15 17:36:38 +09:00
} ) ;
2026-01-08 15:56:06 +09:00
return ;
}
2026-01-15 17:36:38 +09:00
logger . info ( "=== 테이블 엔티티 관계 조회 ===" , { leftTable , rightTable } ) ;
// 두 테이블의 컬럼 라벨 정보 조회
const columnLabelsQuery = `
SELECT
table_name ,
column_name ,
column_label ,
2026-01-28 11:24:25 +09:00
input_type as web_type ,
2026-01-15 17:36:38 +09:00
detail_settings
2026-01-28 11:24:25 +09:00
FROM table_type_columns
2026-01-15 17:36:38 +09:00
WHERE table_name IN ( $1 , $2 )
2026-01-28 11:24:25 +09:00
AND input_type IN ( 'entity' , 'category' )
AND company_code = '*'
2026-01-15 17:36:38 +09:00
` ;
const result = await query ( columnLabelsQuery , [ leftTable , rightTable ] ) ;
// 관계 분석
const relations : Array < {
fromTable : string ;
fromColumn : string ;
toTable : string ;
toColumn : string ;
relationType : string ;
} > = [ ] ;
for ( const row of result ) {
try {
const detailSettings = typeof row . detail_settings === "string"
? JSON . parse ( row . detail_settings )
: row . detail_settings ;
if ( detailSettings && detailSettings . referenceTable ) {
const refTable = detailSettings . referenceTable ;
const refColumn = detailSettings . referenceColumn || "id" ;
// leftTable과 rightTable 간의 관계인지 확인
if (
( row . table_name === leftTable && refTable === rightTable ) ||
( row . table_name === rightTable && refTable === leftTable )
) {
relations . push ( {
fromTable : row.table_name ,
fromColumn : row.column_name ,
toTable : refTable ,
toColumn : refColumn ,
relationType : row.web_type ,
} ) ;
}
}
} catch ( parseError ) {
logger . warn ( "detail_settings 파싱 오류:" , {
table : row.table_name ,
column : row.column_name ,
error : parseError
} ) ;
}
}
2026-01-08 15:56:06 +09:00
2026-01-15 17:36:38 +09:00
logger . info ( "테이블 엔티티 관계 조회 완료" , {
leftTable ,
rightTable ,
relationsCount : relations.length
} ) ;
2026-01-08 15:56:06 +09:00
2026-01-15 17:36:38 +09:00
res . json ( {
2026-01-08 15:56:06 +09:00
success : true ,
data : {
2026-01-15 17:36:38 +09:00
leftTable ,
rightTable ,
2026-01-08 15:56:06 +09:00
relations ,
} ,
2026-01-15 17:36:38 +09:00
} ) ;
} catch ( error : any ) {
logger . error ( "테이블 엔티티 관계 조회 실패:" , error ) ;
res . status ( 500 ) . json ( {
2026-01-08 15:56:06 +09:00
success : false ,
2026-01-15 17:36:38 +09:00
message : "테이블 엔티티 관계 조회에 실패했습니다." ,
error : error.message ,
} ) ;
2026-01-08 15:56:06 +09:00
}
}
2026-01-15 15:17:52 +09:00
/ * *
* 현 재 테 이 블 을 참 조 ( FK로 연 결 ) 하 는 테 이 블 목 록 조 회
* GET / api / table - management / columns / : tableName / referenced - by
*
2026-01-28 11:24:25 +09:00
* table_type_columns에서 reference_table이 현 재 테 이 블 인 레 코 드 를 찾 아 서
2026-01-15 15:17:52 +09:00
* 해 당 테 이 블 과 FK 컬 럼 정 보 를 반 환 합 니 다 .
2026-02-05 10:58:27 +09:00
*
* 우선순위 : 현재 사 용 자 의 company_code > 공 통 ( '*' )
2026-01-15 15:17:52 +09:00
* /
export async function getReferencedByTables (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
2026-02-05 10:58:27 +09:00
// 현재 사용자의 회사 코드 (없으면 '*' 사용)
const userCompanyCode = req . user ? . companyCode || "*" ;
2026-01-15 15:17:52 +09:00
logger . info (
2026-02-05 10:58:27 +09:00
` === 테이블 참조 관계 조회 시작: ${ tableName } 을 참조하는 테이블 (회사코드: ${ userCompanyCode } ) === `
2026-01-15 15:17:52 +09:00
) ;
if ( ! tableName ) {
const response : ApiResponse < null > = {
success : false ,
message : "tableName 파라미터가 필요합니다." ,
error : {
code : "MISSING_PARAMETERS" ,
details : "tableName 경로 파라미터가 필요합니다." ,
} ,
} ;
res . status ( 400 ) . json ( response ) ;
return ;
}
2026-01-28 11:24:25 +09:00
// table_type_columns에서 reference_table이 현재 테이블인 레코드 조회
2026-01-15 15:17:52 +09:00
// input_type이 'entity'인 것만 조회 (실제 FK 관계)
2026-02-05 10:58:27 +09:00
// 우선순위: 현재 사용자의 company_code > 공통('*')
// ROW_NUMBER를 사용해서 같은 테이블/컬럼 조합에서 회사코드 우선순위로 하나만 선택
2026-01-15 15:17:52 +09:00
const sqlQuery = `
2026-02-05 10:58:27 +09:00
WITH ranked AS (
SELECT
ttc . table_name ,
ttc . column_name ,
ttc . column_label ,
ttc . reference_table ,
ttc . reference_column ,
ttc . display_column ,
ttc . company_code ,
ROW_NUMBER ( ) OVER (
PARTITION BY ttc . table_name , ttc . column_name
ORDER BY CASE WHEN ttc . company_code = $2 THEN 1 ELSE 2 END
) as rn
FROM table_type_columns ttc
WHERE ttc . reference_table = $1
AND ttc . input_type = 'entity'
AND ttc . company_code IN ( $2 , '*' )
)
2026-01-15 15:17:52 +09:00
SELECT DISTINCT
2026-02-05 10:58:27 +09:00
table_name ,
column_name ,
column_label ,
reference_table ,
reference_column ,
display_column ,
table_name as table_label
FROM ranked
WHERE rn = 1
ORDER BY table_name , column_name
2026-01-15 15:17:52 +09:00
` ;
2026-02-05 10:58:27 +09:00
const result = await query ( sqlQuery , [ tableName , userCompanyCode ] ) ;
2026-01-15 15:17:52 +09:00
const referencedByTables = result . map ( ( row : any ) = > ( {
tableName : row.table_name ,
tableLabel : row.table_label ,
columnName : row.column_name ,
columnLabel : row.column_label ,
referenceTable : row.reference_table ,
referenceColumn : row.reference_column || "id" ,
displayColumn : row.display_column ,
} ) ) ;
logger . info (
2026-02-05 10:58:27 +09:00
` 테이블 참조 관계 조회 완료: ${ referencedByTables . length } 개 발견 (회사코드: ${ userCompanyCode } ) `
2026-01-15 15:17:52 +09:00
) ;
const response : ApiResponse < any > = {
success : true ,
message : ` ${ referencedByTables . length } 개의 테이블이 ${ tableName } 을 참조합니다. ` ,
data : referencedByTables ,
} ;
res . status ( 200 ) . json ( response ) ;
} catch ( error ) {
logger . error ( "테이블 참조 관계 조회 중 오류 발생:" , error ) ;
const response : ApiResponse < null > = {
success : false ,
message : "테이블 참조 관계 조회 중 오류가 발생했습니다." ,
error : {
code : "REFERENCED_BY_ERROR" ,
details : error instanceof Error ? error . message : "Unknown error" ,
} ,
} ;
res . status ( 500 ) . json ( response ) ;
}
}
2026-02-11 16:07:44 +09:00
// ========================================
// PK / 인덱스 관리 API
// ========================================
/ * *
* PK / 인 덱 스 상 태 조 회
* GET / api / table - management / tables / : tableName / constraints
* /
export async function getTableConstraints (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
if ( ! tableName ) {
res . status ( 400 ) . json ( { success : false , message : "테이블명이 필요합니다." } ) ;
return ;
}
// PK 조회
const pkResult = await query < any > (
` SELECT tc.conname AS constraint_name,
array_agg ( a . attname ORDER BY x . n ) AS columns
FROM pg_constraint tc
JOIN pg_class c ON tc . conrelid = c . oid
JOIN pg_namespace ns ON c . relnamespace = ns . oid
CROSS JOIN LATERAL unnest ( tc . conkey ) WITH ORDINALITY AS x ( attnum , n )
JOIN pg_attribute a ON a . attrelid = tc . conrelid AND a . attnum = x . attnum
WHERE ns . nspname = 'public' AND c . relname = $1 AND tc . contype = 'p'
GROUP BY tc . conname ` ,
[ tableName ]
) ;
// array_agg 결과가 문자열로 올 수 있으므로 안전하게 배열로 변환
const parseColumns = ( cols : any ) : string [ ] = > {
if ( Array . isArray ( cols ) ) return cols ;
if ( typeof cols === "string" ) {
// PostgreSQL 배열 형식: {col1,col2}
return cols . replace ( /[{}]/g , "" ) . split ( "," ) . filter ( Boolean ) ;
}
return [ ] ;
} ;
const primaryKey = pkResult . length > 0
? { name : pkResult [ 0 ] . constraint_name , columns : parseColumns ( pkResult [ 0 ] . columns ) }
: { name : "" , columns : [ ] } ;
// 인덱스 조회 (PK 인덱스 제외)
const indexResult = await query < any > (
` SELECT i.relname AS index_name,
ix . indisunique AS is_unique ,
array_agg ( a . attname ORDER BY x . n ) AS columns
FROM pg_index ix
JOIN pg_class t ON ix . indrelid = t . oid
JOIN pg_class i ON ix . indexrelid = i . oid
JOIN pg_namespace ns ON t . relnamespace = ns . oid
CROSS JOIN LATERAL unnest ( ix . indkey ) WITH ORDINALITY AS x ( attnum , n )
JOIN pg_attribute a ON a . attrelid = t . oid AND a . attnum = x . attnum
WHERE ns . nspname = 'public' AND t . relname = $1
AND ix . indisprimary = false
GROUP BY i . relname , ix . indisunique
ORDER BY i . relname ` ,
[ tableName ]
) ;
const indexes = indexResult . map ( ( row : any ) = > ( {
name : row.index_name ,
columns : parseColumns ( row . columns ) ,
isUnique : row.is_unique ,
} ) ) ;
logger . info ( ` 제약조건 조회: ${ tableName } - PK: ${ primaryKey . columns . join ( "," ) } , 인덱스: ${ indexes . length } 개 ` ) ;
res . status ( 200 ) . json ( {
success : true ,
data : { primaryKey , indexes } ,
} ) ;
} catch ( error ) {
logger . error ( "제약조건 조회 오류:" , error ) ;
res . status ( 500 ) . json ( {
success : false ,
message : "제약조건 조회 중 오류가 발생했습니다." ,
error : error instanceof Error ? error . message : "Unknown error" ,
} ) ;
}
}
/ * *
* PK 설 정
* PUT / api / table - management / tables / : tableName / primary - key
* /
export async function setTablePrimaryKey (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
const { columns } = req . body ;
if ( ! tableName || ! columns || ! Array . isArray ( columns ) || columns . length === 0 ) {
res . status ( 400 ) . json ( { success : false , message : "테이블명과 PK 컬럼 배열이 필요합니다." } ) ;
return ;
}
logger . info ( ` PK 설정: ${ tableName } → [ ${ columns . join ( ", " ) } ] ` ) ;
// 기존 PK 제약조건 이름 조회
const existingPk = await query < any > (
` SELECT conname FROM pg_constraint tc
JOIN pg_class c ON tc . conrelid = c . oid
JOIN pg_namespace ns ON c . relnamespace = ns . oid
WHERE ns . nspname = 'public' AND c . relname = $1 AND tc . contype = 'p' ` ,
[ tableName ]
) ;
// 기존 PK 삭제
if ( existingPk . length > 0 ) {
const dropSql = ` ALTER TABLE "public"." ${ tableName } " DROP CONSTRAINT " ${ existingPk [ 0 ] . conname } " ` ;
logger . info ( ` 기존 PK 삭제: ${ dropSql } ` ) ;
await query ( dropSql ) ;
}
// 새 PK 추가
const colList = columns . map ( ( c : string ) = > ` " ${ c } " ` ) . join ( ", " ) ;
const addSql = ` ALTER TABLE "public"." ${ tableName } " ADD PRIMARY KEY ( ${ colList } ) ` ;
logger . info ( ` 새 PK 추가: ${ addSql } ` ) ;
await query ( addSql ) ;
res . status ( 200 ) . json ( {
success : true ,
message : ` PK가 설정되었습니다: ${ columns . join ( ", " ) } ` ,
} ) ;
} catch ( error ) {
logger . error ( "PK 설정 오류:" , error ) ;
res . status ( 500 ) . json ( {
success : false ,
message : "PK 설정 중 오류가 발생했습니다." ,
error : error instanceof Error ? error . message : "Unknown error" ,
} ) ;
}
}
/ * *
* 인 덱 스 토 글 ( 생 성 / 삭 제 )
* POST / api / table - management / tables / : tableName / indexes
* /
export async function toggleTableIndex (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName } = req . params ;
const { columnName , indexType , action } = req . body ;
if ( ! tableName || ! columnName || ! indexType || ! action ) {
res . status ( 400 ) . json ( {
success : false ,
message : "tableName, columnName, indexType(index|unique), action(create|drop)이 필요합니다." ,
} ) ;
return ;
}
const indexName = ` idx_ ${ tableName } _ ${ columnName } ${ indexType === "unique" ? "_uq" : "" } ` ;
logger . info ( ` 인덱스 ${ action } : ${ indexName } ( ${ indexType } ) ` ) ;
if ( action === "create" ) {
2026-02-25 14:42:42 +09:00
let indexColumns = ` " ${ columnName } " ` ;
// 유니크 인덱스: company_code 컬럼이 있으면 복합 유니크 (회사별 유니크 보장)
if ( indexType === "unique" ) {
const hasCompanyCode = await query (
` SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $ 1 AND column_name = 'company_code' ` ,
[ tableName ]
) ;
if ( hasCompanyCode . length > 0 ) {
indexColumns = ` "company_code", " ${ columnName } " ` ;
logger . info ( ` 멀티테넌시: company_code + ${ columnName } 복합 유니크 인덱스 생성 ` ) ;
}
}
2026-02-11 16:07:44 +09:00
const uniqueClause = indexType === "unique" ? "UNIQUE " : "" ;
2026-02-25 14:42:42 +09:00
const sql = ` CREATE ${ uniqueClause } INDEX IF NOT EXISTS " ${ indexName } " ON "public"." ${ tableName } " ( ${ indexColumns } ) ` ;
2026-02-11 16:07:44 +09:00
logger . info ( ` 인덱스 생성: ${ sql } ` ) ;
await query ( sql ) ;
} else if ( action === "drop" ) {
const sql = ` DROP INDEX IF EXISTS "public"." ${ indexName } " ` ;
logger . info ( ` 인덱스 삭제: ${ sql } ` ) ;
await query ( sql ) ;
} else {
res . status ( 400 ) . json ( { success : false , message : "action은 create 또는 drop이어야 합니다." } ) ;
return ;
}
res . status ( 200 ) . json ( {
success : true ,
message : action === "create"
? ` 인덱스가 생성되었습니다: ${ indexName } `
: ` 인덱스가 삭제되었습니다: ${ indexName } ` ,
} ) ;
} catch ( error : any ) {
logger . error ( "인덱스 토글 오류:" , error ) ;
2026-02-25 14:42:42 +09:00
const errMsg = error . message || "" ;
let userMessage = "인덱스 설정 중 오류가 발생했습니다." ;
let duplicates : any [ ] = [ ] ;
// 중복 데이터로 인한 UNIQUE 인덱스 생성 실패
if (
errMsg . includes ( "could not create unique index" ) ||
errMsg . includes ( "duplicate key" )
) {
const { columnName , tableName } = { . . . req . params , . . . req . body } ;
try {
duplicates = await query (
` SELECT company_code, " ${ columnName } ", COUNT(*) as cnt FROM " ${ tableName } " GROUP BY company_code, " ${ columnName } " HAVING COUNT(*) > 1 ORDER BY cnt DESC LIMIT 10 `
) ;
} catch {
try {
duplicates = await query (
` SELECT " ${ columnName } ", COUNT(*) as cnt FROM " ${ tableName } " GROUP BY " ${ columnName } " HAVING COUNT(*) > 1 ORDER BY cnt DESC LIMIT 10 `
) ;
} catch { /* 중복 조회 실패 시 무시 */ }
}
const dupDetails = duplicates . length > 0
? duplicates . map ( ( d : any ) = > {
const company = d . company_code ? ` [ ${ d . company_code } ] ` : "" ;
return ` ${ company } " ${ d [ columnName ] ? ? 'NULL' } " ( ${ d . cnt } 건) ` ;
} ) . join ( ", " )
: "" ;
userMessage = dupDetails
? ` [ ${ columnName } ] 컬럼에 같은 회사 내 중복 데이터가 있어 유니크 인덱스를 생성할 수 없습니다. 중복 값: ${ dupDetails } `
: ` [ ${ columnName } ] 컬럼에 중복 데이터가 있어 유니크 인덱스를 생성할 수 없습니다. 중복 데이터를 먼저 정리해주세요. ` ;
}
2026-02-11 16:07:44 +09:00
res . status ( 500 ) . json ( {
success : false ,
2026-02-25 14:42:42 +09:00
message : userMessage ,
error : errMsg ,
duplicates ,
2026-02-11 16:07:44 +09:00
} ) ;
}
}
/ * *
2026-02-24 18:40:36 +09:00
* NOT NULL 토 글 ( 회 사 별 소 프 트 제 약 조 건 )
2026-02-11 16:07:44 +09:00
* PUT / api / table - management / tables / : tableName / columns / : columnName / nullable
2026-02-24 18:40:36 +09:00
*
* DB 레 벨 ALTER TABLE 대 신 table_type_columns . is_nullable을 회 사 별 로 관 리 한 다 .
* 멀 티 테 넌 시 환 경 에 서 회 사 A는 NOT NULL , 회 사 B는 NULL 허 용 이 가 능 하 다 .
2026-02-11 16:07:44 +09:00
* /
export async function toggleColumnNullable (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName , columnName } = req . params ;
const { nullable } = req . body ;
2026-02-24 18:40:36 +09:00
const companyCode = req . user ? . companyCode || "*" ;
2026-02-11 16:07:44 +09:00
if ( ! tableName || ! columnName || typeof nullable !== "boolean" ) {
res . status ( 400 ) . json ( {
success : false ,
message : "tableName, columnName, nullable(boolean)이 필요합니다." ,
} ) ;
return ;
}
2026-02-24 18:40:36 +09:00
// is_nullable 값: 'Y' = NULL 허용, 'N' = NOT NULL
const isNullableValue = nullable ? "Y" : "N" ;
if ( ! nullable ) {
// NOT NULL 설정 전 - 해당 회사의 기존 데이터에 NULL이 있는지 확인
const hasCompanyCode = await query < { column_name : string } > (
` SELECT column_name FROM information_schema.columns
WHERE table_name = $1 AND column_name = 'company_code' ` ,
[ tableName ]
) ;
if ( hasCompanyCode . length > 0 ) {
const nullCheckQuery = companyCode === "*"
? ` SELECT COUNT(*) as null_count FROM " ${ tableName } " WHERE " ${ columnName } " IS NULL `
: ` SELECT COUNT(*) as null_count FROM " ${ tableName } " WHERE " ${ columnName } " IS NULL AND company_code = $ 1 ` ;
const nullCheckParams = companyCode === "*" ? [ ] : [ companyCode ] ;
const nullCheckResult = await query < { null_count : string } > ( nullCheckQuery , nullCheckParams ) ;
const nullCount = parseInt ( nullCheckResult [ 0 ] ? . null_count || "0" , 10 ) ;
if ( nullCount > 0 ) {
logger . warn ( ` NOT NULL 설정 불가 - 해당 회사에 NULL 데이터 존재: ${ tableName } . ${ columnName } ` , {
companyCode ,
nullCount ,
} ) ;
res . status ( 400 ) . json ( {
success : false ,
message : ` 현재 회사 데이터에 NULL 값이 ${ nullCount } 건 존재합니다. NULL 데이터를 먼저 정리해주세요. ` ,
} ) ;
return ;
}
}
2026-02-11 16:07:44 +09:00
}
2026-02-24 18:40:36 +09:00
// table_type_columns에 회사별 is_nullable 설정 UPSERT
await query (
` INSERT INTO table_type_columns (table_name, column_name, is_nullable, company_code, created_date, updated_date)
VALUES ( $1 , $2 , $3 , $4 , NOW ( ) , NOW ( ) )
ON CONFLICT ( table_name , column_name , company_code )
DO UPDATE SET is_nullable = $3 , updated_date = NOW ( ) ` ,
[ tableName , columnName , isNullableValue , companyCode ]
) ;
logger . info ( ` NOT NULL 소프트 제약조건 변경: ${ tableName } . ${ columnName } → is_nullable= ${ isNullableValue } ` , {
companyCode ,
} ) ;
2026-02-11 16:07:44 +09:00
res . status ( 200 ) . json ( {
success : true ,
message : nullable
? ` ${ columnName } 컬럼의 NOT NULL 제약이 해제되었습니다. `
: ` ${ columnName } 컬럼이 NOT NULL로 설정되었습니다. ` ,
} ) ;
} catch ( error : any ) {
logger . error ( "NOT NULL 토글 오류:" , error ) ;
res . status ( 500 ) . json ( {
success : false ,
2026-02-24 18:40:36 +09:00
message : "NOT NULL 설정 중 오류가 발생했습니다." ,
2026-02-11 16:07:44 +09:00
error : error instanceof Error ? error . message : "Unknown error" ,
} ) ;
}
}
2026-02-25 14:42:42 +09:00
/ * *
* UNIQUE 토 글 ( 회 사 별 소 프 트 제 약 조 건 )
* PUT / api / table - management / tables / : tableName / columns / : columnName / unique
*
* DB 레 벨 인 덱 스 대 신 table_type_columns . is_unique를 회 사 별 로 관 리 한 다 .
* 저 장 시 앱 레 벨 에 서 중 복 검 증 을 수 행 한 다 .
* /
export async function toggleColumnUnique (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > {
try {
const { tableName , columnName } = req . params ;
const { unique } = req . body ;
const companyCode = req . user ? . companyCode || "*" ;
if ( ! tableName || ! columnName || typeof unique !== "boolean" ) {
res . status ( 400 ) . json ( {
success : false ,
message : "tableName, columnName, unique(boolean)이 필요합니다." ,
} ) ;
return ;
}
const isUniqueValue = unique ? "Y" : "N" ;
if ( unique ) {
// UNIQUE 설정 전 - 해당 회사의 기존 데이터에 중복이 있는지 확인
const hasCompanyCode = await query < { column_name : string } > (
` SELECT column_name FROM information_schema.columns
WHERE table_name = $1 AND column_name = 'company_code' ` ,
[ tableName ]
) ;
if ( hasCompanyCode . length > 0 ) {
const dupQuery = companyCode === "*"
? ` SELECT " ${ columnName } ", COUNT(*) as cnt FROM " ${ tableName } " WHERE " ${ columnName } " IS NOT NULL GROUP BY " ${ columnName } " HAVING COUNT(*) > 1 LIMIT 10 `
: ` SELECT " ${ columnName } ", COUNT(*) as cnt FROM " ${ tableName } " WHERE " ${ columnName } " IS NOT NULL AND company_code = $ 1 GROUP BY " ${ columnName } " HAVING COUNT(*) > 1 LIMIT 10 ` ;
const dupParams = companyCode === "*" ? [ ] : [ companyCode ] ;
const dupResult = await query < any > ( dupQuery , dupParams ) ;
if ( dupResult . length > 0 ) {
const dupDetails = dupResult
. map ( ( d : any ) = > ` " ${ d [ columnName ] } " ( ${ d . cnt } 건) ` )
. join ( ", " ) ;
res . status ( 400 ) . json ( {
success : false ,
message : ` 현재 회사 데이터에 중복 값이 존재합니다. 중복 데이터를 먼저 정리해주세요. 중복 값: ${ dupDetails } ` ,
} ) ;
return ;
}
}
}
// table_type_columns에 회사별 is_unique 설정 UPSERT
await query (
` INSERT INTO table_type_columns (table_name, column_name, is_unique, company_code, created_date, updated_date)
VALUES ( $1 , $2 , $3 , $4 , NOW ( ) , NOW ( ) )
ON CONFLICT ( table_name , column_name , company_code )
DO UPDATE SET is_unique = $3 , updated_date = NOW ( ) ` ,
[ tableName , columnName , isUniqueValue , companyCode ]
) ;
logger . info ( ` UNIQUE 소프트 제약조건 변경: ${ tableName } . ${ columnName } → is_unique= ${ isUniqueValue } ` , {
companyCode ,
} ) ;
res . status ( 200 ) . json ( {
success : true ,
message : unique
? ` ${ columnName } 컬럼이 UNIQUE로 설정되었습니다. `
: ` ${ columnName } 컬럼의 UNIQUE 제약이 해제되었습니다. ` ,
} ) ;
} catch ( error : any ) {
logger . error ( "UNIQUE 토글 오류:" , error ) ;
res . status ( 500 ) . json ( {
success : false ,
message : "UNIQUE 설정 중 오류가 발생했습니다." ,
error : error instanceof Error ? error . message : "Unknown error" ,
} ) ;
}
}