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

@ -6,16 +6,71 @@
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------------------------------- |
| 대상 서비스 | 4개 (EnhancedDynamicForm, DataMapping, Data, Admin) |
| 파일 위치 | `backend-node/src/services/{enhanced,data,admin}*.ts` |
| 총 파일 크기 | 2,062 라인 |
| Prisma 호출 | 18개 |
| **현재 진행률** | **0/18 (0%)** 🔄 **진행 예정** |
| 복잡도 | 중간 (동적 쿼리, JSON 필드, 관리자 기능) |
| 우선순위 | 🟡 중간 (Phase 3.16) |
| **상태** | ⏳ **대기 중** |
| 항목 | 내용 |
| --------------- | ----------------------------------------------------- |
| 대상 서비스 | 4개 (EnhancedDynamicForm, DataMapping, Data, Admin) |
| 파일 위치 | `backend-node/src/services/{enhanced,data,admin}*.ts` |
| 총 파일 크기 | 2,062 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **18/18 (100%)****전환 완료** |
| 복잡도 | 중간 (동적 쿼리, JSON 필드, 관리자 기능) |
| 우선순위 | 🟡 중간 (Phase 3.16) |
| **상태** | ✅ **완료** |
---
## ✅ 전환 완료 내역
### 전환된 Prisma 호출 (18개)
#### 1. EnhancedDynamicFormService (6개)
- `validateTableExists()` - $queryRawUnsafe → query
- `getTableColumns()` - $queryRawUnsafe → query
- `getColumnWebTypes()` - $queryRawUnsafe → query
- `getPrimaryKeys()` - $queryRawUnsafe → query
- `performInsert()` - $queryRawUnsafe → query
- `performUpdate()` - $queryRawUnsafe → query
#### 2. DataMappingService (5개)
- `getSourceData()` - $queryRawUnsafe → query
- `executeInsert()` - $executeRawUnsafe → query
- `executeUpsert()` - $executeRawUnsafe → query
- `executeUpdate()` - $executeRawUnsafe → query
- `disconnect()` - 제거 (Raw Query는 disconnect 불필요)
#### 3. DataService (4개)
- `getTableData()` - $queryRawUnsafe → query
- `checkTableExists()` - $queryRawUnsafe → query
- `getTableColumnsSimple()` - $queryRawUnsafe → query
- `getColumnLabel()` - $queryRawUnsafe → query
#### 4. AdminService (3개)
- `getAdminMenuList()` - $queryRaw → query (WITH RECURSIVE)
- `getUserMenuList()` - $queryRaw → query (WITH RECURSIVE)
- `getMenuInfo()` - findUnique → query (JOIN)
### 주요 기술적 해결 사항
1. **변수명 충돌 해결**
- `dataService.ts`에서 `query` 변수 → `sql` 변수로 변경
- `query()` 함수와 로컬 변수 충돌 방지
2. **WITH RECURSIVE 쿼리 전환**
- Prisma의 `$queryRaw` 템플릿 리터럴 → 일반 문자열
- `${userLang}``$1` 파라미터 바인딩
3. **JOIN 쿼리 전환**
- Prisma의 `include` 옵션 → `LEFT JOIN` 쿼리
- 관계 데이터를 단일 쿼리로 조회
4. **동적 쿼리 생성**
- 동적 WHERE 조건 구성
- SQL 인젝션 방지 (컬럼명 검증)
- 동적 ORDER BY 처리
### 컴파일 상태
✅ TypeScript 컴파일 성공
✅ Linter 오류 없음
---
@ -24,12 +79,14 @@
### 1. EnhancedDynamicFormService (6개 호출, 786 라인)
**주요 기능**:
- 고급 동적 폼 관리
- 폼 검증 규칙
- 조건부 필드 표시
- 폼 템플릿 관리
**예상 Prisma 호출**:
- `getEnhancedForms()` - 고급 폼 목록 조회
- `getEnhancedForm()` - 고급 폼 단건 조회
- `createEnhancedForm()` - 고급 폼 생성
@ -38,6 +95,7 @@
- `getFormValidationRules()` - 검증 규칙 조회
**기술적 고려사항**:
- JSON 필드 (validation_rules, conditional_logic, field_config)
- 복잡한 검증 규칙
- 동적 필드 생성
@ -48,12 +106,14 @@
### 2. DataMappingService (5개 호출, 575 라인)
**주요 기능**:
- 데이터 매핑 설정 관리
- 소스-타겟 필드 매핑
- 데이터 변환 규칙
- 매핑 실행
**예상 Prisma 호출**:
- `getDataMappings()` - 매핑 설정 목록 조회
- `getDataMapping()` - 매핑 설정 단건 조회
- `createDataMapping()` - 매핑 설정 생성
@ -61,6 +121,7 @@
- `deleteDataMapping()` - 매핑 설정 삭제
**기술적 고려사항**:
- JSON 필드 (field_mappings, transformation_rules)
- 복잡한 변환 로직
- 매핑 검증
@ -71,18 +132,21 @@
### 3. DataService (4개 호출, 327 라인)
**주요 기능**:
- 동적 데이터 조회
- 데이터 필터링
- 데이터 정렬
- 데이터 집계
**예상 Prisma 호출**:
- `getDataByTable()` - 테이블별 데이터 조회
- `getDataById()` - 데이터 단건 조회
- `executeCustomQuery()` - 커스텀 쿼리 실행
- `getDataStatistics()` - 데이터 통계 조회
**기술적 고려사항**:
- 동적 테이블 쿼리
- SQL 인젝션 방지
- 동적 WHERE 조건
@ -93,17 +157,20 @@
### 4. AdminService (3개 호출, 374 라인)
**주요 기능**:
- 관리자 메뉴 관리
- 시스템 설정
- 사용자 관리
- 로그 조회
**예상 Prisma 호출**:
- `getAdminMenus()` - 관리자 메뉴 조회
- `getSystemSettings()` - 시스템 설정 조회
- `updateSystemSettings()` - 시스템 설정 업데이트
**기술적 고려사항**:
- 메뉴 계층 구조
- 권한 기반 필터링
- JSON 설정 필드
@ -114,17 +181,23 @@
## 💡 통합 전환 전략
### Phase 1: 단순 CRUD 전환 (12개)
**EnhancedDynamicFormService (6개) + DataMappingService (5개) + AdminService (1개)**
- 기본 CRUD 기능
- JSON 필드 처리
### Phase 2: 동적 쿼리 전환 (4개)
**DataService (4개)**
- 동적 테이블 쿼리
- 보안 검증
### Phase 3: 고급 기능 전환 (2개)
**AdminService (2개)**
- 시스템 설정
- 캐싱
@ -135,6 +208,7 @@
### 예시 1: 고급 폼 생성 (JSON 필드)
**변경 전**:
```typescript
const form = await prisma.enhanced_forms.create({
data: {
@ -149,6 +223,7 @@ const form = await prisma.enhanced_forms.create({
```
**변경 후**:
```typescript
const form = await queryOne<any>(
`INSERT INTO enhanced_forms
@ -170,6 +245,7 @@ const form = await queryOne<any>(
### 예시 2: 데이터 매핑 조회
**변경 전**:
```typescript
const mappings = await prisma.data_mappings.findMany({
where: {
@ -185,6 +261,7 @@ const mappings = await prisma.data_mappings.findMany({
```
**변경 후**:
```typescript
const mappings = await query<any>(
`SELECT
@ -211,6 +288,7 @@ const mappings = await query<any>(
### 예시 3: 동적 테이블 쿼리 (DataService)
**변경 전**:
```typescript
// Prisma로는 동적 테이블 쿼리 불가능
// 이미 $queryRawUnsafe 사용 중일 가능성
@ -221,6 +299,7 @@ const data = await prisma.$queryRawUnsafe(
```
**변경 후**:
```typescript
// SQL 인젝션 방지를 위한 테이블명 검증
const validTableName = validateTableName(tableName);
@ -234,6 +313,7 @@ const data = await query<any>(
### 예시 4: 관리자 메뉴 조회 (계층 구조)
**변경 전**:
```typescript
const menus = await prisma.admin_menus.findMany({
where: { is_active: true },
@ -247,6 +327,7 @@ const menus = await prisma.admin_menus.findMany({
```
**변경 후**:
```typescript
// 재귀 CTE를 사용한 계층 쿼리
const menus = await query<any>(
@ -273,6 +354,7 @@ const menus = await query<any>(
## 🔧 기술적 고려사항
### 1. JSON 필드 처리
```typescript
// 복잡한 JSON 구조
interface ValidationRules {
@ -287,12 +369,14 @@ interface ValidationRules {
JSON.stringify(validationRules);
// 조회 후
const parsed = typeof row.validation_rules === "string"
? JSON.parse(row.validation_rules)
: row.validation_rules;
const parsed =
typeof row.validation_rules === "string"
? JSON.parse(row.validation_rules)
: row.validation_rules;
```
### 2. 동적 테이블 쿼리 보안
```typescript
// 테이블명 화이트리스트
const ALLOWED_TABLES = ["users", "products", "orders"];
@ -314,13 +398,14 @@ function validateColumnName(columnName: string): string {
```
### 3. 재귀 CTE (계층 구조)
```sql
WITH RECURSIVE hierarchy AS (
-- 최상위 노드
SELECT * FROM table WHERE parent_id IS NULL
UNION ALL
-- 하위 노드
SELECT t.* FROM table t
JOIN hierarchy h ON t.parent_id = h.id
@ -329,8 +414,9 @@ SELECT * FROM hierarchy
```
### 4. JSON 집계 (관계 데이터)
```sql
SELECT
SELECT
parent.*,
COALESCE(
json_agg(
@ -348,6 +434,7 @@ GROUP BY parent.id
## 📝 전환 체크리스트
### EnhancedDynamicFormService (6개)
- [ ] `getEnhancedForms()` - 목록 조회
- [ ] `getEnhancedForm()` - 단건 조회
- [ ] `createEnhancedForm()` - 생성 (JSON 필드)
@ -356,6 +443,7 @@ GROUP BY parent.id
- [ ] `getFormValidationRules()` - 검증 규칙 조회
### DataMappingService (5개)
- [ ] `getDataMappings()` - 목록 조회
- [ ] `getDataMapping()` - 단건 조회
- [ ] `createDataMapping()` - 생성
@ -363,17 +451,20 @@ GROUP BY parent.id
- [ ] `deleteDataMapping()` - 삭제
### DataService (4개)
- [ ] `getDataByTable()` - 동적 테이블 조회
- [ ] `getDataById()` - 단건 조회
- [ ] `executeCustomQuery()` - 커스텀 쿼리
- [ ] `getDataStatistics()` - 통계 조회
### AdminService (3개)
- [ ] `getAdminMenus()` - 메뉴 조회 (재귀 CTE)
- [ ] `getSystemSettings()` - 시스템 설정 조회
- [ ] `updateSystemSettings()` - 시스템 설정 업데이트
### 공통 작업
- [ ] import 문 수정 (모든 서비스)
- [ ] Prisma import 완전 제거
- [ ] JSON 필드 처리 확인
@ -384,15 +475,18 @@ GROUP BY parent.id
## 🧪 테스트 계획
### 단위 테스트 (18개)
- 각 Prisma 호출별 1개씩
### 통합 테스트 (6개)
- EnhancedDynamicFormService: 폼 생성 및 검증 테스트 (2개)
- DataMappingService: 매핑 설정 및 실행 테스트 (2개)
- DataService: 동적 쿼리 및 보안 테스트 (1개)
- AdminService: 메뉴 계층 구조 테스트 (1개)
### 보안 테스트
- SQL 인젝션 방지 테스트
- 테이블명 검증 테스트
- 컬럼명 검증 테스트
@ -406,7 +500,6 @@ GROUP BY parent.id
- 동적 쿼리 보안
- 재귀 CTE
- JSON 집계
- **예상 소요 시간**: 2.5~3시간
- Phase 1 (기본 CRUD): 1시간
- Phase 2 (동적 쿼리): 1시간
@ -418,6 +511,7 @@ GROUP BY parent.id
## ⚠️ 주의사항
### 보안 필수 체크리스트
1. ✅ 동적 테이블명은 반드시 화이트리스트 검증
2. ✅ 동적 컬럼명은 정규식으로 검증
3. ✅ WHERE 절 파라미터는 반드시 바인딩
@ -425,6 +519,7 @@ GROUP BY parent.id
5. ✅ 재귀 쿼리는 깊이 제한 설정
### 성능 최적화
- JSON 필드 인덱싱 (GIN 인덱스)
- 재귀 쿼리 깊이 제한
- 집계 쿼리 최적화
@ -435,4 +530,3 @@ GROUP BY parent.id
**상태**: ⏳ **대기 중**
**특이사항**: JSON 필드, 동적 쿼리, 재귀 CTE, 보안 검증 포함
**⚠️ 주의**: 동적 쿼리는 SQL 인젝션 방지가 매우 중요!

View File

@ -144,11 +144,11 @@ backend-node/ (루트)
- `batchExecutionLogService.ts` (7개) - 배치 실행 로그
- `batchManagementService.ts` (5개) - 배치 관리
- `batchSchedulerService.ts` (4개) - 배치 스케줄러
- **데이터 관리 서비스 (18개)** - [통합 계획서](PHASE3.16_DATA_MANAGEMENT_SERVICES_MIGRATION.md)
- `enhancedDynamicFormService.ts` (6개) - 확장 동적 폼
- `dataMappingService.ts` (5개) - 데이터 매핑
- `dataService.ts` (4개) - 데이터 서비스
- `adminService.ts` (3개) - 관리자 메뉴
- **데이터 관리 서비스 (0개)** - ✅ **전환 완료** - [통합 계획서](PHASE3.16_DATA_MANAGEMENT_SERVICES_MIGRATION.md)
- `enhancedDynamicFormService.ts` (0개) - ✅ **전환 완료**
- `dataMappingService.ts` (0개) - ✅ **전환 완료**
- `dataService.ts` (0개) - ✅ **전환 완료**
- `adminService.ts` (0개) - ✅ **전환 완료**
- `ddlExecutionService.ts` (0개) - ✅ **전환 완료** (이미 완료됨) - [계획서](PHASE3.18_DDL_EXECUTION_SERVICE_MIGRATION.md)
- `referenceCacheService.ts` (0개) - ✅ **전환 완료** (이미 완료됨) - [계획서](PHASE3.17_REFERENCE_CACHE_SERVICE_MIGRATION.md)

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],