Phase 1 완료: Raw Query 기반 데이터베이스 아키텍처 구축 ✅ 구현 완료 내용: - DatabaseManager 클래스 구현 (연결 풀, 트랜잭션 관리) - QueryBuilder 유틸리티 (동적 쿼리 생성) - 타입 정의 및 검증 로직 (database.ts, databaseValidator.ts) - 단위 테스트 작성 및 통과 🔧 전환 완료 서비스: - externalCallConfigService.ts (Raw Query 전환) - multiConnectionQueryService.ts (Raw Query 전환) 📚 문서: - PHASE1_USAGE_GUIDE.md (사용 가이드) - DETAILED_FILE_MIGRATION_PLAN.md (상세 계획) - PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md (Phase 1 완료 표시) 🧪 테스트: - database.test.ts (핵심 기능 테스트) - 모든 테스트 통과 확인 이제 Phase 2 (핵심 서비스 전환)로 진행 가능 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1217 lines
32 KiB
Markdown
1217 lines
32 KiB
Markdown
# 📋 파일별 상세 Prisma → Raw Query 마이그레이션 계획
|
|
|
|
## 🎯 개요
|
|
|
|
총 42개 파일, 444개 Prisma 호출을 Raw Query로 전환하는 상세 계획입니다.
|
|
각 파일의 복잡도, 의존성, 전환 전략을 분석하여 기능 손실 없는 완전한 마이그레이션을 수행합니다.
|
|
|
|
---
|
|
|
|
## 🔴 Phase 2: 핵심 서비스 전환 (107개 호출)
|
|
|
|
### 1. screenManagementService.ts (46개 호출) ⭐ 최우선
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 매우 높음 (JSON 처리, 복잡한 조인, 트랜잭션)
|
|
- **주요 기능**: 화면 정의 관리, 레이아웃 저장/불러오기, 메뉴 할당
|
|
- **의존성**: screen_definitions, screen_components, screen_layouts 등
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 1.1 기본 CRUD 작업 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
const screen = await prisma.screen_definitions.create({
|
|
data: {
|
|
screen_name: screenData.screenName,
|
|
screen_code: screenData.screenCode,
|
|
table_name: screenData.tableName,
|
|
company_code: screenData.companyCode,
|
|
description: screenData.description,
|
|
created_by: screenData.createdBy,
|
|
},
|
|
});
|
|
|
|
// 새로운 Raw Query 코드
|
|
const { query, params } = QueryBuilder.insert(
|
|
"screen_definitions",
|
|
{
|
|
screen_name: screenData.screenName,
|
|
screen_code: screenData.screenCode,
|
|
table_name: screenData.tableName,
|
|
company_code: screenData.companyCode,
|
|
description: screenData.description,
|
|
created_by: screenData.createdBy,
|
|
created_at: new Date(),
|
|
updated_at: new Date(),
|
|
},
|
|
{
|
|
returning: ["*"],
|
|
}
|
|
);
|
|
const [screen] = await DatabaseManager.query(query, params);
|
|
```
|
|
|
|
##### 1.2 복잡한 조인 쿼리 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드 (복잡한 include)
|
|
const screens = await prisma.screen_definitions.findMany({
|
|
where: whereClause,
|
|
include: {
|
|
screen_components: true,
|
|
screen_layouts: true,
|
|
},
|
|
orderBy: { created_at: "desc" },
|
|
skip: (page - 1) * size,
|
|
take: size,
|
|
});
|
|
|
|
// 새로운 Raw Query 코드
|
|
const { query, params } = QueryBuilder.select("screen_definitions", {
|
|
columns: [
|
|
"sd.*",
|
|
"json_agg(DISTINCT sc.*) as screen_components",
|
|
"json_agg(DISTINCT sl.*) as screen_layouts",
|
|
],
|
|
joins: [
|
|
{
|
|
type: "LEFT",
|
|
table: "screen_components sc",
|
|
on: "sd.id = sc.screen_id",
|
|
},
|
|
{
|
|
type: "LEFT",
|
|
table: "screen_layouts sl",
|
|
on: "sd.id = sl.screen_id",
|
|
},
|
|
],
|
|
where: whereClause,
|
|
orderBy: "sd.created_at DESC",
|
|
limit: size,
|
|
offset: (page - 1) * size,
|
|
groupBy: ["sd.id"],
|
|
});
|
|
const screens = await DatabaseManager.query(query, params);
|
|
```
|
|
|
|
##### 1.3 JSON 데이터 처리 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드 (JSON 검색)
|
|
const screens = await prisma.screen_definitions.findMany({
|
|
where: {
|
|
screen_config: { path: ["type"], equals: "form" },
|
|
},
|
|
});
|
|
|
|
// 새로운 Raw Query 코드
|
|
const screens = await DatabaseManager.query(
|
|
`
|
|
SELECT * FROM screen_definitions
|
|
WHERE screen_config->>'type' = $1
|
|
`,
|
|
["form"]
|
|
);
|
|
```
|
|
|
|
##### 1.4 트랜잭션 처리 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma 트랜잭션
|
|
await prisma.$transaction(async (tx) => {
|
|
const screen = await tx.screen_definitions.create({ data: screenData });
|
|
await tx.screen_components.createMany({ data: components });
|
|
return screen;
|
|
});
|
|
|
|
// 새로운 Raw Query 트랜잭션
|
|
await DatabaseManager.transaction(async (client) => {
|
|
const screenResult = await client.query(
|
|
"INSERT INTO screen_definitions (...) VALUES (...) RETURNING *",
|
|
screenParams
|
|
);
|
|
const screen = screenResult.rows[0];
|
|
|
|
for (const component of components) {
|
|
await client.query(
|
|
"INSERT INTO screen_components (...) VALUES (...)",
|
|
componentParams
|
|
);
|
|
}
|
|
|
|
return screen;
|
|
});
|
|
```
|
|
|
|
#### 🧪 테스트 전략
|
|
|
|
1. **단위 테스트**: 각 메서드별 입출력 검증
|
|
2. **통합 테스트**: 화면 생성 → 조회 → 수정 → 삭제 전체 플로우
|
|
3. **JSON 데이터 테스트**: 복잡한 레이아웃 데이터 저장/복원
|
|
4. **성능 테스트**: 대량 화면 데이터 처리 성능 비교
|
|
|
|
#### ⚠️ 주의사항
|
|
|
|
- JSON 데이터 타입 변환 주의 (PostgreSQL JSONB ↔ JavaScript Object)
|
|
- 날짜 타입 변환 (Prisma DateTime ↔ PostgreSQL timestamp)
|
|
- NULL 값 처리 일관성 유지
|
|
|
|
---
|
|
|
|
### 2. tableManagementService.ts (35개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 매우 높음 (메타데이터 조회, DDL 실행, 동적 쿼리)
|
|
- **주요 기능**: 테이블/컬럼 정보 관리, 동적 테이블 생성, 메타데이터 캐싱
|
|
- **의존성**: information_schema, table_labels, column_labels
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 2.1 메타데이터 조회 (이미 Raw Query 사용 중)
|
|
|
|
```typescript
|
|
// 현재 코드 (이미 Raw Query)
|
|
const rawTables = await prisma.$queryRaw<any[]>`
|
|
SELECT
|
|
t.table_name as "tableName",
|
|
COALESCE(tl.table_label, t.table_name) as "displayName"
|
|
FROM information_schema.tables t
|
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
|
WHERE t.table_schema = 'public'
|
|
`;
|
|
|
|
// 새로운 Raw Query 코드 (Prisma 제거)
|
|
const rawTables = await DatabaseManager.query(`
|
|
SELECT
|
|
t.table_name as "tableName",
|
|
COALESCE(tl.table_label, t.table_name) as "displayName"
|
|
FROM information_schema.tables t
|
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
|
WHERE t.table_schema = 'public'
|
|
`);
|
|
```
|
|
|
|
##### 2.2 동적 테이블 데이터 조회
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드 (동적 테이블명)
|
|
const data = await prisma.$queryRawUnsafe(
|
|
`SELECT * FROM ${tableName} WHERE id = $1`,
|
|
[id]
|
|
);
|
|
|
|
// 새로운 Raw Query 코드
|
|
const data = await DatabaseManager.queryUnsafe(
|
|
`SELECT * FROM ${DatabaseValidator.sanitizeTableName(
|
|
tableName
|
|
)} WHERE id = $1`,
|
|
[id]
|
|
);
|
|
```
|
|
|
|
##### 2.3 UPSERT 작업 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma UPSERT
|
|
await prisma.table_labels.upsert({
|
|
where: { table_name: tableName },
|
|
update: { table_label: label, updated_date: new Date() },
|
|
create: {
|
|
table_name: tableName,
|
|
table_label: label,
|
|
created_date: new Date(),
|
|
},
|
|
});
|
|
|
|
// 새로운 Raw Query UPSERT
|
|
const { query, params } = QueryBuilder.insert(
|
|
"table_labels",
|
|
{
|
|
table_name: tableName,
|
|
table_label: label,
|
|
created_date: new Date(),
|
|
updated_date: new Date(),
|
|
},
|
|
{
|
|
onConflict: {
|
|
columns: ["table_name"],
|
|
action: "DO UPDATE",
|
|
updateSet: ["table_label", "updated_date"],
|
|
},
|
|
returning: ["*"],
|
|
}
|
|
);
|
|
await DatabaseManager.query(query, params);
|
|
```
|
|
|
|
#### 🧪 테스트 전략
|
|
|
|
1. **메타데이터 테스트**: information_schema 조회 결과 일치성
|
|
2. **동적 쿼리 테스트**: 다양한 테이블명/컬럼명으로 안전성 검증
|
|
3. **DDL 테스트**: 테이블 생성/수정/삭제 기능
|
|
4. **캐시 테스트**: 메타데이터 캐싱 동작 검증
|
|
|
|
---
|
|
|
|
### 3. dataflowService.ts (31개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 높음 (관계 관리, 복잡한 비즈니스 로직)
|
|
- **주요 기능**: 테이블 관계 관리, 데이터플로우 다이어그램
|
|
- **의존성**: table_relationships, dataflow_diagrams
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 3.1 관계 생성 로직
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
const maxDiagramId = await prisma.table_relationships.findFirst({
|
|
where: { company_code: data.companyCode },
|
|
orderBy: { diagram_id: "desc" },
|
|
select: { diagram_id: true },
|
|
});
|
|
|
|
const relationship = await prisma.table_relationships.create({
|
|
data: {
|
|
diagram_id: diagramId,
|
|
relationship_name: data.relationshipName,
|
|
// ... 기타 필드
|
|
},
|
|
});
|
|
|
|
// 새로운 Raw Query 코드
|
|
const maxDiagramResult = await DatabaseManager.query(
|
|
`
|
|
SELECT diagram_id FROM table_relationships
|
|
WHERE company_code = $1
|
|
ORDER BY diagram_id DESC
|
|
LIMIT 1
|
|
`,
|
|
[data.companyCode]
|
|
);
|
|
|
|
const diagramId = (maxDiagramResult[0]?.diagram_id || 0) + 1;
|
|
|
|
const { query, params } = QueryBuilder.insert(
|
|
"table_relationships",
|
|
{
|
|
diagram_id: diagramId,
|
|
relationship_name: data.relationshipName,
|
|
// ... 기타 필드
|
|
},
|
|
{ returning: ["*"] }
|
|
);
|
|
|
|
const [relationship] = await DatabaseManager.query(query, params);
|
|
```
|
|
|
|
#### 🧪 테스트 전략
|
|
|
|
1. **관계 생성 테스트**: 다양한 테이블 관계 패턴
|
|
2. **중복 검증 테스트**: 동일 관계 생성 방지
|
|
3. **다이어그램 테스트**: 복잡한 관계도 생성/조회
|
|
|
|
---
|
|
|
|
## 🟡 Phase 3: 관리 기능 전환 (162개 호출)
|
|
|
|
### 4. multilangService.ts (25개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 높음 (재귀 쿼리, 다국어 처리)
|
|
- **주요 기능**: 다국어 번역 관리, 계층 구조 처리
|
|
- **의존성**: multilang_translations, 재귀 관계
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 4.1 재귀 쿼리 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드 (재귀 관계 조회)
|
|
const translations = await prisma.multilang_translations.findMany({
|
|
where: { parent_id: null },
|
|
include: {
|
|
children: {
|
|
include: {
|
|
children: true, // 중첩 include
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// 새로운 Raw Query 코드 (WITH RECURSIVE)
|
|
const translations = await DatabaseManager.query(`
|
|
WITH RECURSIVE translation_tree AS (
|
|
SELECT *, 0 as level
|
|
FROM multilang_translations
|
|
WHERE parent_id IS NULL
|
|
|
|
UNION ALL
|
|
|
|
SELECT t.*, tt.level + 1
|
|
FROM multilang_translations t
|
|
JOIN translation_tree tt ON t.parent_id = tt.id
|
|
)
|
|
SELECT * FROM translation_tree
|
|
ORDER BY level, sort_order
|
|
`);
|
|
```
|
|
|
|
#### 🧪 테스트 전략
|
|
|
|
1. **재귀 쿼리 테스트**: 깊은 계층 구조 처리
|
|
2. **다국어 테스트**: 다양한 언어 코드 처리
|
|
3. **성능 테스트**: 대량 번역 데이터 처리
|
|
|
|
---
|
|
|
|
### 5. batchService.ts (16개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 중간 (배치 작업 관리)
|
|
- **주요 기능**: 배치 작업 스케줄링, 실행 이력 관리
|
|
- **의존성**: batch_configs, batch_execution_logs
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 5.1 배치 설정 관리
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
const batchConfigs = await prisma.batch_configs.findMany({
|
|
where: { is_active: true },
|
|
include: { execution_logs: { take: 10, orderBy: { created_at: "desc" } } },
|
|
});
|
|
|
|
// 새로운 Raw Query 코드
|
|
const batchConfigs = await DatabaseManager.query(`
|
|
SELECT
|
|
bc.*,
|
|
json_agg(
|
|
json_build_object(
|
|
'id', bel.id,
|
|
'status', bel.status,
|
|
'created_at', bel.created_at
|
|
) ORDER BY bel.created_at DESC
|
|
) FILTER (WHERE bel.id IS NOT NULL) as execution_logs
|
|
FROM batch_configs bc
|
|
LEFT JOIN (
|
|
SELECT DISTINCT ON (batch_config_id)
|
|
batch_config_id, id, status, created_at,
|
|
ROW_NUMBER() OVER (PARTITION BY batch_config_id ORDER BY created_at DESC) as rn
|
|
FROM batch_execution_logs
|
|
) bel ON bc.id = bel.batch_config_id AND bel.rn <= 10
|
|
WHERE bc.is_active = true
|
|
GROUP BY bc.id
|
|
`);
|
|
```
|
|
|
|
---
|
|
|
|
### 7. dynamicFormService.ts (15개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 높음 (UPSERT, 동적 테이블 처리, 타입 변환)
|
|
- **주요 기능**: 동적 폼 데이터 저장/조회, 데이터 검증, 타입 변환
|
|
- **의존성**: 동적 테이블들, form_data, 이벤트 트리거
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 7.1 동적 UPSERT 로직 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드 (동적 테이블 UPSERT)
|
|
const existingRecord = await prisma.$queryRawUnsafe(
|
|
`SELECT * FROM ${tableName} WHERE id = $1`,
|
|
[id]
|
|
);
|
|
|
|
if (existingRecord.length > 0) {
|
|
await prisma.$executeRawUnsafe(updateQuery, updateValues);
|
|
} else {
|
|
await prisma.$executeRawUnsafe(insertQuery, insertValues);
|
|
}
|
|
|
|
// 새로운 Raw Query 코드 (PostgreSQL UPSERT)
|
|
const upsertQuery = `
|
|
INSERT INTO ${DatabaseValidator.sanitizeTableName(tableName)}
|
|
(${columns.join(", ")})
|
|
VALUES (${placeholders.join(", ")})
|
|
ON CONFLICT (id)
|
|
DO UPDATE SET
|
|
${updateColumns.map((col) => `${col} = EXCLUDED.${col}`).join(", ")},
|
|
updated_at = NOW()
|
|
RETURNING *
|
|
`;
|
|
|
|
const [result] = await DatabaseManager.query(upsertQuery, values);
|
|
```
|
|
|
|
##### 7.2 타입 변환 로직 강화
|
|
|
|
```typescript
|
|
// 기존 타입 변환 (Prisma 자동 처리)
|
|
const data = await prisma.someTable.create({ data: formData });
|
|
|
|
// 새로운 타입 변환 (명시적 처리)
|
|
const convertedData = this.convertFormDataForPostgreSQL(formData, tableSchema);
|
|
const { query, params } = QueryBuilder.insert(tableName, convertedData, {
|
|
returning: ["*"]
|
|
});
|
|
const [result] = await DatabaseManager.query(query, params);
|
|
|
|
// 타입 변환 함수 강화
|
|
private convertFormDataForPostgreSQL(data: any, schema: TableColumn[]): any {
|
|
const converted = {};
|
|
|
|
for (const [key, value] of Object.entries(data)) {
|
|
const column = schema.find(col => col.columnName === key);
|
|
if (column) {
|
|
converted[key] = this.convertValueByType(value, column.dataType);
|
|
}
|
|
}
|
|
|
|
return converted;
|
|
}
|
|
```
|
|
|
|
##### 7.3 동적 검증 로직
|
|
|
|
```typescript
|
|
// 기존 Prisma 검증 (스키마 기반)
|
|
const validation = await prisma.$validator.validate(data, schema);
|
|
|
|
// 새로운 Raw Query 검증
|
|
async validateFormData(data: any, tableName: string): Promise<ValidationResult> {
|
|
// 테이블 스키마 조회
|
|
const schema = await this.getTableSchema(tableName);
|
|
|
|
const errors: ValidationError[] = [];
|
|
|
|
for (const column of schema) {
|
|
const value = data[column.columnName];
|
|
|
|
// NULL 검증
|
|
if (!column.nullable && (value === null || value === undefined)) {
|
|
errors.push({
|
|
field: column.columnName,
|
|
message: `${column.columnName}은(는) 필수 입력 항목입니다.`,
|
|
code: 'REQUIRED_FIELD'
|
|
});
|
|
}
|
|
|
|
// 타입 검증
|
|
if (value !== null && !this.isValidType(value, column.dataType)) {
|
|
errors.push({
|
|
field: column.columnName,
|
|
message: `${column.columnName}의 데이터 타입이 올바르지 않습니다.`,
|
|
code: 'INVALID_TYPE'
|
|
});
|
|
}
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
```
|
|
|
|
#### 🧪 테스트 전략
|
|
|
|
1. **UPSERT 테스트**: 신규 생성 vs 기존 업데이트 시나리오
|
|
2. **타입 변환 테스트**: 다양한 PostgreSQL 타입 변환
|
|
3. **검증 테스트**: 필수 필드, 타입 검증, 길이 제한
|
|
4. **동적 테이블 테스트**: 런타임에 생성된 테이블 처리
|
|
|
|
---
|
|
|
|
### 8. externalDbConnectionService.ts (15개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 높음 (다중 DB 연결, 외부 시스템 연동)
|
|
- **주요 기능**: 외부 데이터베이스 연결 관리, 스키마 동기화
|
|
- **의존성**: external_db_connections, connection_pools
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 8.1 연결 설정 관리
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
const connections = await prisma.external_db_connections.findMany({
|
|
where: { is_active: true },
|
|
include: { connection_pools: true },
|
|
});
|
|
|
|
// 새로운 Raw Query 코드
|
|
const connections = await DatabaseManager.query(`
|
|
SELECT
|
|
edc.*,
|
|
json_agg(cp.*) as connection_pools
|
|
FROM external_db_connections edc
|
|
LEFT JOIN connection_pools cp ON edc.id = cp.connection_id
|
|
WHERE edc.is_active = true
|
|
GROUP BY edc.id
|
|
`);
|
|
```
|
|
|
|
##### 8.2 연결 풀 관리
|
|
|
|
```typescript
|
|
// 외부 DB 연결 풀 생성 및 관리
|
|
class ExternalConnectionManager {
|
|
private static pools = new Map<string, Pool>();
|
|
|
|
static async getConnection(connectionId: string): Promise<PoolClient> {
|
|
if (!this.pools.has(connectionId)) {
|
|
const config = await this.getConnectionConfig(connectionId);
|
|
this.pools.set(connectionId, new Pool(config));
|
|
}
|
|
|
|
return this.pools.get(connectionId)!.connect();
|
|
}
|
|
|
|
private static async getConnectionConfig(connectionId: string) {
|
|
const [config] = await DatabaseManager.query(
|
|
`
|
|
SELECT host, port, database, username, password, ssl_config
|
|
FROM external_db_connections
|
|
WHERE id = $1 AND is_active = true
|
|
`,
|
|
[connectionId]
|
|
);
|
|
|
|
return {
|
|
host: config.host,
|
|
port: config.port,
|
|
database: config.database,
|
|
user: config.username,
|
|
password: EncryptUtil.decrypt(config.password),
|
|
ssl: config.ssl_config,
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🟢 Phase 4: 확장 기능 전환 (129개 호출)
|
|
|
|
### 9. adminController.ts (28개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 중간 (컨트롤러 레이어, 다양한 관리 기능)
|
|
- **주요 기능**: 관리자 메뉴, 사용자 관리, 권한 관리, 시스템 설정
|
|
- **의존성**: user_info, menu_info, auth_groups, company_mng
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 9.1 메뉴 관리 API 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
export async function getAdminMenus(req: AuthenticatedRequest, res: Response) {
|
|
const menus = await prisma.menu_info.findMany({
|
|
where: {
|
|
is_active: "Y",
|
|
company_code: userCompanyCode,
|
|
},
|
|
include: {
|
|
parent: true,
|
|
children: { where: { is_active: "Y" } },
|
|
},
|
|
orderBy: { sort_order: "asc" },
|
|
});
|
|
|
|
res.json({ success: true, data: menus });
|
|
}
|
|
|
|
// 새로운 Raw Query 코드
|
|
export async function getAdminMenus(req: AuthenticatedRequest, res: Response) {
|
|
// 계층형 메뉴 구조를 한 번의 쿼리로 조회
|
|
const menus = await DatabaseManager.query(
|
|
`
|
|
WITH RECURSIVE menu_tree AS (
|
|
SELECT
|
|
m.*,
|
|
0 as level,
|
|
ARRAY[m.sort_order] as path
|
|
FROM menu_info m
|
|
WHERE m.parent_id IS NULL
|
|
AND m.is_active = 'Y'
|
|
AND m.company_code = $1
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
m.*,
|
|
mt.level + 1,
|
|
mt.path || m.sort_order
|
|
FROM menu_info m
|
|
JOIN menu_tree mt ON m.parent_id = mt.id
|
|
WHERE m.is_active = 'Y'
|
|
)
|
|
SELECT * FROM menu_tree
|
|
ORDER BY path
|
|
`,
|
|
[userCompanyCode]
|
|
);
|
|
|
|
res.json({ success: true, data: menus });
|
|
}
|
|
```
|
|
|
|
##### 9.2 사용자 관리 API 전환
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
export async function getUserList(req: AuthenticatedRequest, res: Response) {
|
|
const users = await prisma.user_info.findMany({
|
|
where: { company_code: userCompanyCode },
|
|
include: {
|
|
dept_info: true,
|
|
user_auth: { include: { auth_group: true } },
|
|
},
|
|
});
|
|
}
|
|
|
|
// 새로운 Raw Query 코드
|
|
export async function getUserList(req: AuthenticatedRequest, res: Response) {
|
|
const users = await DatabaseManager.query(
|
|
`
|
|
SELECT
|
|
ui.*,
|
|
di.dept_name,
|
|
json_agg(
|
|
json_build_object(
|
|
'auth_code', ag.auth_code,
|
|
'auth_name', ag.auth_name
|
|
)
|
|
) FILTER (WHERE ag.auth_code IS NOT NULL) as authorities
|
|
FROM user_info ui
|
|
LEFT JOIN dept_info di ON ui.dept_code = di.dept_code
|
|
LEFT JOIN user_auth ua ON ui.user_id = ua.user_id
|
|
LEFT JOIN auth_group ag ON ua.auth_code = ag.auth_code
|
|
WHERE ui.company_code = $1
|
|
GROUP BY ui.user_id, di.dept_name
|
|
ORDER BY ui.created_date DESC
|
|
`,
|
|
[userCompanyCode]
|
|
);
|
|
}
|
|
```
|
|
|
|
##### 9.3 권한 관리 API 전환
|
|
|
|
```typescript
|
|
// 복잡한 권한 체크 로직
|
|
export async function checkUserPermission(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
) {
|
|
const { menuUrl } = req.body;
|
|
|
|
const hasPermission = await DatabaseManager.query(
|
|
`
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM user_auth ua
|
|
JOIN auth_group ag ON ua.auth_code = ag.auth_code
|
|
JOIN menu_auth_group mag ON ag.auth_code = mag.auth_code
|
|
JOIN menu_info mi ON mag.menu_id = mi.id
|
|
WHERE ua.user_id = $1
|
|
AND mi.url = $2
|
|
AND ua.is_active = 'Y'
|
|
AND ag.is_active = 'Y'
|
|
AND mi.is_active = 'Y'
|
|
) as has_permission
|
|
`,
|
|
[req.user.userId, menuUrl]
|
|
);
|
|
|
|
res.json({ success: true, hasPermission: hasPermission[0].has_permission });
|
|
}
|
|
```
|
|
|
|
#### 🧪 테스트 전략
|
|
|
|
1. **API 응답 테스트**: 기존 API와 동일한 응답 구조 확인
|
|
2. **권한 테스트**: 다양한 사용자 권한 시나리오
|
|
3. **계층 구조 테스트**: 메뉴 트리 구조 정확성
|
|
4. **성능 테스트**: 복잡한 조인 쿼리 성능
|
|
|
|
---
|
|
|
|
### 10. componentStandardService.ts (16개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 중간 (컴포넌트 표준 관리)
|
|
- **주요 기능**: UI 컴포넌트 표준 정의, 템플릿 관리
|
|
- **의존성**: component_standards, ui_templates
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 10.1 컴포넌트 표준 조회
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
const components = await prisma.component_standards.findMany({
|
|
where: { category: category },
|
|
include: {
|
|
templates: { where: { is_active: true } },
|
|
properties: true,
|
|
},
|
|
});
|
|
|
|
// 새로운 Raw Query 코드
|
|
const components = await DatabaseManager.query(
|
|
`
|
|
SELECT
|
|
cs.*,
|
|
json_agg(
|
|
DISTINCT jsonb_build_object(
|
|
'id', ut.id,
|
|
'template_name', ut.template_name,
|
|
'template_config', ut.template_config
|
|
)
|
|
) FILTER (WHERE ut.id IS NOT NULL) as templates,
|
|
json_agg(
|
|
DISTINCT jsonb_build_object(
|
|
'property_name', cp.property_name,
|
|
'property_type', cp.property_type,
|
|
'default_value', cp.default_value
|
|
)
|
|
) FILTER (WHERE cp.id IS NOT NULL) as properties
|
|
FROM component_standards cs
|
|
LEFT JOIN ui_templates ut ON cs.id = ut.component_id AND ut.is_active = true
|
|
LEFT JOIN component_properties cp ON cs.id = cp.component_id
|
|
WHERE cs.category = $1
|
|
GROUP BY cs.id
|
|
`,
|
|
[category]
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### 11. commonCodeService.ts (15개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 중간 (코드 관리, 계층 구조)
|
|
- **주요 기능**: 공통 코드 관리, 코드 카테고리 관리
|
|
- **의존성**: code_info, code_category
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 11.1 계층형 코드 구조 처리
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
const codes = await prisma.code_info.findMany({
|
|
where: { category_code: categoryCode },
|
|
include: {
|
|
parent: true,
|
|
children: { where: { is_active: "Y" } },
|
|
},
|
|
});
|
|
|
|
// 새로운 Raw Query 코드 (재귀 CTE 사용)
|
|
const codes = await DatabaseManager.query(
|
|
`
|
|
WITH RECURSIVE code_tree AS (
|
|
SELECT
|
|
ci.*,
|
|
0 as level,
|
|
CAST(ci.sort_order as TEXT) as path
|
|
FROM code_info ci
|
|
WHERE ci.parent_code IS NULL
|
|
AND ci.category_code = $1
|
|
AND ci.is_active = 'Y'
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
ci.*,
|
|
ct.level + 1,
|
|
ct.path || '.' || ci.sort_order
|
|
FROM code_info ci
|
|
JOIN code_tree ct ON ci.parent_code = ct.code
|
|
WHERE ci.is_active = 'Y'
|
|
)
|
|
SELECT * FROM code_tree
|
|
ORDER BY path
|
|
`,
|
|
[categoryCode]
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### 12. batchService.ts (16개 호출)
|
|
|
|
#### 📊 현재 상태 분석
|
|
|
|
- **복잡도**: 중간-높음 (배치 작업, 스케줄링)
|
|
- **주요 기능**: 배치 작업 관리, 실행 이력, 스케줄링
|
|
- **의존성**: batch_configs, batch_execution_logs
|
|
|
|
#### 🔧 전환 전략
|
|
|
|
##### 12.1 배치 실행 이력 관리
|
|
|
|
```typescript
|
|
// 기존 Prisma 코드
|
|
const batchHistory = await prisma.batch_execution_logs.findMany({
|
|
where: { batch_config_id: configId },
|
|
include: { batch_config: true },
|
|
orderBy: { created_at: "desc" },
|
|
take: 50,
|
|
});
|
|
|
|
// 새로운 Raw Query 코드
|
|
const batchHistory = await DatabaseManager.query(
|
|
`
|
|
SELECT
|
|
bel.*,
|
|
bc.batch_name,
|
|
bc.description as batch_description
|
|
FROM batch_execution_logs bel
|
|
JOIN batch_configs bc ON bel.batch_config_id = bc.id
|
|
WHERE bel.batch_config_id = $1
|
|
ORDER BY bel.created_at DESC
|
|
LIMIT 50
|
|
`,
|
|
[configId]
|
|
);
|
|
```
|
|
|
|
##### 12.2 배치 상태 업데이트
|
|
|
|
```typescript
|
|
// 트랜잭션을 사용한 배치 상태 관리
|
|
async updateBatchStatus(batchId: number, status: string, result?: any) {
|
|
await DatabaseManager.transaction(async (client) => {
|
|
// 실행 로그 업데이트
|
|
await client.query(`
|
|
UPDATE batch_execution_logs
|
|
SET status = $1,
|
|
result = $2,
|
|
completed_at = NOW(),
|
|
updated_at = NOW()
|
|
WHERE id = $3
|
|
`, [status, result, batchId]);
|
|
|
|
// 배치 설정의 마지막 실행 시간 업데이트
|
|
await client.query(`
|
|
UPDATE batch_configs
|
|
SET last_executed_at = NOW(),
|
|
last_status = $1,
|
|
updated_at = NOW()
|
|
WHERE id = (
|
|
SELECT batch_config_id
|
|
FROM batch_execution_logs
|
|
WHERE id = $2
|
|
)
|
|
`, [status, batchId]);
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 나머지 파일들 요약 전환 계획
|
|
|
|
### Phase 2 나머지 파일들 (6개 호출)
|
|
|
|
- **dataflowControlService.ts** (6개): 제어 로직, 조건부 실행
|
|
- **ddlExecutionService.ts** (6개): DDL 실행, 스키마 변경
|
|
- **authService.ts** (5개): 사용자 인증, 토큰 관리
|
|
- **multiConnectionQueryService.ts** (4개): 다중 DB 연결
|
|
|
|
### Phase 3 나머지 파일들 (121개 호출)
|
|
|
|
- **dataflowDiagramService.ts** (12개): 다이어그램 관리, JSON 처리
|
|
- **collectionService.ts** (11개): 컬렉션 관리
|
|
- **layoutService.ts** (10개): 레이아웃 관리
|
|
- **dbTypeCategoryService.ts** (10개): DB 타입 분류
|
|
- **templateStandardService.ts** (9개): 템플릿 표준
|
|
- **ddlAuditLogger.ts** (8개): DDL 감사 로그
|
|
- **externalCallConfigService.ts** (8개): 외부 호출 설정
|
|
- **batchExternalDbService.ts** (8개): 배치 외부DB
|
|
- **batchExecutionLogService.ts** (7개): 배치 실행 로그
|
|
- **eventTriggerService.ts** (6개): 이벤트 트리거
|
|
- **enhancedDynamicFormService.ts** (6개): 확장 동적 폼
|
|
- **entityJoinService.ts** (5개): 엔티티 조인
|
|
- **dataMappingService.ts** (5개): 데이터 매핑
|
|
- **batchManagementService.ts** (5개): 배치 관리
|
|
- **batchSchedulerService.ts** (4개): 배치 스케줄러
|
|
- **dataService.ts** (4개): 데이터 서비스
|
|
- **adminService.ts** (3개): 관리자 서비스
|
|
- **referenceCacheService.ts** (3개): 참조 캐시
|
|
|
|
### Phase 4 나머지 파일들 (101개 호출)
|
|
|
|
- **webTypeStandardController.ts** (11개): 웹타입 표준 컨트롤러
|
|
- **fileController.ts** (11개): 파일 업로드/다운로드 컨트롤러
|
|
- **buttonActionStandardController.ts** (11개): 버튼 액션 표준
|
|
- **entityReferenceController.ts** (4개): 엔티티 참조 컨트롤러
|
|
- **database.ts** (4개): 데이터베이스 설정
|
|
- **dataflowExecutionController.ts** (3개): 데이터플로우 실행
|
|
- **screenFileController.ts** (2개): 화면 파일 컨트롤러
|
|
- **ddlRoutes.ts** (2개): DDL 라우트
|
|
- **companyManagementRoutes.ts** (2개): 회사 관리 라우트
|
|
|
|
---
|
|
|
|
## 🔗 파일 간 의존성 분석
|
|
|
|
### 1. 핵심 의존성 체인
|
|
|
|
```
|
|
DatabaseManager (기반)
|
|
↓
|
|
QueryBuilder (쿼리 생성)
|
|
↓
|
|
Services (비즈니스 로직)
|
|
↓
|
|
Controllers (API 엔드포인트)
|
|
↓
|
|
Routes (라우팅)
|
|
```
|
|
|
|
### 2. 서비스 간 의존성
|
|
|
|
```
|
|
tableManagementService
|
|
↓ (테이블 메타데이터)
|
|
screenManagementService
|
|
↓ (화면 정의)
|
|
dynamicFormService
|
|
↓ (폼 데이터)
|
|
dataflowControlService
|
|
```
|
|
|
|
### 3. 전환 순서 (의존성 고려)
|
|
|
|
1. **기반 구조**: DatabaseManager, QueryBuilder
|
|
2. **메타데이터 서비스**: tableManagementService
|
|
3. **핵심 비즈니스**: screenManagementService, dataflowService
|
|
4. **폼 처리**: dynamicFormService
|
|
5. **외부 연동**: externalDbConnectionService
|
|
6. **관리 기능**: adminService, commonCodeService
|
|
7. **배치 시스템**: batchService 계열
|
|
8. **컨트롤러**: adminController 등
|
|
9. **라우트**: 각종 라우트 파일들
|
|
|
|
---
|
|
|
|
## ⚡ 전환 가속화 전략
|
|
|
|
### 1. 병렬 전환 가능 그룹
|
|
|
|
```
|
|
그룹 A (독립적): authService, adminService, commonCodeService
|
|
그룹 B (배치 관련): batchService, batchSchedulerService, batchExecutionLogService
|
|
그룹 C (컨트롤러): adminController, fileController, webTypeStandardController
|
|
그룹 D (표준 관리): componentStandardService, templateStandardService
|
|
```
|
|
|
|
### 2. 공통 패턴 템플릿 활용
|
|
|
|
```typescript
|
|
// 표준 CRUD 템플릿
|
|
class StandardCRUDTemplate {
|
|
static async create(tableName: string, data: any) {
|
|
const { query, params } = QueryBuilder.insert(tableName, data, {
|
|
returning: ["*"],
|
|
});
|
|
return await DatabaseManager.query(query, params);
|
|
}
|
|
|
|
static async findMany(tableName: string, options: any) {
|
|
const { query, params } = QueryBuilder.select(tableName, options);
|
|
return await DatabaseManager.query(query, params);
|
|
}
|
|
|
|
// ... 기타 표준 메서드들
|
|
}
|
|
```
|
|
|
|
### 3. 자동화 도구 활용
|
|
|
|
```typescript
|
|
// Prisma → Raw Query 자동 변환 도구
|
|
class PrismaToRawConverter {
|
|
static convertFindMany(prismaCall: string): string {
|
|
// Prisma findMany 호출을 Raw Query로 자동 변환
|
|
}
|
|
|
|
static convertCreate(prismaCall: string): string {
|
|
// Prisma create 호출을 Raw Query로 자동 변환
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 통합 테스트 전략
|
|
|
|
### 1. 파일별 테스트 매트릭스
|
|
|
|
| 파일명 | 단위테스트 | 통합테스트 | 성능테스트 | E2E테스트 |
|
|
| ----------------------- | ---------- | ---------- | ---------- | --------- |
|
|
| screenManagementService | ✅ | ✅ | ✅ | ✅ |
|
|
| tableManagementService | ✅ | ✅ | ✅ | ❌ |
|
|
| dataflowService | ✅ | ✅ | ❌ | ❌ |
|
|
| adminController | ✅ | ✅ | ❌ | ✅ |
|
|
|
|
### 2. 회귀 테스트 자동화
|
|
|
|
```typescript
|
|
// 기존 Prisma vs 새로운 Raw Query 결과 비교
|
|
describe("Migration Regression Tests", () => {
|
|
test("screenManagementService.getScreens should return identical results", async () => {
|
|
const prismaResult = await oldScreenService.getScreens(params);
|
|
const rawQueryResult = await newScreenService.getScreens(params);
|
|
|
|
expect(normalizeResult(rawQueryResult)).toEqual(
|
|
normalizeResult(prismaResult)
|
|
);
|
|
});
|
|
});
|
|
```
|
|
|
|
### 3. 성능 벤치마크
|
|
|
|
```typescript
|
|
// 성능 비교 테스트
|
|
describe("Performance Benchmarks", () => {
|
|
test("Complex query performance comparison", async () => {
|
|
const iterations = 1000;
|
|
|
|
const prismaTime = await measureTime(
|
|
() => prismaService.complexQuery(params),
|
|
iterations
|
|
);
|
|
|
|
const rawQueryTime = await measureTime(
|
|
() => rawQueryService.complexQuery(params),
|
|
iterations
|
|
);
|
|
|
|
expect(rawQueryTime).toBeLessThanOrEqual(prismaTime * 1.1); // 10% 허용
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 공통 전환 패턴
|
|
|
|
### 1. 기본 CRUD 패턴
|
|
|
|
```typescript
|
|
// CREATE
|
|
const { query, params } = QueryBuilder.insert(tableName, data, {
|
|
returning: ["*"],
|
|
});
|
|
const [result] = await DatabaseManager.query(query, params);
|
|
|
|
// READ
|
|
const { query, params } = QueryBuilder.select(tableName, {
|
|
where,
|
|
orderBy,
|
|
limit,
|
|
});
|
|
const results = await DatabaseManager.query(query, params);
|
|
|
|
// UPDATE
|
|
const { query, params } = QueryBuilder.update(tableName, data, where);
|
|
const [result] = await DatabaseManager.query(query, params);
|
|
|
|
// DELETE
|
|
const { query, params } = QueryBuilder.delete(tableName, where);
|
|
const results = await DatabaseManager.query(query, params);
|
|
```
|
|
|
|
### 2. 트랜잭션 패턴
|
|
|
|
```typescript
|
|
await DatabaseManager.transaction(async (client) => {
|
|
const result1 = await client.query(query1, params1);
|
|
const result2 = await client.query(query2, params2);
|
|
return { result1, result2 };
|
|
});
|
|
```
|
|
|
|
### 3. 동적 쿼리 패턴
|
|
|
|
```typescript
|
|
const tableName = DatabaseValidator.sanitizeTableName(userInput);
|
|
const columnName = DatabaseValidator.sanitizeColumnName(userInput);
|
|
const query = `SELECT ${columnName} FROM ${tableName} WHERE id = $1`;
|
|
const result = await DatabaseManager.query(query, [id]);
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 전환 체크리스트
|
|
|
|
### 각 파일별 필수 확인사항
|
|
|
|
- [ ] 모든 Prisma 호출 식별 및 변환
|
|
- [ ] 타입 변환 (Date, JSON, BigInt) 처리
|
|
- [ ] NULL 값 처리 일관성
|
|
- [ ] 트랜잭션 경계 유지
|
|
- [ ] 에러 처리 로직 보존
|
|
- [ ] 성능 최적화 (인덱스 힌트 등)
|
|
- [ ] 단위 테스트 작성
|
|
- [ ] 통합 테스트 실행
|
|
- [ ] 기능 동작 검증
|
|
- [ ] 성능 비교 테스트
|
|
|
|
### 공통 주의사항
|
|
|
|
1. **SQL 인젝션 방지**: 모든 동적 쿼리에 파라미터 바인딩 사용
|
|
2. **타입 안전성**: TypeScript 타입 정의 유지
|
|
3. **에러 처리**: Prisma 에러를 적절한 HTTP 상태코드로 변환
|
|
4. **로깅**: 쿼리 실행 로그 및 성능 모니터링
|
|
5. **백워드 호환성**: API 응답 형식 유지
|
|
|
|
---
|
|
|
|
## 🎯 성공 기준
|
|
|
|
### 기능적 요구사항
|
|
|
|
- [ ] 모든 기존 API 엔드포인트 정상 동작
|
|
- [ ] 데이터 일관성 유지
|
|
- [ ] 트랜잭션 무결성 보장
|
|
- [ ] 에러 처리 동일성
|
|
|
|
### 성능 요구사항
|
|
|
|
- [ ] 응답 시간 기존 대비 ±10% 이내
|
|
- [ ] 메모리 사용량 최적화
|
|
- [ ] 동시 접속 처리 능력 유지
|
|
|
|
### 품질 요구사항
|
|
|
|
- [ ] 코드 커버리지 90% 이상
|
|
- [ ] 타입 안전성 보장
|
|
- [ ] 보안 검증 통과
|
|
- [ ] 문서화 완료
|
|
|
|
이 상세 계획을 통해 각 파일별로 체계적이고 안전한 마이그레이션을 수행할 수 있습니다.
|