13 KiB
13 KiB
📋 Phase 3.16: Data Management Services Raw Query 전환 계획
📋 개요
데이터 관리 관련 서비스들은 총 18개의 Prisma 호출이 있으며, 동적 폼, 데이터 매핑, 데이터 서비스, 관리자 기능을 담당합니다.
📊 기본 정보
| 항목 | 내용 |
|---|---|
| 대상 서비스 | 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 → querygetTableColumns()- $queryRawUnsafe → querygetColumnWebTypes()- $queryRawUnsafe → querygetPrimaryKeys()- $queryRawUnsafe → queryperformInsert()- $queryRawUnsafe → queryperformUpdate()- $queryRawUnsafe → query
2. DataMappingService (5개)
getSourceData()- $queryRawUnsafe → queryexecuteInsert()- $executeRawUnsafe → queryexecuteUpsert()- $executeRawUnsafe → queryexecuteUpdate()- $executeRawUnsafe → querydisconnect()- 제거 (Raw Query는 disconnect 불필요)
3. DataService (4개)
getTableData()- $queryRawUnsafe → querycheckTableExists()- $queryRawUnsafe → querygetTableColumnsSimple()- $queryRawUnsafe → querygetColumnLabel()- $queryRawUnsafe → query
4. AdminService (3개)
getAdminMenuList()- $queryRaw → query (WITH RECURSIVE)getUserMenuList()- $queryRaw → query (WITH RECURSIVE)getMenuInfo()- findUnique → query (JOIN)
주요 기술적 해결 사항
-
변수명 충돌 해결
dataService.ts에서query변수 →sql변수로 변경query()함수와 로컬 변수 충돌 방지
-
WITH RECURSIVE 쿼리 전환
- Prisma의
$queryRaw템플릿 리터럴 → 일반 문자열 ${userLang}→$1파라미터 바인딩
- Prisma의
-
JOIN 쿼리 전환
- Prisma의
include옵션 →LEFT JOIN쿼리 - 관계 데이터를 단일 쿼리로 조회
- Prisma의
-
동적 쿼리 생성
- 동적 WHERE 조건 구성
- SQL 인젝션 방지 (컬럼명 검증)
- 동적 ORDER BY 처리
컴파일 상태
✅ TypeScript 컴파일 성공
✅ Linter 오류 없음
🔍 서비스별 상세 분석
1. EnhancedDynamicFormService (6개 호출, 786 라인)
주요 기능:
- 고급 동적 폼 관리
- 폼 검증 규칙
- 조건부 필드 표시
- 폼 템플릿 관리
예상 Prisma 호출:
getEnhancedForms()- 고급 폼 목록 조회getEnhancedForm()- 고급 폼 단건 조회createEnhancedForm()- 고급 폼 생성updateEnhancedForm()- 고급 폼 수정deleteEnhancedForm()- 고급 폼 삭제getFormValidationRules()- 검증 규칙 조회
기술적 고려사항:
- JSON 필드 (validation_rules, conditional_logic, field_config)
- 복잡한 검증 규칙
- 동적 필드 생성
- 조건부 표시 로직
2. DataMappingService (5개 호출, 575 라인)
주요 기능:
- 데이터 매핑 설정 관리
- 소스-타겟 필드 매핑
- 데이터 변환 규칙
- 매핑 실행
예상 Prisma 호출:
getDataMappings()- 매핑 설정 목록 조회getDataMapping()- 매핑 설정 단건 조회createDataMapping()- 매핑 설정 생성updateDataMapping()- 매핑 설정 수정deleteDataMapping()- 매핑 설정 삭제
기술적 고려사항:
- JSON 필드 (field_mappings, transformation_rules)
- 복잡한 변환 로직
- 매핑 검증
- 실행 이력 추적
3. DataService (4개 호출, 327 라인)
주요 기능:
- 동적 데이터 조회
- 데이터 필터링
- 데이터 정렬
- 데이터 집계
예상 Prisma 호출:
getDataByTable()- 테이블별 데이터 조회getDataById()- 데이터 단건 조회executeCustomQuery()- 커스텀 쿼리 실행getDataStatistics()- 데이터 통계 조회
기술적 고려사항:
- 동적 테이블 쿼리
- SQL 인젝션 방지
- 동적 WHERE 조건
- 집계 쿼리
4. AdminService (3개 호출, 374 라인)
주요 기능:
- 관리자 메뉴 관리
- 시스템 설정
- 사용자 관리
- 로그 조회
예상 Prisma 호출:
getAdminMenus()- 관리자 메뉴 조회getSystemSettings()- 시스템 설정 조회updateSystemSettings()- 시스템 설정 업데이트
기술적 고려사항:
- 메뉴 계층 구조
- 권한 기반 필터링
- JSON 설정 필드
- 캐싱
💡 통합 전환 전략
Phase 1: 단순 CRUD 전환 (12개)
EnhancedDynamicFormService (6개) + DataMappingService (5개) + AdminService (1개)
- 기본 CRUD 기능
- JSON 필드 처리
Phase 2: 동적 쿼리 전환 (4개)
DataService (4개)
- 동적 테이블 쿼리
- 보안 검증
Phase 3: 고급 기능 전환 (2개)
AdminService (2개)
- 시스템 설정
- 캐싱
💻 전환 예시
예시 1: 고급 폼 생성 (JSON 필드)
변경 전:
const form = await prisma.enhanced_forms.create({
data: {
form_code: formCode,
form_name: formName,
validation_rules: validationRules, // JSON
conditional_logic: conditionalLogic, // JSON
field_config: fieldConfig, // JSON
company_code: companyCode,
},
});
변경 후:
const form = await queryOne<any>(
`INSERT INTO enhanced_forms
(form_code, form_name, validation_rules, conditional_logic,
field_config, company_code, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING *`,
[
formCode,
formName,
JSON.stringify(validationRules),
JSON.stringify(conditionalLogic),
JSON.stringify(fieldConfig),
companyCode,
]
);
예시 2: 데이터 매핑 조회
변경 전:
const mappings = await prisma.data_mappings.findMany({
where: {
source_table: sourceTable,
target_table: targetTable,
is_active: true,
},
include: {
source_columns: true,
target_columns: true,
},
});
변경 후:
const mappings = await query<any>(
`SELECT
dm.*,
json_agg(DISTINCT jsonb_build_object(
'column_id', sc.column_id,
'column_name', sc.column_name
)) FILTER (WHERE sc.column_id IS NOT NULL) as source_columns,
json_agg(DISTINCT jsonb_build_object(
'column_id', tc.column_id,
'column_name', tc.column_name
)) FILTER (WHERE tc.column_id IS NOT NULL) as target_columns
FROM data_mappings dm
LEFT JOIN columns sc ON dm.mapping_id = sc.mapping_id AND sc.type = 'source'
LEFT JOIN columns tc ON dm.mapping_id = tc.mapping_id AND tc.type = 'target'
WHERE dm.source_table = $1
AND dm.target_table = $2
AND dm.is_active = $3
GROUP BY dm.mapping_id`,
[sourceTable, targetTable, true]
);
예시 3: 동적 테이블 쿼리 (DataService)
변경 전:
// Prisma로는 동적 테이블 쿼리 불가능
// 이미 $queryRawUnsafe 사용 중일 가능성
const data = await prisma.$queryRawUnsafe(
`SELECT * FROM ${tableName} WHERE ${whereClause}`,
...params
);
변경 후:
// SQL 인젝션 방지를 위한 테이블명 검증
const validTableName = validateTableName(tableName);
const data = await query<any>(
`SELECT * FROM ${validTableName} WHERE ${whereClause}`,
params
);
예시 4: 관리자 메뉴 조회 (계층 구조)
변경 전:
const menus = await prisma.admin_menus.findMany({
where: { is_active: true },
orderBy: { sort_order: "asc" },
include: {
children: {
orderBy: { sort_order: "asc" },
},
},
});
변경 후:
// 재귀 CTE를 사용한 계층 쿼리
const menus = await query<any>(
`WITH RECURSIVE menu_tree AS (
SELECT *, 0 as level, ARRAY[menu_id] as path
FROM admin_menus
WHERE parent_id IS NULL AND is_active = $1
UNION ALL
SELECT m.*, mt.level + 1, mt.path || m.menu_id
FROM admin_menus m
JOIN menu_tree mt ON m.parent_id = mt.menu_id
WHERE m.is_active = $1
)
SELECT * FROM menu_tree
ORDER BY path, sort_order`,
[true]
);
🔧 기술적 고려사항
1. JSON 필드 처리
// 복잡한 JSON 구조
interface ValidationRules {
required?: string[];
min?: Record<string, number>;
max?: Record<string, number>;
pattern?: Record<string, string>;
custom?: Array<{ field: string; rule: string }>;
}
// 저장 시
JSON.stringify(validationRules);
// 조회 후
const parsed =
typeof row.validation_rules === "string"
? JSON.parse(row.validation_rules)
: row.validation_rules;
2. 동적 테이블 쿼리 보안
// 테이블명 화이트리스트
const ALLOWED_TABLES = ["users", "products", "orders"];
function validateTableName(tableName: string): string {
if (!ALLOWED_TABLES.includes(tableName)) {
throw new Error("Invalid table name");
}
return tableName;
}
// 컬럼명 검증
function validateColumnName(columnName: string): string {
if (!/^[a-z_][a-z0-9_]*$/i.test(columnName)) {
throw new Error("Invalid column name");
}
return columnName;
}
3. 재귀 CTE (계층 구조)
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
)
SELECT * FROM hierarchy
4. JSON 집계 (관계 데이터)
SELECT
parent.*,
COALESCE(
json_agg(
jsonb_build_object('id', child.id, 'name', child.name)
) FILTER (WHERE child.id IS NOT NULL),
'[]'
) as children
FROM parent
LEFT JOIN child ON parent.id = child.parent_id
GROUP BY parent.id
📝 전환 체크리스트
EnhancedDynamicFormService (6개)
getEnhancedForms()- 목록 조회getEnhancedForm()- 단건 조회createEnhancedForm()- 생성 (JSON 필드)updateEnhancedForm()- 수정 (JSON 필드)deleteEnhancedForm()- 삭제getFormValidationRules()- 검증 규칙 조회
DataMappingService (5개)
getDataMappings()- 목록 조회getDataMapping()- 단건 조회createDataMapping()- 생성updateDataMapping()- 수정deleteDataMapping()- 삭제
DataService (4개)
getDataByTable()- 동적 테이블 조회getDataById()- 단건 조회executeCustomQuery()- 커스텀 쿼리getDataStatistics()- 통계 조회
AdminService (3개)
getAdminMenus()- 메뉴 조회 (재귀 CTE)getSystemSettings()- 시스템 설정 조회updateSystemSettings()- 시스템 설정 업데이트
공통 작업
- import 문 수정 (모든 서비스)
- Prisma import 완전 제거
- JSON 필드 처리 확인
- 보안 검증 (SQL 인젝션)
🧪 테스트 계획
단위 테스트 (18개)
- 각 Prisma 호출별 1개씩
통합 테스트 (6개)
- EnhancedDynamicFormService: 폼 생성 및 검증 테스트 (2개)
- DataMappingService: 매핑 설정 및 실행 테스트 (2개)
- DataService: 동적 쿼리 및 보안 테스트 (1개)
- AdminService: 메뉴 계층 구조 테스트 (1개)
보안 테스트
- SQL 인젝션 방지 테스트
- 테이블명 검증 테스트
- 컬럼명 검증 테스트
🎯 예상 난이도 및 소요 시간
- 난이도: ⭐⭐⭐⭐ (높음)
- JSON 필드 처리
- 동적 쿼리 보안
- 재귀 CTE
- JSON 집계
- 예상 소요 시간: 2.5~3시간
- Phase 1 (기본 CRUD): 1시간
- Phase 2 (동적 쿼리): 1시간
- Phase 3 (고급 기능): 0.5시간
- 테스트 및 문서화: 0.5시간
⚠️ 주의사항
보안 필수 체크리스트
- ✅ 동적 테이블명은 반드시 화이트리스트 검증
- ✅ 동적 컬럼명은 정규식으로 검증
- ✅ WHERE 절 파라미터는 반드시 바인딩
- ✅ JSON 필드는 파싱 에러 처리
- ✅ 재귀 쿼리는 깊이 제한 설정
성능 최적화
- JSON 필드 인덱싱 (GIN 인덱스)
- 재귀 쿼리 깊이 제한
- 집계 쿼리 최적화
- 필요시 캐싱 적용
상태: ⏳ 대기 중
특이사항: JSON 필드, 동적 쿼리, 재귀 CTE, 보안 검증 포함
⚠️ 주의: 동적 쿼리는 SQL 인젝션 방지가 매우 중요!