ERP-node/PHASE3.13_ENTITY_JOIN_SERVI...

8.2 KiB

📋 Phase 3.13: EntityJoinService Raw Query 전환 계획

📋 개요

EntityJoinService는 5개의 Prisma 호출이 있으며, 엔티티 간 조인 관계 관리를 담당하는 서비스입니다.

📊 기본 정보

항목 내용
파일 위치 backend-node/src/services/entityJoinService.ts
파일 크기 575 라인
Prisma 호출 0개 (전환 완료)
현재 진행률 5/5 (100%) 전환 완료
복잡도 중간 (조인 쿼리, 관계 설정)
우선순위 🟡 중간 (Phase 3.13)
상태 완료

🎯 전환 목표

  • 5개 모든 Prisma 호출을 db.tsquery(), queryOne() 함수로 교체
  • 엔티티 조인 설정 CRUD 기능 정상 동작
  • 복잡한 조인 쿼리 전환 (LEFT JOIN, INNER JOIN)
  • 조인 유효성 검증
  • TypeScript 컴파일 성공
  • Prisma import 완전 제거

🔍 예상 Prisma 사용 패턴

주요 기능 (5개 예상)

1. 엔티티 조인 목록 조회

  • findMany with filters
  • 동적 WHERE 조건
  • 페이징, 정렬

2. 엔티티 조인 단건 조회

  • findUnique or findFirst
  • join_id 기준

3. 엔티티 조인 생성

  • create
  • 조인 유효성 검증

4. 엔티티 조인 수정

  • update
  • 동적 UPDATE 쿼리

5. 엔티티 조인 삭제

  • delete

💡 전환 전략

1단계: 기본 CRUD 전환 (5개)

  • getEntityJoins() - 목록 조회
  • getEntityJoin() - 단건 조회
  • createEntityJoin() - 생성
  • updateEntityJoin() - 수정
  • deleteEntityJoin() - 삭제

💻 전환 예시

예시 1: 조인 설정 조회 (LEFT JOIN으로 테이블 정보 포함)

변경 전:

const joins = await prisma.entity_joins.findMany({
  where: {
    company_code: companyCode,
    is_active: true,
  },
  include: {
    source_table: true,
    target_table: true,
  },
  orderBy: { created_at: "desc" },
});

변경 후:

const joins = await query<any>(
  `SELECT 
     ej.*,
     st.table_name as source_table_name,
     st.table_label as source_table_label,
     tt.table_name as target_table_name,
     tt.table_label as target_table_label
   FROM entity_joins ej
   LEFT JOIN tables st ON ej.source_table_id = st.table_id
   LEFT JOIN tables tt ON ej.target_table_id = tt.table_id
   WHERE ej.company_code = $1 AND ej.is_active = $2
   ORDER BY ej.created_at DESC`,
  [companyCode, true]
);

예시 2: 조인 생성 (유효성 검증 포함)

변경 전:

// 조인 유효성 검증
const sourceTable = await prisma.tables.findUnique({
  where: { table_id: sourceTableId },
});

const targetTable = await prisma.tables.findUnique({
  where: { table_id: targetTableId },
});

if (!sourceTable || !targetTable) {
  throw new Error("Invalid table references");
}

// 조인 생성
const join = await prisma.entity_joins.create({
  data: {
    source_table_id: sourceTableId,
    target_table_id: targetTableId,
    join_type: joinType,
    join_condition: joinCondition,
    company_code: companyCode,
  },
});

변경 후:

// 조인 유효성 검증 (Promise.all로 병렬 실행)
const [sourceTable, targetTable] = await Promise.all([
  queryOne<any>(`SELECT * FROM tables WHERE table_id = $1`, [sourceTableId]),
  queryOne<any>(`SELECT * FROM tables WHERE table_id = $1`, [targetTableId]),
]);

if (!sourceTable || !targetTable) {
  throw new Error("Invalid table references");
}

// 조인 생성
const join = await queryOne<any>(
  `INSERT INTO entity_joins 
   (source_table_id, target_table_id, join_type, join_condition, 
    company_code, created_at, updated_at)
   VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
   RETURNING *`,
  [sourceTableId, targetTableId, joinType, joinCondition, companyCode]
);

예시 3: 조인 수정

변경 전:

const join = await prisma.entity_joins.update({
  where: { join_id: joinId },
  data: {
    join_type: joinType,
    join_condition: joinCondition,
    is_active: isActive,
  },
});

