Files
vexplor/PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md
kjs 16d4ba4a51 feat: Phase 3.9 TemplateStandardService Raw Query 전환 완료
7개 Prisma 호출을 모두 Raw Query로 전환
- 템플릿 목록 조회 (getTemplates - 복잡한 OR 조건, Promise.all)
- 템플릿 단건 조회 (getTemplate)
- 템플릿 생성 (createTemplate - 중복 검사)
- 템플릿 수정 (updateTemplate - 동적 UPDATE, 11개 필드)
- 템플릿 삭제 (deleteTemplate)
- 정렬 순서 일괄 업데이트 (updateSortOrder - Promise.all)
- 카테고리 목록 조회 (getCategories - DISTINCT)

주요 기술적 해결:
- 복잡한 OR 조건 처리 (is_public OR company_code)
- 동적 WHERE 조건 생성 (ILIKE 다중 검색)
- 동적 UPDATE 쿼리 (11개 필드 조건부 업데이트)
- DISTINCT 쿼리 (카테고리 목록)
- Promise.all 병렬 쿼리 (목록 + 개수 동시 조회)
- Promise.all 병렬 업데이트 (정렬 순서 일괄 업데이트)

TypeScript 컴파일 성공
Prisma import 완전 제거

Phase 3 진행률: 114/162 (70.4%)
전체 진행률: 365/444 (82.2%)
2025-10-01 11:40:48 +09:00

10 KiB

📋 Phase 3.9: TemplateStandardService Raw Query 전환 계획

📋 개요

TemplateStandardService는 6개의 Prisma 호출이 있으며, 템플릿 표준 관리를 담당하는 서비스입니다.

📊 기본 정보

항목 내용
파일 위치 backend-node/src/services/templateStandardService.ts
파일 크기 395 라인
Prisma 호출 6개
현재 진행률 7/7 (100%) 전환 완료
복잡도 낮음 (기본 CRUD + DISTINCT)
우선순위 🟢 낮음 (Phase 3.9)
상태 완료

🎯 전환 목표

  • 7개 모든 Prisma 호출을 db.tsquery(), queryOne() 함수로 교체
  • 템플릿 CRUD 기능 정상 동작
  • DISTINCT 쿼리 전환
  • Promise.all 병렬 쿼리 (목록 + 개수)
  • 동적 UPDATE 쿼리 (11개 필드)
  • TypeScript 컴파일 성공
  • Prisma import 완전 제거

🔍 Prisma 사용 현황 분석

주요 Prisma 호출 (6개)

1. getTemplateByCode() - 템플릿 단건 조회

// Line 76
return await prisma.template_standards.findUnique({
  where: {
    template_code: templateCode,
    company_code: companyCode,
  },
});

2. createTemplate() - 템플릿 생성

// Line 86
const existing = await prisma.template_standards.findUnique({
  where: {
    template_code: data.template_code,
    company_code: data.company_code,
  },
});

// Line 96
return await prisma.template_standards.create({
  data: {
    ...data,
    created_date: new Date(),
    updated_date: new Date(),
  },
});

3. updateTemplate() - 템플릿 수정

// Line 164
return await prisma.template_standards.update({
  where: {
    template_code_company_code: {
      template_code: templateCode,
      company_code: companyCode,
    },
  },
  data: {
    ...data,
    updated_date: new Date(),
  },
});

4. deleteTemplate() - 템플릿 삭제

// Line 181
await prisma.template_standards.delete({
  where: {
    template_code_company_code: {
      template_code: templateCode,
      company_code: companyCode,
    },
  },
});

5. getTemplateCategories() - 카테고리 목록 (DISTINCT)

// Line 262
const categories = await prisma.template_standards.findMany({
  where: {
    company_code: companyCode,
  },
  select: {
    category: true,
  },
  distinct: ["category"],
});

📝 전환 계획

1단계: 기본 CRUD 전환 (4개 함수)

함수 목록:

  • getTemplateByCode() - 단건 조회 (findUnique)
  • createTemplate() - 생성 (findUnique + create)
  • updateTemplate() - 수정 (update)
  • deleteTemplate() - 삭제 (delete)

2단계: 추가 기능 전환 (1개 함수)

함수 목록:

  • getTemplateCategories() - 카테고리 목록 (findMany + distinct)

💻 전환 예시

예시 1: 복합 키 조회

// 기존 Prisma
return await prisma.template_standards.findUnique({
  where: {
    template_code: templateCode,
    company_code: companyCode,
  },
});

// 전환 후
import { queryOne } from "../database/db";

return await queryOne<any>(
  `SELECT * FROM template_standards 
   WHERE template_code = $1 AND company_code = $2`,
  [templateCode, companyCode]
);

예시 2: 중복 확인 후 생성

// 기존 Prisma
const existing = await prisma.template_standards.findUnique({
  where: {
    template_code: data.template_code,
    company_code: data.company_code,
  },
});

if (existing) {
  throw new Error("이미 존재하는 템플릿 코드입니다.");
}

return await prisma.template_standards.create({
  data: {
    ...data,
    created_date: new Date(),
    updated_date: new Date(),
  },
});

// 전환 후
const existing = await queryOne<any>(
  `SELECT * FROM template_standards 
   WHERE template_code = $1 AND company_code = $2`,
  [data.template_code, data.company_code]
);

if (existing) {
  throw new Error("이미 존재하는 템플릿 코드입니다.");
}

return await queryOne<any>(
  `INSERT INTO template_standards 
   (template_code, template_name, category, template_type, layout_config,
    description, is_active, company_code, created_by, updated_by, 
    created_date, updated_date)
   VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())
   RETURNING *`,
  [
    data.template_code,
    data.template_name,
    data.category,
    data.template_type,
    JSON.stringify(data.layout_config),
    data.description,
    data.is_active,
    data.company_code,
    data.created_by,
    data.updated_by,
  ]
);

