feat: Phase 3.16 데이터 관리 서비스 Raw Query 전환 완료

4개 서비스 18개 Prisma 호출 전환 완료:

1. **EnhancedDynamicFormService** (6개)
   - validateTableExists - information_schema 조회
   - getTableColumns - 테이블 컬럼 정보 조회 with 캐싱
   - getColumnWebTypes - 웹타입 정보 조회
   - getPrimaryKeys - Primary Key 조회
   - performInsert - 동적 INSERT with RETURNING
   - performUpdate - 동적 UPDATE with RETURNING

2. **DataMappingService** (5개)
   - getSourceData - 소스 테이블 데이터 조회
   - executeInsert - 동적 INSERT
   - executeUpsert - ON CONFLICT DO UPDATE
   - executeUpdate - 동적 UPDATE
   - disconnect - 제거 (Raw Query 불필요)

3. **DataService** (4개)
   - getTableData - 동적 SELECT with 동적 WHERE/ORDER BY
   - checkTableExists - information_schema 테이블 존재 확인
   - getTableColumnsSimple - 컬럼 정보 조회
   - getColumnLabel - 컬럼 라벨 조회

4. **AdminService** (3개)
   - getAdminMenuList - WITH RECURSIVE 쿼리
   - getUserMenuList - WITH RECURSIVE 쿼리
   - getMenuInfo - LEFT JOIN으로 회사 정보 포함

기술적 성과:
- 변수명 충돌 해결 (query vs sql)
- WITH RECURSIVE 쿼리 전환
- Prisma include → LEFT JOIN 전환
- 동적 쿼리 생성 (WHERE, ORDER BY)
- SQL 인젝션 방지 (컬럼명 검증)

진행률: Phase 3 173/186 (93.0%)
문서: PHASE3.16_DATA_MANAGEMENT_SERVICES_MIGRATION.md
This commit is contained in:
kjs
2025-10-01 12:27:32 +09:00
parent 1791cd9f3f
commit 3d8f70e181
6 changed files with 217 additions and 148 deletions

View File