변경 후:

const updateFields: string[] = ["updated_at = NOW()"];
const values: any[] = [];
let paramIndex = 1;

if (joinType !== undefined) {
  updateFields.push(`join_type = $${paramIndex++}`);
  values.push(joinType);
}

if (joinCondition !== undefined) {
  updateFields.push(`join_condition = $${paramIndex++}`);
  values.push(joinCondition);
}

if (isActive !== undefined) {
  updateFields.push(`is_active = $${paramIndex++}`);
  values.push(isActive);
}

const join = await queryOne<any>(
  `UPDATE entity_joins 
   SET ${updateFields.join(", ")}
   WHERE join_id = $${paramIndex}
   RETURNING *`,
  [...values, joinId]
);

🔧 기술적 고려사항

1. 조인 타입 검증

const VALID_JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "FULL"];
if (!VALID_JOIN_TYPES.includes(joinType)) {
  throw new Error("Invalid join type");
}

2. 조인 조건 검증

// 조인 조건은 SQL 조건식 형태 (예: "source.id = target.parent_id")
// SQL 인젝션 방지를 위한 검증 필요
const isValidJoinCondition = /^[\w\s.=<>]+$/.test(joinCondition);
if (!isValidJoinCondition) {
  throw new Error("Invalid join condition");
}

3. 순환 참조 방지

// 조인이 순환 참조를 만들지 않는지 검증
async function checkCircularReference(
  sourceTableId: number,
  targetTableId: number
): Promise<boolean> {
  // 재귀적으로 조인 관계 확인
  // ...
}

4. LEFT JOIN으로 관련 테이블 정보 조회

조인 설정 조회 시 source/target 테이블 정보를 함께 가져오기 위해 LEFT JOIN 사용


전환 완료 내역

전환된 Prisma 호출 (5개)

  1. detectEntityJoins() - 엔티티 컬럼 감지 (findMany → query)

    • column_labels 조회
    • web_type = 'entity' 필터
    • reference_table/reference_column IS NOT NULL
  2. validateJoinConfig() - 테이블 존재 확인 ($queryRaw → query)

    • information_schema.tables 조회
    • 참조 테이블 검증
  3. validateJoinConfig() - 컬럼 존재 확인 ($queryRaw → query)

    • information_schema.columns 조회
    • 표시 컬럼 검증
  4. getReferenceTableColumns() - 컬럼 정보 조회 ($queryRaw → query)

    • information_schema.columns 조회
    • 문자열 타입 컬럼만 필터
  5. getReferenceTableColumns() - 라벨 정보 조회 (findMany → query)

    • column_labels 조회
    • 컬럼명과 라벨 매핑

주요 기술적 개선사항

  • information_schema 쿼리: 파라미터 바인딩으로 변경 ($1, $2)
  • 타입 안전성: 명확한 반환 타입 지정
  • IS NOT NULL 조건: Prisma의 { not: null } → IS NOT NULL
  • IN 조건: 여러 데이터 타입 필터링

코드 정리

  • PrismaClient import 제거
  • import 문 수정 완료
  • TypeScript 컴파일 성공
  • Linter 오류 없음

📝 원본 전환 체크리스트

1단계: Prisma 호출 전환 ( 완료)

  • getEntityJoins() - 목록 조회 (findMany with include)
  • getEntityJoin() - 단건 조회 (findUnique)
  • createEntityJoin() - 생성 (create with validation)
  • updateEntityJoin() - 수정 (update)
  • deleteEntityJoin() - 삭제 (delete)

2단계: 코드 정리

  • import 문 수정 (prismaquery, queryOne)
  • 조인 유효성 검증 로직 유지
  • Prisma import 완전 제거

3단계: 테스트

  • 단위 테스트 작성 (5개)
  • 조인 유효성 검증 테스트
  • 순환 참조 방지 테스트
  • 통합 테스트 작성 (2개)

4단계: 문서화

  • 전환 완료 문서 업데이트

🎯 예상 난이도 및 소요 시간

  • 난이도: (중간)
    • LEFT JOIN 쿼리
    • 조인 유효성 검증
    • 순환 참조 방지
  • 예상 소요 시간: 1시간

상태: 대기 중
특이사항: LEFT JOIN, 조인 유효성 검증, 순환 참조 방지 포함