예시 3: 복합 키 UPDATE

// 기존 Prisma
return await prisma.template_standards.update({
  where: {
    template_code_company_code: {
      template_code: templateCode,
      company_code: companyCode,
    },
  },
  data: {
    ...data,
    updated_date: new Date(),
  },
});

// 전환 후
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = ["updated_date = NOW()"];
const values: any[] = [];
let paramIndex = 1;

if (data.template_name !== undefined) {
  updateFields.push(`template_name = $${paramIndex++}`);
  values.push(data.template_name);
}
if (data.category !== undefined) {
  updateFields.push(`category = $${paramIndex++}`);
  values.push(data.category);
}
if (data.template_type !== undefined) {
  updateFields.push(`template_type = $${paramIndex++}`);
  values.push(data.template_type);
}
if (data.layout_config !== undefined) {
  updateFields.push(`layout_config = $${paramIndex++}`);
  values.push(JSON.stringify(data.layout_config));
}
if (data.description !== undefined) {
  updateFields.push(`description = $${paramIndex++}`);
  values.push(data.description);
}
if (data.is_active !== undefined) {
  updateFields.push(`is_active = $${paramIndex++}`);
  values.push(data.is_active);
}
if (data.updated_by !== undefined) {
  updateFields.push(`updated_by = $${paramIndex++}`);
  values.push(data.updated_by);
}

return await queryOne<any>(
  `UPDATE template_standards 
   SET ${updateFields.join(", ")}
   WHERE template_code = $${paramIndex++} AND company_code = $${paramIndex}
   RETURNING *`,
  [...values, templateCode, companyCode]
);

예시 4: 복합 키 DELETE

// 기존 Prisma
await prisma.template_standards.delete({
  where: {
    template_code_company_code: {
      template_code: templateCode,
      company_code: companyCode,
    },
  },
});

// 전환 후
import { query } from "../database/db";

await query(
  `DELETE FROM template_standards 
   WHERE template_code = $1 AND company_code = $2`,
  [templateCode, companyCode]
);

예시 5: DISTINCT 쿼리

// 기존 Prisma
const categories = await prisma.template_standards.findMany({
  where: {
    company_code: companyCode,
  },
  select: {
    category: true,
  },
  distinct: ["category"],
});

return categories
  .map((c) => c.category)
  .filter((c): c is string => c !== null && c !== undefined)
  .sort();

// 전환 후
const categories = await query<{ category: string }>(
  `SELECT DISTINCT category 
   FROM template_standards 
   WHERE company_code = $1 AND category IS NOT NULL
   ORDER BY category ASC`,
  [companyCode]
);

return categories.map((c) => c.category);

완료 기준

  • 6개 모든 Prisma 호출을 Raw Query로 전환 완료
  • 복합 기본 키 처리 (template_code + company_code)
  • 동적 UPDATE 쿼리 생성
  • DISTINCT 쿼리 전환
  • JSON 필드 처리 (layout_config)
  • 모든 TypeScript 컴파일 오류 해결
  • import prisma 완전 제거
  • 모든 단위 테스트 통과 (6개)
  • 통합 테스트 작성 완료 (2개 시나리오)

🔧 주요 기술적 과제

1. 복합 기본 키

template_standards 테이블은 (template_code, company_code) 복합 기본 키를 사용합니다.

  • WHERE 절에서 두 컬럼 모두 지정 필요
  • Prisma의 template_code_company_code 표현식을 template_code = $1 AND company_code = $2로 변환

2. JSON 필드

layout_config 필드는 JSON 타입으로, INSERT/UPDATE 시 JSON.stringify() 필요합니다.

3. DISTINCT + NULL 제외

카테고리 목록 조회 시 DISTINCT 사용하며, NULL 값은 WHERE category IS NOT NULL로 제외합니다.


📋 체크리스트

코드 전환

  • import 문 수정 (prismaquery, queryOne)
  • getTemplateByCode() - findUnique → queryOne (복합 키)
  • createTemplate() - findUnique + create → queryOne (중복 확인 + INSERT)
  • updateTemplate() - update → queryOne (동적 UPDATE, 복합 키)
  • deleteTemplate() - delete → query (복합 키)
  • getTemplateCategories() - findMany + distinct → query (DISTINCT)
  • JSON 필드 처리 (layout_config)
  • Prisma import 완전 제거

테스트

  • 단위 테스트 작성 (6개)
  • 통합 테스트 작성 (2개)
  • TypeScript 컴파일 성공
  • 성능 벤치마크 테스트

💡 특이사항

복합 기본 키 패턴

이 서비스는 (template_code, company_code) 복합 기본 키를 사용하므로, 모든 조회/수정/삭제 작업에서 두 컬럼을 모두 WHERE 조건에 포함해야 합니다.

JSON 레이아웃 설정

layout_config 필드는 템플릿의 레이아웃 설정을 JSON 형태로 저장합니다. Raw Query 전환 시 JSON.stringify()를 사용하여 문자열로 변환해야 합니다.

카테고리 관리

템플릿은 카테고리별로 분류되며, getTemplateCategories() 메서드로 고유한 카테고리 목록을 조회할 수 있습니다.


작성일: 2025-10-01
예상 소요 시간: 45분
담당자: 백엔드 개발팀
우선순위: 🟢 낮음 (Phase 3.9)
상태: 대기 중
특이사항: 복합 기본 키, JSON 필드, DISTINCT 쿼리 포함