@@ -1,7 +1,5 @@
import { logger } from "../utils/logger";
// 🔧 Prisma 클라이언트 중복 생성 방지 - 기존 인스턴스 재사용
import prisma = require("../config/database");
import { query, queryOne } from "../database/db";
export class AdminService {
/**
@@ -13,9 +11,9 @@ export class AdminService {
const { userLang = "ko" } = paramMap;
// 기존 Java의 selectAdminMenuList 쿼리를 Prisma로 포팅
// WITH RECURSIVE 쿼리를 Prisma의 $queryRaw로 구현
const menuList = await prisma.$queryRaw<any[]>`
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
// WITH RECURSIVE 쿼리 구현
const menuList = await query<any>(`
WITH RECURSIVE v_menu(
LEVEL,
MENU_TYPE,
@@ -62,14 +60,14 @@ export class AdminService {
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
WHERE MLKM.lang_key = MENU.LANG_KEY
AND (MLKM.company_code = MENU.COMPANY_CODE OR (MENU.COMPANY_CODE IS NULL AND MLKM.company_code = '*'))
AND MLT.lang_code = ${userLang}
AND MLT.lang_code = $1
LIMIT 1),
(SELECT MLT.lang_text
FROM MULTI_LANG_KEY_MASTER MLKM
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
WHERE MLKM.lang_key = MENU.LANG_KEY
AND MLKM.company_code = '*'
AND MLT.lang_code = ${userLang}
AND MLT.lang_code = $1
LIMIT 1),
MENU.MENU_NAME_KOR
),
@@ -80,14 +78,14 @@ export class AdminService {
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
WHERE MLKM.lang_key = MENU.LANG_KEY_DESC
AND (MLKM.company_code = MENU.COMPANY_CODE OR (MENU.COMPANY_CODE IS NULL AND MLKM.company_code = '*'))
AND MLT.lang_code = ${userLang}
AND MLT.lang_code = $1
LIMIT 1),
(SELECT MLT.lang_text
FROM MULTI_LANG_KEY_MASTER MLKM
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
WHERE MLKM.lang_key = MENU.LANG_KEY_DESC
AND MLKM.company_code = '*'
AND MLT.lang_code = ${userLang}
AND MLT.lang_code = $1
LIMIT 1),
MENU.MENU_DESC
)
@@ -125,14 +123,14 @@ export class AdminService {
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
WHERE MLKM.lang_key = MENU_SUB.LANG_KEY
AND (MLKM.company_code = MENU_SUB.COMPANY_CODE OR (MENU_SUB.COMPANY_CODE IS NULL AND MLKM.company_code = '*'))
AND MLT.lang_code = ${userLang}
AND MLT.lang_code = $1
LIMIT 1),
(SELECT MLT.lang_text
FROM MULTI_LANG_KEY_MASTER MLKM
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
WHERE MLKM.lang_key = MENU_SUB.LANG_KEY
AND MLKM.company_code = '*'
AND MLT.lang_code = ${userLang}
AND MLT.lang_code = $1
LIMIT 1),
MENU_SUB.MENU_NAME_KOR
),
@@ -143,14 +141,14 @@ export class AdminService {
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
WHERE MLKM.lang_key = MENU_SUB.LANG_KEY_DESC
AND (MLKM.company_code = MENU_SUB.COMPANY_CODE OR (MENU_SUB.COMPANY_CODE IS NULL AND MLKM.company_code = '*'))
AND MLT.lang_code = ${userLang}
AND MLT.lang_code = $1
LIMIT 1),
(SELECT MLT.lang_text
FROM MULTI_LANG_KEY_MASTER MLKM
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
WHERE MLKM.lang_key = MENU_SUB.LANG_KEY_DESC
AND MLKM.company_code = '*'
AND MLT.lang_code = ${userLang}
AND MLT.lang_code = $1
LIMIT 1),
MENU_SUB.MENU_DESC
)
@@ -190,7 +188,7 @@ export class AdminService {
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
WHERE 1 = 1
ORDER BY PATH, SEQ
`;
`, [userLang]);
logger.info(`관리자 메뉴 목록 조회 결과: ${menuList.length}`);
if (menuList.length > 0) {
@@ -213,8 +211,8 @@ export class AdminService {
const { userLang = "ko" } = paramMap;
// 기존 Java의 selectUserMenuList 쿼리를 Prisma로 포팅
const menuList = await prisma.$queryRaw<any[]>`
// 기존 Java의 selectUserMenuList 쿼리를 Raw Query로 포팅
const menuList = await query<any>(`
WITH RECURSIVE v_menu(
LEVEL,
MENU_TYPE,
@@ -310,12 +308,12 @@ export class AdminService {
FROM v_menu A
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
LEFT JOIN MULTI_LANG_KEY_MASTER MLKM_NAME ON A.LANG_KEY = MLKM_NAME.lang_key
LEFT JOIN MULTI_LANG_TEXT MLT_NAME ON MLKM_NAME.key_id = MLT_NAME.key_id AND MLT_NAME.lang_code = ${userLang}
LEFT JOIN MULTI_LANG_TEXT MLT_NAME ON MLKM_NAME.key_id = MLT_NAME.key_id AND MLT_NAME.lang_code = $1
LEFT JOIN MULTI_LANG_KEY_MASTER MLKM_DESC ON A.LANG_KEY_DESC = MLKM_DESC.lang_key
LEFT JOIN MULTI_LANG_TEXT MLT_DESC ON MLKM_DESC.key_id = MLT_DESC.key_id AND MLT_DESC.lang_code = ${userLang}
LEFT JOIN MULTI_LANG_TEXT MLT_DESC ON MLKM_DESC.key_id = MLT_DESC.key_id AND MLT_DESC.lang_code = $1
WHERE 1 = 1
ORDER BY PATH, SEQ
`;
`, [userLang]);
logger.info(`사용자 메뉴 목록 조회 결과: ${menuList.length}`);
if (menuList.length > 0) {
@@ -336,32 +334,31 @@ export class AdminService {
try {
logger.info(`AdminService.getMenuInfo 시작 - menuId: ${menuId}`);
// Prisma ORM을 사용한 메뉴 정보 조회 (회사 정보 포함)
const menuInfo = await prisma.menu_info.findUnique({
where: {
objid: Number(menuId),
},
include: {
company: {
select: {
company_name: true,
},
},
},
});
// Raw Query를 사용한 메뉴 정보 조회 (회사 정보 포함)
const menuResult = await query<any>(
`SELECT
m.*,
c.company_name
FROM menu_info m
LEFT JOIN company_mng c ON m.company_code = c.company_code
WHERE m.objid = $1::numeric`,
[menuId]
);
if (!menuInfo) {
if (!menuResult || menuResult.length === 0) {
return null;
}
const menuInfo = menuResult[0];
// 응답 형식 조정 (기존 형식과 호환성 유지)
const result = {
...menuInfo,
objid: menuInfo.objid.toString(), // BigInt를 문자열로 변환
objid: menuInfo.objid?.toString(),
menu_type: menuInfo.menu_type?.toString(),
parent_obj_id: menuInfo.parent_obj_id?.toString(),
seq: menuInfo.seq?.toString(),
company_name: menuInfo.company?.company_name || "미지정",
company_name: menuInfo.company_name || "미지정",
};
logger.info("메뉴 정보 조회 결과:", result);

View File

@@ -1,4 +1,4 @@
import { PrismaClient } from "@prisma/client";
import { query } from "../database/db";
import {
DataMappingConfig,
InboundMapping,
@@ -11,10 +11,8 @@ import {
} from "../types/dataMappingTypes";
export class DataMappingService {
private prisma: PrismaClient;
constructor() {
this.prisma = new PrismaClient();
// No prisma instance needed
}
/**
@@ -404,10 +402,10 @@ export class DataMappingService {
}
// Raw SQL을 사용한 동적 쿼리
const query = `SELECT * FROM ${tableName}${mapping.sourceFilter ? ` WHERE ${mapping.sourceFilter}` : ""}`;
console.log(`🔍 [DataMappingService] 쿼리 실행: ${query}`);
const sql = `SELECT * FROM ${tableName}${mapping.sourceFilter ? ` WHERE ${mapping.sourceFilter}` : ""}`;
console.log(`🔍 [DataMappingService] 쿼리 실행: ${sql}`);
const result = await this.prisma.$queryRawUnsafe(query);
const result = await query<any>(sql, []);
return result;
} catch (error) {
console.error(
@@ -429,14 +427,14 @@ export class DataMappingService {
const values = Object.values(data);
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
const query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
const sql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
console.log(`📝 [DataMappingService] INSERT 실행:`, {
table: tableName,
columns,
query,
query: sql,
});
await this.prisma.$executeRawUnsafe(query, ...values);
await query(sql, values);
}
/**
@@ -460,7 +458,7 @@ export class DataMappingService {
.map((col) => `${col} = EXCLUDED.${col}`)
.join(", ");
const query = `
const sql = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
ON CONFLICT (${keyFields.join(", ")})
@@ -470,9 +468,9 @@ export class DataMappingService {
console.log(`🔄 [DataMappingService] UPSERT 실행:`, {
table: tableName,
keyFields,
query,
query: sql,
});
await this.prisma.$executeRawUnsafe(query, ...values);
await query(sql, values);
}
/**
@@ -503,14 +501,14 @@ export class DataMappingService {
...keyFields.map((field) => data[field]),
];
const query = `UPDATE ${tableName} SET ${updateClauses} WHERE ${whereConditions}`;
const sql = `UPDATE ${tableName} SET ${updateClauses} WHERE ${whereConditions}`;
console.log(`✏️ [DataMappingService] UPDATE 실행:`, {
table: tableName,
keyFields,
query,
query: sql,
});
await this.prisma.$executeRawUnsafe(query, ...values);
await query(sql, values);
}
/**
@@ -570,6 +568,6 @@ export class DataMappingService {
* 리소스 정리
*/
async disconnect(): Promise<void> {
await this.prisma.$disconnect();
// No disconnect needed for raw queries
}
}

