Skip to main content

관계 정의 (relationships)

관계 정의

{
"sourceRef": "hcm_items",
"targetRef": "hcm_bom",
"relationType": "1:N",
"relationName": "품목→BOM"
}

관계 필드 명세

필드타입필수설명
sourceRefstring필수출발 테이블 refId
targetRefstring필수도착 테이블 refId
relationTypestring필수"1:1", "1:N", "M:N" 중 택1
relationNamestring선택한글 관계명 (예: "품목→BOM")
fkBindingsarray선택FK 컬럼 명시적 바인딩

관계 타입

타입의미예시
1:1일대일사원 → 사원상세
1:N일대다 (가장 일반적)거래처 → 발주서, 작업지시 → 실적
M:N다대다품목 ↔ 공급업체 (중간 테이블 필요)

fkBindings (선택적 FK 바인딩)

{
"sourceRef": "hcm_purchase_orders",
"targetRef": "hcm_partners",
"relationType": "1:N",
"fkBindings": [
{
"sourceColumn": "partner_id",
"targetColumn": "id",
"displayColumn": "partner_name"
}
]
}
  • 보통 생략 가능 — 시스템이 FK 컬럼의 fkRef에서 자동 추론
  • 하나의 관계에 여러 FK 바인딩이 있을 때 명시적으로 사용

관계 규칙

  1. sourceReftargetRef는 같은 JSON 파일 내 테이블 refId만 가능
  2. 자기 참조 가능 (예: hcm_item_categorieshcm_item_categories)
  3. 모든 FK 컬럼에 대응하는 관계가 하나 이상 존재해야 함
  4. M:N 관계는 반드시 중간 테이블 정의 필요

CascadeUpdate Rules (연쇄 갱신 규칙)

CascadeUpdateService는 특정 테이블에 INSERT 또는 UPDATE가 발생할 때, 관련 상위 테이블의 수량/상태를 자동으로 연쇄 갱신하는 서비스입니다. 관계 정의와 함께 사용되어 데이터 정합성을 보장합니다.

구현된 규칙 (5건)

규칙트리거대상 테이블갱신 내용
Rule 1production_results INSERTwork_ordersgood_qty, defect_qty 집계 갱신
Rule 2production_results INSERTproduction_ordersactual_qty, progress 재계산
Rule 3shipments 확정 (confirm)sales_order_itemsshipped_qty 증가
Rule 4shipments 취소 (cancel)sales_order_itemsshipped_qty 롤백
Rule 5sales_order_items shipped_qty 변경sales_ordersstatus 자동 갱신 (부분출하/완료)

트랜잭션 패턴

모든 연쇄 갱신은 QueryRunner를 사용한 단일 트랜잭션 내에서 실행됩니다. 대상 행에 FOR UPDATE 잠금을 걸어 동시성 충돌을 방지합니다.

// CascadeUpdateService 내부 트랜잭션 패턴
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();

