feat: Phase 3.7 LayoutService Raw Query 전환 완료
10개 Prisma 호출을 모두 Raw Query로 전환 - 레이아웃 목록 조회 (getLayouts - 복잡한 OR 조건, Promise.all) - 레이아웃 단건 조회 (getLayoutById - OR 조건) - 레이아웃 생성 (createLayout - JSON 필드) - 레이아웃 수정 (updateLayout - 동적 UPDATE, 10개 필드) - 레이아웃 삭제 (deleteLayout - Soft Delete) - 레이아웃 복제 (duplicateLayout - 기존 함수 재사용) - 카테고리별 통계 (getLayoutCountsByCategory - GROUP BY) - 코드 자동 생성 (generateLayoutCode - LIKE 검색) 주요 기술적 해결: - 복잡한 OR 조건 처리 (company_code OR is_public) - 동적 WHERE 조건 생성 (ILIKE 다중 검색) - 동적 UPDATE 쿼리 (10개 필드 조건부 업데이트) - JSON 필드 처리 (default_size, layout_config, zones_config) - GROUP BY 통계 쿼리 (카테고리별 개수) - LIKE 검색 (코드 생성 시 패턴 검색) - Promise.all 병렬 쿼리 (목록 + 개수 동시 조회) - safeJSONStringify 헬퍼 함수 활용 TypeScript 컴파일 성공 Prisma import 완전 제거 Phase 3 진행률: 97/162 (59.9%) 전체 진행률: 348/444 (78.4%)
This commit is contained in:
parent
45ec38790b
commit
4c20d93c87
|
|
@ -128,7 +128,7 @@ backend-node/ (루트)
|
|||
- `commonCodeService.ts` (0개) - ✅ **전환 완료** (Phase 3.4)
|
||||
- `dataflowDiagramService.ts` (0개) - ✅ **전환 완료** (Phase 3.5)
|
||||
- `collectionService.ts` (0개) - ✅ **전환 완료** (Phase 3.6)
|
||||
- `layoutService.ts` (10개) - 레이아웃 관리
|
||||
- `layoutService.ts` (0개) - ✅ **전환 완료** (Phase 3.7)
|
||||
- `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견
|
||||
- `templateStandardService.ts` (9개) - 템플릿 표준
|
||||
- `eventTriggerService.ts` (6개) - JSON 검색 쿼리
|
||||
|
|
@ -1183,12 +1183,22 @@ describe("Performance Benchmarks", () => {
|
|||
- [x] 비동기 작업 처리 (setTimeout 내 query 사용)
|
||||
- [x] TypeScript 컴파일 성공
|
||||
- [x] Prisma import 완전 제거
|
||||
- [x] **LayoutService 전환 (10개)** ✅ **완료** (Phase 3.7)
|
||||
- [x] 10개 Prisma 호출 전환 완료 (레이아웃 CRUD, 통계)
|
||||
- [x] 복잡한 OR 조건 처리 (company_code OR is_public)
|
||||
- [x] 동적 WHERE 조건 생성 (ILIKE 다중 검색)
|
||||
- [x] 동적 UPDATE 쿼리 (10개 필드 조건부 업데이트)
|
||||
- [x] JSON 필드 처리 (default_size, layout_config, zones_config)
|
||||
- [x] GROUP BY 통계 쿼리 (카테고리별 개수)
|
||||
- [x] LIKE 검색 (코드 생성 시 패턴 검색)
|
||||
- [x] Promise.all 병렬 쿼리 (목록 + 개수)
|
||||
- [x] TypeScript 컴파일 성공
|
||||
- [x] Prisma import 완전 제거
|
||||
- [ ] 배치 관련 서비스 전환 (26개) ⭐ 대규모 신규 발견
|
||||
- [ ] BatchExternalDbService (8개)
|
||||
- [ ] BatchExecutionLogService (7개), BatchManagementService (5개)
|
||||
- [ ] BatchSchedulerService (4개)
|
||||
- [ ] 표준 관리 서비스 전환 (16개)
|
||||
- [ ] LayoutService (10개) - [계획서](PHASE3.7_LAYOUT_SERVICE_MIGRATION.md)
|
||||
- [ ] 표준 관리 서비스 전환 (6개)
|
||||
- [ ] TemplateStandardService (6개) - [계획서](PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md)
|
||||
- [ ] 데이터플로우 관련 서비스 (6개) ⭐ 신규 발견
|
||||
- [ ] DataflowControlService (6개)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import prisma from "../config/database";
|
||||
import { query, queryOne } from "../database/db";
|
||||
import {
|
||||
CreateLayoutRequest,
|
||||
UpdateLayoutRequest,
|
||||
|
|
@ -77,42 +76,59 @@ export class LayoutService {
|
|||
|
||||
const skip = (page - 1) * size;
|
||||
|
||||
// 검색 조건 구성
|
||||
const where: any = {
|
||||
is_active: "Y",
|
||||
OR: [
|
||||
{ company_code: companyCode },
|
||||
...(includePublic ? [{ is_public: "Y" }] : []),
|
||||
],
|
||||
};
|
||||
// 동적 WHERE 조건 구성
|
||||
const whereConditions: string[] = ["is_active = $1"];
|
||||
const values: any[] = ["Y"];
|
||||
let paramIndex = 2;
|
||||
|
||||
// company_code OR is_public 조건
|
||||
if (includePublic) {
|
||||
whereConditions.push(
|
||||
`(company_code = $${paramIndex} OR is_public = $${paramIndex + 1})`
|
||||
);
|
||||
values.push(companyCode, "Y");
|
||||
paramIndex += 2;
|
||||
} else {
|
||||
whereConditions.push(`company_code = $${paramIndex++}`);
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
if (category) {
|
||||
where.category = category;
|
||||
whereConditions.push(`category = $${paramIndex++}`);
|
||||
values.push(category);
|
||||
}
|
||||
|
||||
if (layoutType) {
|
||||
where.layout_type = layoutType;
|
||||
whereConditions.push(`layout_type = $${paramIndex++}`);
|
||||
values.push(layoutType);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
where.OR = [
|
||||
...where.OR,
|
||||
{ layout_name: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ layout_name_eng: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ description: { contains: searchTerm, mode: "insensitive" } },
|
||||
];
|
||||
whereConditions.push(
|
||||
`(layout_name ILIKE $${paramIndex} OR layout_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
|
||||
);
|
||||
values.push(`%${searchTerm}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.layout_standards.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: size,
|
||||
orderBy: [{ sort_order: "asc" }, { created_date: "desc" }],
|
||||
}),
|
||||
prisma.layout_standards.count({ where }),
|
||||
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
|
||||
|
||||
const [data, countResult] = await Promise.all([
|
||||
query<any>(
|
||||
`SELECT * FROM layout_standards
|
||||
${whereClause}
|
||||
ORDER BY sort_order ASC, created_date DESC
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
||||
[...values, size, skip]
|
||||
),
|
||||
queryOne<{ count: string }>(
|
||||
`SELECT COUNT(*) as count FROM layout_standards ${whereClause}`,
|
||||
values
|
||||
),
|
||||
]);
|
||||
|
||||
const total = parseInt(countResult?.count || "0");
|
||||
|
||||
return {
|
||||
data: data.map(
|
||||
(layout) =>
|
||||
|
|
@ -149,13 +165,13 @@ export class LayoutService {
|
|||
layoutCode: string,
|
||||
companyCode: string
|
||||
): Promise<LayoutStandard | null> {
|
||||
const layout = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: layoutCode,
|
||||
is_active: "Y",
|
||||
OR: [{ company_code: companyCode }, { is_public: "Y" }],
|
||||
},
|
||||
});
|
||||
const layout = await queryOne<any>(
|
||||
`SELECT * FROM layout_standards
|
||||
WHERE layout_code = $1 AND is_active = $2
|
||||
AND (company_code = $3 OR is_public = $4)
|
||||
LIMIT 1`,
|
||||
[layoutCode, "Y", companyCode, "Y"]
|
||||
);
|
||||
|
||||
if (!layout) return null;
|
||||
|
||||
|
|
@ -196,24 +212,31 @@ export class LayoutService {
|
|||
companyCode
|
||||
);
|
||||
|
||||
const layout = await prisma.layout_standards.create({
|
||||
data: {
|
||||
layout_code: layoutCode,
|
||||
layout_name: request.layoutName,
|
||||
layout_name_eng: request.layoutNameEng,
|
||||
description: request.description,
|
||||
layout_type: request.layoutType,
|
||||
category: request.category,
|
||||
icon_name: request.iconName,
|
||||
default_size: safeJSONStringify(request.defaultSize) as any,
|
||||
layout_config: safeJSONStringify(request.layoutConfig) as any,
|
||||
zones_config: safeJSONStringify(request.zonesConfig) as any,
|
||||
is_public: request.isPublic ? "Y" : "N",
|
||||
company_code: companyCode,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
},
|
||||
});
|
||||
const layout = await queryOne<any>(
|
||||
`INSERT INTO layout_standards
|
||||
(layout_code, layout_name, layout_name_eng, description, layout_type, category,
|
||||
icon_name, default_size, layout_config, zones_config, is_public, is_active,
|
||||
company_code, created_by, updated_by, created_date, updated_date, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW(), 0)
|
||||
RETURNING *`,
|
||||
[
|
||||
layoutCode,
|
||||
request.layoutName,
|
||||
request.layoutNameEng,
|
||||
request.description,
|
||||
request.layoutType,
|
||||
request.category,
|
||||
request.iconName,
|
||||
safeJSONStringify(request.defaultSize),
|
||||
safeJSONStringify(request.layoutConfig),
|
||||
safeJSONStringify(request.zonesConfig),
|
||||
request.isPublic ? "Y" : "N",
|
||||
"Y",
|
||||
companyCode,
|
||||
userId,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
return this.mapToLayoutStandard(layout);
|
||||
}
|
||||
|
|
@ -227,47 +250,69 @@ export class LayoutService {
|
|||
userId: string
|
||||
): Promise<LayoutStandard | null> {
|
||||
// 수정 권한 확인
|
||||
const existing = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: request.layoutCode,
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
const existing = await queryOne<any>(
|
||||
`SELECT * FROM layout_standards
|
||||
WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`,
|
||||
[request.layoutCode, companyCode, "Y"]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("레이아웃을 찾을 수 없거나 수정 권한이 없습니다.");
|
||||
}
|
||||
|
||||
const updateData: any = {
|
||||
updated_by: userId,
|
||||
updated_date: new Date(),
|
||||
};
|
||||
// 동적 UPDATE 쿼리 생성
|
||||
const updateFields: string[] = ["updated_by = $1", "updated_date = NOW()"];
|
||||
const values: any[] = [userId];
|
||||
let paramIndex = 2;
|
||||
|
||||
// 수정할 필드만 업데이트
|
||||
if (request.layoutName !== undefined)
|
||||
updateData.layout_name = request.layoutName;
|
||||
if (request.layoutNameEng !== undefined)
|
||||
updateData.layout_name_eng = request.layoutNameEng;
|
||||
if (request.description !== undefined)
|
||||
updateData.description = request.description;
|
||||
if (request.layoutType !== undefined)
|
||||
updateData.layout_type = request.layoutType;
|
||||
if (request.category !== undefined) updateData.category = request.category;
|
||||
if (request.iconName !== undefined) updateData.icon_name = request.iconName;
|
||||
if (request.defaultSize !== undefined)
|
||||
updateData.default_size = safeJSONStringify(request.defaultSize) as any;
|
||||
if (request.layoutConfig !== undefined)
|
||||
updateData.layout_config = safeJSONStringify(request.layoutConfig) as any;
|
||||
if (request.zonesConfig !== undefined)
|
||||
updateData.zones_config = safeJSONStringify(request.zonesConfig) as any;
|
||||
if (request.isPublic !== undefined)
|
||||
updateData.is_public = request.isPublic ? "Y" : "N";
|
||||
if (request.layoutName !== undefined) {
|
||||
updateFields.push(`layout_name = $${paramIndex++}`);
|
||||
values.push(request.layoutName);
|
||||
}
|
||||
if (request.layoutNameEng !== undefined) {
|
||||
updateFields.push(`layout_name_eng = $${paramIndex++}`);
|
||||
values.push(request.layoutNameEng);
|
||||
}
|
||||
if (request.description !== undefined) {
|
||||
updateFields.push(`description = $${paramIndex++}`);
|
||||
values.push(request.description);
|
||||
}
|
||||
if (request.layoutType !== undefined) {
|
||||
updateFields.push(`layout_type = $${paramIndex++}`);
|
||||
values.push(request.layoutType);
|
||||
}
|
||||
if (request.category !== undefined) {
|
||||
updateFields.push(`category = $${paramIndex++}`);
|
||||
values.push(request.category);
|
||||
}
|
||||
if (request.iconName !== undefined) {
|
||||
updateFields.push(`icon_name = $${paramIndex++}`);
|
||||
values.push(request.iconName);
|
||||
}
|
||||
if (request.defaultSize !== undefined) {
|
||||
updateFields.push(`default_size = $${paramIndex++}`);
|
||||
values.push(safeJSONStringify(request.defaultSize));
|
||||
}
|
||||
if (request.layoutConfig !== undefined) {
|
||||
updateFields.push(`layout_config = $${paramIndex++}`);
|
||||
values.push(safeJSONStringify(request.layoutConfig));
|
||||
}
|
||||
if (request.zonesConfig !== undefined) {
|
||||
updateFields.push(`zones_config = $${paramIndex++}`);
|
||||
values.push(safeJSONStringify(request.zonesConfig));
|
||||
}
|
||||
if (request.isPublic !== undefined) {
|
||||
updateFields.push(`is_public = $${paramIndex++}`);
|
||||
values.push(request.isPublic ? "Y" : "N");
|
||||
}
|
||||
|
||||
const updated = await prisma.layout_standards.update({
|
||||
where: { layout_code: request.layoutCode },
|
||||
data: updateData,
|
||||
});
|
||||
const updated = await queryOne<any>(
|
||||
`UPDATE layout_standards
|
||||
SET ${updateFields.join(", ")}
|
||||
WHERE layout_code = $${paramIndex}
|
||||
RETURNING *`,
|
||||
[...values, request.layoutCode]
|
||||
);
|
||||
|
||||
return this.mapToLayoutStandard(updated);
|
||||
}
|
||||
|
|
@ -280,26 +325,22 @@ export class LayoutService {
|
|||
companyCode: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
const existing = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: layoutCode,
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
const existing = await queryOne<any>(
|
||||
`SELECT * FROM layout_standards
|
||||
WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`,
|
||||
[layoutCode, companyCode, "Y"]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("레이아웃을 찾을 수 없거나 삭제 권한이 없습니다.");
|
||||
}
|
||||
|
||||
await prisma.layout_standards.update({
|
||||
where: { layout_code: layoutCode },
|
||||
data: {
|
||||
is_active: "N",
|
||||
updated_by: userId,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
await query(
|
||||
`UPDATE layout_standards
|
||||
SET is_active = $1, updated_by = $2, updated_date = NOW()
|
||||
WHERE layout_code = $3`,
|
||||
["N", userId, layoutCode]
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -342,20 +383,17 @@ export class LayoutService {
|
|||
async getLayoutCountsByCategory(
|
||||
companyCode: string
|
||||
): Promise<Record<string, number>> {
|
||||
const counts = await prisma.layout_standards.groupBy({
|
||||
by: ["category"],
|
||||
_count: {
|
||||
layout_code: true,
|
||||
},
|
||||
where: {
|
||||
is_active: "Y",
|
||||
OR: [{ company_code: companyCode }, { is_public: "Y" }],
|
||||
},
|
||||
});
|
||||
const counts = await query<{ category: string; count: string }>(
|
||||
`SELECT category, COUNT(*) as count
|
||||
FROM layout_standards
|
||||
WHERE is_active = $1 AND (company_code = $2 OR is_public = $3)
|
||||
GROUP BY category`,
|
||||
["Y", companyCode, "Y"]
|
||||
);
|
||||
|
||||
return counts.reduce(
|
||||
(acc: Record<string, number>, item: any) => {
|
||||
acc[item.category] = item._count.layout_code;
|
||||
acc[item.category] = parseInt(item.count);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
|
|
@ -370,16 +408,11 @@ export class LayoutService {
|
|||
companyCode: string
|
||||
): Promise<string> {
|
||||
const prefix = `${layoutType.toUpperCase()}_${companyCode}`;
|
||||
const existingCodes = await prisma.layout_standards.findMany({
|
||||
where: {
|
||||
layout_code: {
|
||||
startsWith: prefix,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
layout_code: true,
|
||||
},
|
||||
});
|
||||
const existingCodes = await query<{ layout_code: string }>(
|
||||
`SELECT layout_code FROM layout_standards
|
||||
WHERE layout_code LIKE $1`,
|
||||
[`${prefix}%`]
|
||||
);
|
||||
|
||||
const maxNumber = existingCodes.reduce((max: number, item: any) => {
|
||||
const match = item.layout_code.match(/_(\d+)$/);
|
||||
|
|
|
|||
Loading…
Reference in New Issue