429 lines
12 KiB
Markdown
429 lines
12 KiB
Markdown
# 🗂️ Phase 2.2: TableManagementService Raw Query 전환 계획
|
|
|
|
## 📋 개요
|
|
|
|
TableManagementService는 **33개의 Prisma 호출**이 있습니다. 대부분(약 26개)은 `$queryRaw`를 사용하고 있어 SQL은 이미 작성되어 있지만, **Prisma 클라이언트를 완전히 제거하려면 33개 모두를 `db.ts`의 `query` 함수로 교체**해야 합니다.
|
|
|
|
### 📊 기본 정보
|
|
|
|
| 항목 | 내용 |
|
|
| --------------- | ----------------------------------------------------- |
|
|
| 파일 위치 | `backend-node/src/services/tableManagementService.ts` |
|
|
| 파일 크기 | 3,178 라인 |
|
|
| Prisma 호출 | 33개 ($queryRaw: 26개, ORM: 7개) |
|
|
| **현재 진행률** | **0/33 (0%)** ⏳ **전환 필요** |
|
|
| **전환 필요** | **33개 모두 전환 필요** (SQL은 이미 작성되어 있음) |
|
|
| 복잡도 | 중간 (SQL 작성은 완료, `query()` 함수로 교체만 필요) |
|
|
| 우선순위 | 🟡 중간 (Phase 2.2) |
|
|
|
|
### 🎯 전환 목표
|
|
|
|
- ✅ **33개 모든 Prisma 호출을 `db.ts`의 `query()` 함수로 교체**
|
|
- 26개 `$queryRaw` → `query()` 또는 `queryOne()`
|
|
- 7개 ORM 메서드 → `query()` (SQL 새로 작성)
|
|
- 1개 `$transaction` → `transaction()`
|
|
- ✅ 트랜잭션 처리 정상 동작 확인
|
|
- ✅ 모든 단위 테스트 통과
|
|
- ✅ **Prisma import 완전 제거**
|
|
|
|
---
|
|
|
|
## 🔍 Prisma 사용 현황 분석
|
|
|
|
### 1. `$queryRaw` / `$queryRawUnsafe` 사용 (26개)
|
|
|
|
**현재 상태**: SQL은 이미 작성되어 있음 ✅
|
|
**전환 작업**: `prisma.$queryRaw` → `query()` 함수로 교체만 하면 됨
|
|
|
|
```typescript
|
|
// 기존
|
|
await prisma.$queryRaw`SELECT ...`;
|
|
await prisma.$queryRawUnsafe(sqlString, ...params);
|
|
|
|
// 전환 후
|
|
import { query } from "../database/db";
|
|
await query(`SELECT ...`);
|
|
await query(sqlString, params);
|
|
```
|
|
|
|
### 2. ORM 메서드 사용 (7개)
|
|
|
|
**현재 상태**: Prisma ORM 메서드 사용
|
|
**전환 작업**: SQL 작성 필요
|
|
|
|
#### 1. table_labels 관리 (2개)
|
|
|
|
```typescript
|
|
// Line 254: 테이블 라벨 UPSERT
|
|
await prisma.table_labels.upsert({
|
|
where: { table_name: tableName },
|
|
update: {},
|
|
create: { table_name, table_label, description }
|
|
});
|
|
|
|
// Line 437: 테이블 라벨 조회
|
|
await prisma.table_labels.findUnique({
|
|
where: { table_name: tableName },
|
|
select: { table_name, table_label, description, ... }
|
|
});
|
|
```
|
|
|
|
#### 2. column_labels 관리 (5개)
|
|
|
|
```typescript
|
|
// Line 323: 컬럼 라벨 UPSERT
|
|
await prisma.column_labels.upsert({
|
|
where: {
|
|
table_name_column_name: {
|
|
table_name: tableName,
|
|
column_name: columnName
|
|
}
|
|
},
|
|
update: { column_label, input_type, ... },
|
|
create: { table_name, column_name, ... }
|
|
});
|
|
|
|
// Line 481: 컬럼 라벨 조회
|
|
await prisma.column_labels.findUnique({
|
|
where: {
|
|
table_name_column_name: {
|
|
table_name: tableName,
|
|
column_name: columnName
|
|
}
|
|
},
|
|
select: { id, table_name, column_name, ... }
|
|
});
|
|
|
|
// Line 567: 컬럼 존재 확인
|
|
await prisma.column_labels.findFirst({
|
|
where: { table_name, column_name }
|
|
});
|
|
|
|
// Line 586: 컬럼 라벨 업데이트
|
|
await prisma.column_labels.update({
|
|
where: { id: existingColumn.id },
|
|
data: { web_type, detail_settings, ... }
|
|
});
|
|
|
|
// Line 610: 컬럼 라벨 생성
|
|
await prisma.column_labels.create({
|
|
data: { table_name, column_name, web_type, ... }
|
|
});
|
|
|
|
// Line 1003: 파일 타입 컬럼 조회
|
|
await prisma.column_labels.findMany({
|
|
where: { table_name, web_type: 'file' },
|
|
select: { column_name }
|
|
});
|
|
|
|
// Line 1382: 컬럼 웹타입 정보 조회
|
|
await prisma.column_labels.findFirst({
|
|
where: { table_name, column_name },
|
|
select: { web_type, code_category, ... }
|
|
});
|
|
|
|
// Line 2690: 컬럼 라벨 UPSERT (복제)
|
|
await prisma.column_labels.upsert({
|
|
where: {
|
|
table_name_column_name: { table_name, column_name }
|
|
},
|
|
update: { column_label, web_type, ... },
|
|
create: { table_name, column_name, ... }
|
|
});
|
|
```
|
|
|
|
#### 3. attach_file_info 관리 (2개)
|
|
|
|
```typescript
|
|
// Line 914: 파일 정보 조회
|
|
await prisma.attach_file_info.findMany({
|
|
where: { target_objid, doc_type, status: 'ACTIVE' },
|
|
select: { objid, real_file_name, file_size, ... },
|
|
orderBy: { regdate: 'desc' }
|
|
});
|
|
|
|
// Line 959: 파일 경로로 파일 정보 조회
|
|
await prisma.attach_file_info.findFirst({
|
|
where: { file_path, status: 'ACTIVE' },
|
|
select: { objid, real_file_name, ... }
|
|
});
|
|
```
|
|
|
|
#### 4. 트랜잭션 (1개)
|
|
|
|
```typescript
|
|
// Line 391: 전체 컬럼 설정 일괄 업데이트
|
|
await prisma.$transaction(async (tx) => {
|
|
await this.insertTableIfNotExists(tableName);
|
|
for (const columnSetting of columnSettings) {
|
|
await this.updateColumnSettings(tableName, columnName, columnSetting);
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 전환 예시
|
|
|
|
### 예시 1: table_labels UPSERT 전환
|
|
|
|
**기존 Prisma 코드:**
|
|
|
|
```typescript
|
|
await prisma.table_labels.upsert({
|
|
where: { table_name: tableName },
|
|
update: {},
|
|
create: {
|
|
table_name: tableName,
|
|
table_label: tableName,
|
|
description: "",
|
|
},
|
|
});
|
|
```
|
|
|
|
**새로운 Raw Query 코드:**
|
|
|
|
```typescript
|
|
import { query } from "../database/db";
|
|
|
|
await query(
|
|
`INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
|
|
VALUES ($1, $2, $3, NOW(), NOW())
|
|
ON CONFLICT (table_name) DO NOTHING`,
|
|
[tableName, tableName, ""]
|
|
);
|
|
```
|
|
|
|
### 예시 2: column_labels UPSERT 전환
|
|
|
|
**기존 Prisma 코드:**
|
|
|
|
```typescript
|
|
await prisma.column_labels.upsert({
|
|
where: {
|
|
table_name_column_name: {
|
|
table_name: tableName,
|
|
column_name: columnName,
|
|
},
|
|
},
|
|
update: {
|
|
column_label: settings.columnLabel,
|
|
input_type: settings.inputType,
|
|
detail_settings: settings.detailSettings,
|
|
updated_date: new Date(),
|
|
},
|
|
create: {
|
|
table_name: tableName,
|
|
column_name: columnName,
|
|
column_label: settings.columnLabel,
|
|
input_type: settings.inputType,
|
|
detail_settings: settings.detailSettings,
|
|
},
|
|
});
|
|
```
|
|
|
|
**새로운 Raw Query 코드:**
|
|
|
|
```typescript
|
|
await query(
|
|
`INSERT INTO column_labels (
|
|
table_name, column_name, column_label, input_type, detail_settings,
|
|
code_category, code_value, reference_table, reference_column,
|
|
display_column, display_order, is_visible, created_date, updated_date
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
|
|
ON CONFLICT (table_name, column_name)
|
|
DO UPDATE SET
|
|
column_label = EXCLUDED.column_label,
|
|
input_type = EXCLUDED.input_type,
|
|
detail_settings = EXCLUDED.detail_settings,
|
|
code_category = EXCLUDED.code_category,
|
|
code_value = EXCLUDED.code_value,
|
|
reference_table = EXCLUDED.reference_table,
|
|
reference_column = EXCLUDED.reference_column,
|
|
display_column = EXCLUDED.display_column,
|
|
display_order = EXCLUDED.display_order,
|
|
is_visible = EXCLUDED.is_visible,
|
|
updated_date = NOW()`,
|
|
[
|
|
tableName,
|
|
columnName,
|
|
settings.columnLabel,
|
|
settings.inputType,
|
|
settings.detailSettings,
|
|
settings.codeCategory,
|
|
settings.codeValue,
|
|
settings.referenceTable,
|
|
settings.referenceColumn,
|
|
settings.displayColumn,
|
|
settings.displayOrder || 0,
|
|
settings.isVisible !== undefined ? settings.isVisible : true,
|
|
]
|
|
);
|
|
```
|
|
|
|
### 예시 3: 트랜잭션 전환
|
|
|
|
**기존 Prisma 코드:**
|
|
|
|
```typescript
|
|
await prisma.$transaction(async (tx) => {
|
|
await this.insertTableIfNotExists(tableName);
|
|
for (const columnSetting of columnSettings) {
|
|
await this.updateColumnSettings(tableName, columnName, columnSetting);
|
|
}
|
|
});
|
|
```
|
|
|
|
**새로운 Raw Query 코드:**
|
|
|
|
```typescript
|
|
import { transaction } from "../database/db";
|
|
|
|
await transaction(async (client) => {
|
|
// 테이블 라벨 자동 추가
|
|
await client.query(
|
|
`INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
|
|
VALUES ($1, $2, $3, NOW(), NOW())
|
|
ON CONFLICT (table_name) DO NOTHING`,
|
|
[tableName, tableName, ""]
|
|
);
|
|
|
|
// 각 컬럼 설정 업데이트
|
|
for (const columnSetting of columnSettings) {
|
|
const columnName = columnSetting.columnName;
|
|
if (columnName) {
|
|
await client.query(
|
|
`INSERT INTO column_labels (...)
|
|
VALUES (...)
|
|
ON CONFLICT (table_name, column_name) DO UPDATE SET ...`,
|
|
[...]
|
|
);
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 테스트 계획
|
|
|
|
### 단위 테스트 (10개)
|
|
|
|
```typescript
|
|
describe("TableManagementService Raw Query 전환 테스트", () => {
|
|
describe("insertTableIfNotExists", () => {
|
|
test("테이블 라벨 UPSERT 성공", async () => { ... });
|
|
test("중복 테이블 처리", async () => { ... });
|
|
});
|
|
|
|
describe("updateColumnSettings", () => {
|
|
test("컬럼 설정 UPSERT 성공", async () => { ... });
|
|
test("기존 컬럼 업데이트", async () => { ... });
|
|
});
|
|
|
|
describe("getTableLabels", () => {
|
|
test("테이블 라벨 조회 성공", async () => { ... });
|
|
});
|
|
|
|
describe("getColumnLabels", () => {
|
|
test("컬럼 라벨 조회 성공", async () => { ... });
|
|
});
|
|
|
|
describe("updateAllColumnSettings", () => {
|
|
test("일괄 업데이트 성공 (트랜잭션)", async () => { ... });
|
|
test("부분 실패 시 롤백", async () => { ... });
|
|
});
|
|
|
|
describe("getFileInfoByColumnAndTarget", () => {
|
|
test("파일 정보 조회 성공", async () => { ... });
|
|
});
|
|
});
|
|
```
|
|
|
|
### 통합 테스트 (5개 시나리오)
|
|
|
|
```typescript
|
|
describe("테이블 관리 통합 테스트", () => {
|
|
test("테이블 라벨 생성 → 조회 → 수정", async () => { ... });
|
|
test("컬럼 라벨 생성 → 조회 → 수정", async () => { ... });
|
|
test("컬럼 일괄 설정 업데이트", async () => { ... });
|
|
test("파일 정보 조회 및 보강", async () => { ... });
|
|
test("트랜잭션 롤백 테스트", async () => { ... });
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 체크리스트
|
|
|
|
### 1단계: table_labels 전환 (2개 함수) ⏳ **진행 예정**
|
|
|
|
- [ ] `insertTableIfNotExists()` - UPSERT
|
|
- [ ] `getTableLabels()` - 조회
|
|
|
|
### 2단계: column_labels 전환 (5개 함수) ⏳ **진행 예정**
|
|
|
|
- [ ] `updateColumnSettings()` - UPSERT
|
|
- [ ] `getColumnLabels()` - 조회
|
|
- [ ] `updateColumnWebType()` - findFirst + update/create
|
|
- [ ] `getColumnWebTypeInfo()` - findFirst
|
|
- [ ] `updateColumnLabel()` - UPSERT (복제)
|
|
|
|
### 3단계: attach_file_info 전환 (2개 함수) ⏳ **진행 예정**
|
|
|
|
- [ ] `getFileInfoByColumnAndTarget()` - findMany
|
|
- [ ] `getFileInfoByPath()` - findFirst
|
|
|
|
### 4단계: 트랜잭션 전환 (1개 함수) ⏳ **진행 예정**
|
|
|
|
- [ ] `updateAllColumnSettings()` - 트랜잭션
|
|
|
|
### 5단계: 테스트 & 검증 ⏳ **진행 예정**
|
|
|
|
- [ ] 단위 테스트 작성 (10개)
|
|
- [ ] 통합 테스트 작성 (5개 시나리오)
|
|
- [ ] Prisma import 완전 제거 확인
|
|
- [ ] 성능 테스트
|
|
|
|
---
|
|
|
|
## 🎯 완료 기준
|
|
|
|
- [ ] **33개 모든 Prisma 호출을 Raw Query로 전환 완료**
|
|
- [ ] 26개 `$queryRaw` → `query()` 함수로 교체
|
|
- [ ] 7개 ORM 메서드 → `query()` 함수로 전환 (SQL 작성)
|
|
- [ ] **모든 TypeScript 컴파일 오류 해결**
|
|
- [ ] **트랜잭션 정상 동작 확인**
|
|
- [ ] **에러 처리 및 롤백 정상 동작**
|
|
- [ ] **모든 단위 테스트 통과 (10개)**
|
|
- [ ] **모든 통합 테스트 작성 완료 (5개 시나리오)**
|
|
- [ ] **`import prisma` 완전 제거 및 `import { query, transaction } from "../database/db"` 사용**
|
|
- [ ] **성능 저하 없음 (기존 대비 ±10% 이내)**
|
|
|
|
---
|
|
|
|
## 💡 특이사항
|
|
|
|
### SQL은 이미 대부분 작성되어 있음
|
|
|
|
이 서비스는 이미 79%가 `$queryRaw`를 사용하고 있어, **SQL 작성은 완료**되었습니다:
|
|
|
|
- ✅ `information_schema` 조회: SQL 작성 완료 (`$queryRaw` 사용 중)
|
|
- ✅ 동적 테이블 쿼리: SQL 작성 완료 (`$queryRawUnsafe` 사용 중)
|
|
- ✅ DDL 실행: SQL 작성 완료 (`$executeRaw` 사용 중)
|
|
- ⏳ **전환 작업**: `prisma.$queryRaw` → `query()` 함수로 **단순 교체만 필요**
|
|
- ⏳ CRUD 작업: 7개만 SQL 새로 작성 필요
|
|
|
|
### UPSERT 패턴 중요
|
|
|
|
대부분의 전환이 UPSERT 패턴이므로 PostgreSQL의 `ON CONFLICT` 구문을 활용합니다.
|
|
|
|
---
|
|
|
|
**작성일**: 2025-09-30
|
|
**예상 소요 시간**: 1-1.5일 (SQL은 79% 작성 완료, 함수 교체 작업 필요)
|
|
**담당자**: 백엔드 개발팀
|
|
**우선순위**: 🟡 중간 (Phase 2.2)
|
|
**상태**: ⏳ **진행 예정**
|
|
**특이사항**: SQL은 대부분 작성되어 있어 `prisma.$queryRaw` → `query()` 단순 교체 작업이 주요 작업
|