본문으로 건너뛰기

관계 정의 (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. sourceRef와 targetRef는 같은 JSON 파일 내 테이블 refId만 가능
  2. 자기 참조 가능 (예: hcm_item_categories → hcm_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'(경고 표시)

수식 보안 규칙​

lineFormulas의 expression은 다음 보안 규칙을 따릅니다.

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

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