View File

@@ -1,5 +1,4 @@
import { PrismaClient } from "@prisma/client";
import prisma from "../config/database";
import { query, queryOne } from "../database/db";
interface GetTableDataParams {
tableName: string;
@@ -111,7 +110,7 @@ class DataService {
}
// 동적 SQL 쿼리 생성
let query = `SELECT * FROM "${tableName}"`;
let sql = `SELECT * FROM "${tableName}"`;
const queryParams: any[] = [];
let paramIndex = 1;
@@ -150,7 +149,7 @@ class DataService {
// WHERE 절 추가
if (whereConditions.length > 0) {
query += ` WHERE ${whereConditions.join(" AND ")}`;
sql += ` WHERE ${whereConditions.join(" AND ")}`;
}
// ORDER BY 절 추가
@@ -162,7 +161,7 @@ class DataService {
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {
const validDirection = direction === "DESC" ? "DESC" : "ASC";
query += ` ORDER BY "${columnName}" ${validDirection}`;
sql += ` ORDER BY "${columnName}" ${validDirection}`;
}
} else {
// 기본 정렬: 최신순 (가능한 컬럼 시도)
@@ -179,23 +178,23 @@ class DataService {
);
if (availableDateColumn) {
query += ` ORDER BY "${availableDateColumn}" DESC`;
sql += ` ORDER BY "${availableDateColumn}" DESC`;
}
}
// LIMIT과 OFFSET 추가
query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
queryParams.push(limit, offset);
console.log("🔍 실행할 쿼리:", query);
console.log("🔍 실행할 쿼리:", sql);
console.log("📊 쿼리 파라미터:", queryParams);
// 쿼리 실행
const result = await prisma.$queryRawUnsafe(query, ...queryParams);
const result = await query<any>(sql, queryParams);
return {
success: true,
data: result as any[],
data: result,
};
} catch (error) {
console.error(`데이터 조회 오류 (${tableName}):`, error);
@@ -259,18 +258,16 @@ class DataService {
*/
private async checkTableExists(tableName: string): Promise<boolean> {
try {
const result = await prisma.$queryRawUnsafe(
`
SELECT EXISTS (
const result = await query<{ exists: boolean }>(
`SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = $1
);
`,
tableName
)`,
[tableName]
);
return (result as any)[0]?.exists || false;
return result[0]?.exists || false;
} catch (error) {
console.error("테이블 존재 확인 오류:", error);
return false;
@@ -281,18 +278,16 @@ class DataService {
* 테이블 컬럼 정보 조회 (간단 버전)
*/
private async getTableColumnsSimple(tableName: string): Promise<any[]> {
const result = await prisma.$queryRawUnsafe(
`
SELECT column_name, data_type, is_nullable, column_default
const result = await query<any>(
`SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position;
`,
tableName
ORDER BY ordinal_position`,
[tableName]
);
return result as any[];
return result;
}
/**
@@ -304,19 +299,15 @@ class DataService {
): Promise<string | null> {
try {
// column_labels 테이블에서 라벨 조회
const result = await prisma.$queryRawUnsafe(
`
SELECT label_ko
const result = await query<{ label_ko: string }>(
`SELECT label_ko
FROM column_labels
WHERE table_name = $1 AND column_name = $2
LIMIT 1;
`,
tableName,
columnName
LIMIT 1`,
[tableName, columnName]
);
const labelResult = result as any[];
return labelResult[0]?.label_ko || null;
return result[0]?.label_ko || null;
} catch (error) {
// column_labels 테이블이 없거나 오류가 발생하면 null 반환
return null;

View File

@@ -3,7 +3,7 @@
* 타입 안전성과 검증 강화
*/
import { PrismaClient } from "@prisma/client";
import { query, queryOne } from "../database/db";
import {
WebType,
DynamicWebType,
@@ -14,8 +14,6 @@ import {
} from "../types/unified-web-types";
import { DataflowControlService } from "./dataflowControlService";
const prisma = new PrismaClient();
// 테이블 컬럼 정보
export interface TableColumn {
column_name: string;
@@ -156,17 +154,15 @@ export class EnhancedDynamicFormService {
*/
private async validateTableExists(tableName: string): Promise<boolean> {
try {
const result = await prisma.$queryRawUnsafe(
`
SELECT EXISTS (
const result = await query<{ exists: boolean }>(
`SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = $1
) as exists
`,
tableName
) as exists`,
[tableName]
);
return (result as any)[0]?.exists || false;
return result[0]?.exists || false;
} catch (error) {
console.error(`❌ 테이블 존재 여부 확인 실패: ${tableName}`, error);
return false;
@@ -184,9 +180,8 @@ export class EnhancedDynamicFormService {
}
try {
const columns = (await prisma.$queryRawUnsafe(
`
SELECT
const columns = await query<TableColumn>(
`SELECT
column_name,
data_type,
is_nullable,
@@ -196,10 +191,9 @@ export class EnhancedDynamicFormService {
numeric_scale
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position
`,
tableName
)) as TableColumn[];
ORDER BY ordinal_position`,
[tableName]
);
// 캐시 저장 (10분)
this.columnCache.set(tableName, columns);
@@ -226,18 +220,21 @@ export class EnhancedDynamicFormService {
try {
// table_type_columns에서 웹타입 정보 조회
const webTypeData = (await prisma.$queryRawUnsafe(
`
SELECT
const webTypeData = await query<{
column_name: string;
web_type: string;
is_nullable: string;
detail_settings: any;
}>(
`SELECT
column_name,
web_type,
is_nullable,
detail_settings
FROM table_type_columns
WHERE table_name = $1
`,
tableName
)) as any[];
WHERE table_name = $1`,
[tableName]
);
const columnWebTypes: ColumnWebTypeInfo[] = webTypeData.map((row) => ({
columnName: row.column_name,
@@ -555,15 +552,13 @@ export class EnhancedDynamicFormService {
*/
private async getPrimaryKeys(tableName: string): Promise<string[]> {
try {
const result = (await prisma.$queryRawUnsafe(
`
SELECT column_name
const result = await query<{ column_name: string }>(
`SELECT column_name
FROM information_schema.key_column_usage
WHERE table_name = $1
AND constraint_name LIKE '%_pkey'
`,
tableName
)) as any[];
AND constraint_name LIKE '%_pkey'`,
[tableName]
);
return result.map((row) => row.column_name);
} catch (error) {
@@ -594,10 +589,7 @@ export class EnhancedDynamicFormService {
query: insertQuery.replace(/\n\s+/g, " "),
});
const result = (await prisma.$queryRawUnsafe(
insertQuery,
...values
)) as any[];
const result = await query<any>(insertQuery, values);
return {
data: result[0],
@@ -649,10 +641,7 @@ export class EnhancedDynamicFormService {
query: updateQuery.replace(/\n\s+/g, " "),
});
const result = (await prisma.$queryRawUnsafe(
updateQuery,
...updateValues
)) as any[];
const result = await query<any>(updateQuery, updateValues);
return {
data: result[0],