try {
// 1. 대상 행 잠금
const target = await queryRunner.query(
`SELECT * FROM work_orders WHERE id = $1 FOR UPDATE`,
[workOrderId]
);

// 2. 집계 계산
const aggregated = await queryRunner.query(
`SELECT SUM(good_qty) as total_good, SUM(defect_qty) as total_defect
FROM production_results WHERE work_order_id = $1`,
[workOrderId]
);

// 3. 상위 테이블 갱신
await queryRunner.query(
`UPDATE work_orders SET good_qty = $1, defect_qty = $2 WHERE id = $3`,
[aggregated.total_good, aggregated.total_defect, workOrderId]
);

// 4. audit_trail 자동 삽입
await queryRunner.query(
`INSERT INTO audit_trail
(entity_type, entity_id, field_name, old_value, new_value,
changed_by, source_entity_type, source_entity_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
['work_orders', workOrderId, 'good_qty',
oldValue, newValue, userId,
'production_results', resultId]
);

await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}

audit_trail 자동 삽입

연쇄 갱신이 발생할 때마다 audit_trail 테이블에 변경 이력이 자동으로 기록됩니다.

  • source_entity_type / source_entity_id: 변경을 유발한 원본 엔티티 정보
  • entity_type / entity_id: 실제 갱신된 대상 엔티티 정보
  • 각 cascade 단계마다 별도 행 삽입 (예: Rule 1 + Rule 2가 연쇄 실행되면 2건의 audit_trail 생성)

규칙 정의 예시 (Rule 3: 출하 확정 → 수주항목 출하수량)

// 출하 확정 시 수주항목의 shipped_qty를 증가시키는 규칙
{
trigger: {
table: 'shipments',
event: 'confirm', // status가 '확정'으로 변경될 때
},
cascade: {
targetTable: 'sales_order_items',
joinColumn: 'sales_order_item_id', // shipment_items → sales_order_items FK
updateExpression: 'shipped_qty = shipped_qty + NEW.ship_qty',
},
audit: {
sourceEntityType: 'shipments',
fieldName: 'shipped_qty',
}
}

참고: CascadeUpdate 규칙은 관계 정의(relationships)의 FK 바인딩과 독립적으로 동작합니다. 관계 정의는 캔버스 시각화와 DDL 생성에 사용되고, CascadeUpdate 규칙은 런타임 데이터 정합성에 사용됩니다.


Document Conversion Rules (문서 전환 규칙)

CascadeUpdateService는 연쇄 갱신 외에도 문서 간 전환(견적→수주, 수주→생산오더, 생산오더→작업지시)을 트랜잭션 안전하게 처리합니다. 각 전환은 화면의 actionButtons에서 트리거됩니다.

구현된 전환 규칙 (3건)

규칙API 엔드포인트소스 테이블대상 테이블동작
DC-1POST :tableName/_convert-quotationquotations + quotation_itemssales_orders + sales_order_items견적서 → 수주 전환
DC-2POST :tableName/_create-production-orderssales_orders + sales_order_itemsproduction_orders수주 → 생산오더 발행
DC-3POST :tableName/_create-work-ordersproduction_orderswork_orders생산오더 → 작업지시 발행

전환 트랜잭션 패턴

모든 전환은 CascadeUpdate와 동일한 트랜잭션 패턴을 사용합니다.

// DC-1: 견적서 → 수주 전환
async convertQuotationToSalesOrder(tenantId: string, quotationId: string) {
const qr = dataSource.createQueryRunner();
await qr.startTransaction();

try {
await qr.query(`SET search_path TO tenant_${tenantId}`);

// 1. 소스 행 잠금 + 상태 검증
const quotation = await qr.query(
`SELECT * FROM quotations WHERE id = $1 FOR UPDATE`, [quotationId]
);
if (quotation.status !== '확정') throw new BadRequestException();

// 2. 대상 레코드 생성 (헤더 + 항목)
const salesOrder = await qr.query(
`INSERT INTO sales_orders (partner_id, ...) VALUES ($1, ...) RETURNING id`,
[quotation.partner_id]
);
// quotation_items → sales_order_items 복사

// 3. 소스 상태 변경
await qr.query(
`UPDATE quotations SET status = '수주전환' WHERE id = $1`, [quotationId]
);

// 4. audit_trail 삽입
await qr.query(
`INSERT INTO audit_trail (entity_type, entity_id, field_name, ...) VALUES (...)`,
['quotations', quotationId, 'status', '확정', '수주전환', ...]
);

await qr.commitTransaction();
} catch (err) {
await qr.rollbackTransaction();
throw err;
} finally {
await qr.release();
}
}

actionButtons와 전환 API 연동

화면의 actionButtons에서 action: "custom"으로 전환을 트리거합니다. visibleWhen으로 전환 가능 상태에서만 버튼을 노출합니다.

{
"refId": "screen_quotations",
"actionButtons": [
{
"label": "수주전환",
"action": "custom",
"style": "primary",
"confirmMessage": "선택한 견적서를 수주로 전환하시겠습니까?",
"order": 1,
"visibleWhen": { "status": ["확정"] }
}
]
}
  • "수주전환" 버튼 → POST /_convert-quotation (DC-1)
  • "생산의뢰" 버튼 → POST /_create-production-orders (DC-2)
  • "작업지시 발행" 버튼 → POST /_create-work-orders (DC-3)

전환 규칙

  1. 소스 문서는 특정 상태에서만 전환 가능 (확정, 승인 등)
  2. 전환 후 소스 문서의 상태는 자동 변경 (예: 확정수주전환)
  3. 전환된 문서 간 FK 관계가 자동 설정됨 (예: sales_orders.quotation_id)
  4. 한 번 전환된 문서는 중복 전환 불가 (상태 체크로 방지)
  5. 전환 실패 시 전체 롤백 — 소스 상태도 원복

Edge subtableConfig와 Document 화면 연동

1:N 관계의 Edge에 subtableConfig를 설정하면, 해당 Edge의 **부모 테이블(target)**을 바인딩하는 document 화면 노드에서 자동으로 documentConfig.lineItems가 생성됩니다.

자동 연동 규칙

순서규칙설명
1적용 조건Edge의 relationType"1:N"이고 subtableConfig.subtableEnabled = true인 경우만 적용
2화면 매칭Screen 노드의 boundTableRef가 Edge의 target(부모/헤더 테이블)과 일치해야 함
3FK 매핑Edge의 source(자식 테이블)의 FK 컬럼 정보가 lineItems[].foreignKeyColumn으로 매핑
4복수 Edge 처리여러 1:N Edge가 있으면 order 기준 정렬 후 여러 lineItems 생성
5수식/룩업 전파Edge에서 설정한 lineFormulas, itemLookup도 자동 전파
6병합 우선순위프리셋 JSON에도 lineItems가 정의된 경우, boundTableRef를 키로 병합 (Edge 우선)

Edge 방향 규칙

Edge 방향과 개념적 1:N 방향이 반대임에 주의하세요.

edge.source = 자식 테이블 (FK 보유) 예: quotation_items
edge.target = 부모 테이블 (참조 대상) 예: quotations
  • React Flow에서 화살표는 source → target 방향으로 그려집니다.
  • 데이터 관점에서 FK는 자식(source)이 보유하고, 부모(target)를 참조합니다.
  • 따라서 quotation_items(source) → quotations(target) Edge는 "quotations가 여러 quotation_items를 가짐"을 의미합니다.

subtableConfig 전체 필드

interface SubtableEdgeConfig {
subtableEnabled: boolean;
subtableTitle?: string;
subtableColumns?: string[];
subtableAddable?: boolean;
subtableDeletable?: boolean;
order?: number;
lineFormulas?: Array<{
targetColumn: string;
expression: string;
dependencies: string[];
}>;
itemLookup?: {
lookupTableRef: string;
fkColumn: string;
autoFillMappings: Array<{
sourceColumn: string;
targetColumn: string;
}>;
onLookupFailed?: 'zero' | 'empty' | 'warn';
};
}
필드타입필수설명
subtableEnabledboolean필수서브테이블 활성화 여부
subtableTitlestring선택서브테이블 탭/섹션 제목 (미지정 시 자식 테이블명 사용)
subtableColumnsstring[]선택표시할 컬럼 목록 (미지정 시 전체 컬럼)
subtableAddableboolean선택행 추가 허용 (기본값: true)
subtableDeletableboolean선택행 삭제 허용 (기본값: true)
ordernumber선택복수 서브테이블 정렬 순서 (기본값: 0)
lineFormulasarray선택행 단위 자동 계산 수식
itemLookupobject선택항목 선택 시 자동 채움 설정

lineFormulas 사용 예시

{
"subtableEnabled": true,
"subtableTitle": "견적항목",
"lineFormulas": [
{
"targetColumn": "amount",
"expression": "quantity * unit_price",
"dependencies": ["quantity", "unit_price"]
},
{
"targetColumn": "tax_amount",
"expression": "round(amount * 0.1)",
"dependencies": ["amount"]
}
]
}

itemLookup 사용 예시

{
"subtableEnabled": true,
"itemLookup": {
"lookupTableRef": "items",
"fkColumn": "item_id",
"autoFillMappings": [
{ "sourceColumn": "item_name", "targetColumn": "item_name" },
{ "sourceColumn": "unit_price", "targetColumn": "unit_price" },
{ "sourceColumn": "unit", "targetColumn": "unit" }
],
"onLookupFailed": "warn"
}
}
  • lookupTableRef: 조회 대상 마스터 테이블의 refId
  • fkColumn: 서브테이블에서 마스터를 참조하는 FK 컬럼
  • autoFillMappings: 마스터 컬럼 → 서브테이블 컬럼 자동 채움 매핑
  • onLookupFailed: 조회 실패 시 동작 — 'zero'(숫자 0), 'empty'(빈 문자열), 'warn'(경고 표시)

수식 보안 규칙

lineFormulasexpression은 다음 보안 규칙을 따릅니다.

규칙설명
실행 방식AST 기반 수식 파서 사용 (safe-formula.ts)
금지 함수eval(), new Function() 사용 금지
허용 연산자+, -, *, /, ()
허용 함수round, floor, ceil
허용 변수해당 서브테이블의 컬럼명만 (화이트리스트 기반)
길이 제한수식 최대 200자
순환 참조DFS 기반 순환 참조 감지 — 감지 시 수식 평가 거부

이전: 컬럼 정의 (고급) | 다음: 화면 구성 (기본)