흐름맵 BPMN 서브셋 · 온톨로지 참조 설계 (0단계)
대상 모듈:
client/src/features/workflow-map/범위: 교환(D-교환) 대비 정규 모델까지. 실행 엔진(D-실행)은 이 문서의 범위가 아니다. 상태: 확정(권고 기본값으로 결정, 근거는 §7)
1. 배경·목표·비목표
TaskFlowMap은 증상 기반으로 규칙이 누적되어 흐름(flow) 과 자사 규칙(레벨·권한·외부유입) 이 한 덩어리로 섞였다. 확장성·표준화·온톨로지 연계를 위해 흐름을 BPMN 서브셋으로 정규화하고, 의미는 온톨로지 참조로 분리한다.
목표
- 흐름은 표준 BPMN 요소로만 표현한다(커스텀 노드 종류를 늘리지 않는다).
- 자사 규칙은 표준 확장 슬롯(
extensionElements)으로만 분리한다. - BPMN Task가 온톨로지 개념을 참조한다(링크만, 전문은 온톨로지가 소유).
.bpmn+ 온톨로지 참조로 export/import 가능한 정규 모델을 갖는다.
비목표
- 실행 엔진(Camunda/Zeebe) 도입 — 별도 프로젝트.
- 편집기를
bpmn-jsModeler로 교체 — 후속 결정. - 온톨로지 전체 큐레이션 — 참조 대상으로만 사용.
- 이 단계에서 화면(UI) 변경 — 없다.
2. 계층 구조
온톨로지(의미) 개념·용어·관계 ← 참조 대상(termId/uri)
BPMN 서브셋(흐름) Task·Gateway·Flow ← 정규 모델(canonical)
렌더러(ui/) 자체 SVG / bpmn-js ← 교체 가능
(미래) 실행기 엔진 어댑터 ← domain 경계에 이음새만3. 정규 모델
domain/bpmn.ts에 정의한다. UI·DB 타입에 의존하지 않는다.
ts
type BpmnSubsetModel = {
processId: string;
level?: string | null; // L4/L5 (extensionElements)
lanes: Lane[];
nodes: BpmnNode[];
flows: BpmnFlow[];
participants: ParticipantRef[]; // 다른 L5(외부유입) 참조 대상
extension?: ModelExtension;
};
type BpmnNode =
| { kind: "startEvent"; id: string; laneId: string; extension?: NodeExtension }
| { kind: "endEvent"; id: string; laneId: string; extension?: NodeExtension }
| { kind: "task"; id: string; laneId: string; performers: RoleRef[];
ontologyTerm?: OntologyTermRef; extension?: NodeExtension }
| { kind: "exclusiveGateway"; id: string; laneId: string;
documentation?: LocalizedText; defaultFlowId?: string; extension?: NodeExtension };
type BpmnFlow = {
id: string;
kind: "sequence" | "message"; // message = 외부유입
sourceId: string | null; // message + 외부 출발이면 null
targetId: string;
externalSource?: ExternalSourceRef; // message 전용
condition?: FlowCondition; // sequence 출구 조건
label?: LocalizedText | null;
extension?: FlowExtension;
};식별자 규칙(왕복 변환을 단순하게 유지):
| 대상 | 규칙 | 예 |
|---|---|---|
| 노드 | step:<stepId> | step:62 |
| 내부 흐름 | transition:<transitionId> | transition:9 |
| 외부유입 흐름 | inbound:<inboundId> | inbound:41 |
| lane | lane:<l5Id> | lane:50 |
| participant | participant:l5:<l5Id> | participant:l5:70 |
각 요소의 extension에 원본 식별자를 남겨 역변환이 추측 없이 가능하게 한다(§5).
4. 현재 모델 → BPMN 서브셋 매핑
| 현재 | BPMN 서브셋 | 비고 |
|---|---|---|
step.kind: "start" | startEvent | |
step.kind: "end" | endEvent | |
step.kind: "process" | task | performers ← 배정 역할 |
step.kind: "gateway" | exclusiveGateway | documentation ← question |
step.kind: "condition" | exclusiveGateway | 도메인 구분은 extension.sourceKind에 보존 |
transition.outcome: "YES"|"NO" | sequence + condition | { type: "outcome", value } |
transition.outcome: "DEFAULT" | sequence + 게이트웨이 defaultFlowId | 출발이 게이트웨이가 아니면 조건 없이 둔다 |
transition.conditionExpression | condition { type: "expression" } | 표준 conditionExpression으로 직렬화 |
inboundTransition | message + externalSource | 출발 L5는 participant(collapsed pool) |
배정 역할(roleMasterId/code) | performers[].roleCode | 표준 performer 계열 |
l5Id(단일 L5) | lane:1개 | L4 통합맵은 L5별 lane |
5. 확장(extensionElements) 계약
표준에 없는 자사 규칙은 여기에만 둔다. 네임스페이스는 고정 상수로 관리한다.
ts
export const PC_EXTENSION_NAMESPACE = "urn:process-center:bpmn:extension:1";| 확장 | 붙는 곳 | 내용 |
|---|---|---|
sourceStepId | node/fLow extension | 원본 workflow_steps.id / workflow_transitions.id (역변환용) |
sourceKind | gateway extension | "gateway" | "condition" 도메인 구분 |
level | model extension | "L4" | "L5" |
externalSource | message flow | { l5Id, l5Code?, stepId, stepKind, isReturn? } |
ontologyTerm | task | { termId, uri? } |
.bpmn 직렬화 예:
xml
<bpmn:task id="step:62" name="승인">
<bpmn:documentation>승인되었습니까?</bpmn:documentation>
<bpmn:extensionElements>
<pc:sourceStepId>62</pc:sourceStepId>
<pc:ontologyTerm termId="activity.approval" uri="https://terms.example/activity.approval" />
</bpmn:extensionElements>
</bpmn:task>6. 교환 포맷
- 흐름:
.bpmn(BPMN 2.0 XML).sequenceFlow·conditionExpression·exclusiveGateway·performer·participant는 표준 요소. - 의미: 온톨로지는 별도 직렬화(후속: OWL/JSON-LD)로 교환하고,
.bpmn안에는ontologyTerm참조만 넣는다. - export: 정규 모델 →
.bpmn. 렌더러와 무관하게 동작해야 한다. - import:
.bpmn파싱 → 정규 모델. 확장이 없는 외부 BPMN도 허용하고(레벨·외부유입은 기본값), 알 수 없는 확장은 보존한다.
7. 확정된 결정 (권고 기본값)
| # | 질문 | 결정 | 근거 |
|---|---|---|---|
| 1 | 온톨로지 식별자 | termId(슬러그) 필수, uri 선택 | 내부 용어집은 슬러그로 관리되고, 외부 온톨로지는 URI가 있을 수 있어 둘 다 수용 |
| 2 | 의사결정 질문 | 정규 모델에 구조화 필드 + .bpmn에는 표준 documentation | 표준 요소로 사람이 읽을 수 있게 하고, 기계 판독이 필요해지면 확장 추가 |
| 3 | 확장 네임스페이스 | urn:process-center:bpmn:extension:1 | 실제 도메인을 약속하지 않는 URN. 값은 상수 1곳에서 관리 |
| 4 | 정규 모델 위치 | 클라이언트 domain/ (의존성 0) | 서버 개입이 필요해지면 그대로 shared/로 이동 가능 |
8. 마이그레이션(정규화)
domain/normalize.ts — 기존 UI 데이터를 정규 모델로 바꾸는 순수 함수. DB·trpc에 의존하지 않는다.
ts
normalizeWorkflowMap({
processId, level?, steps, transitions, inboundTransitions?, roleAssignments?, lanes?
}): BpmnSubsetModel규칙:
- 노드/흐름 id는 §3의 접두 규칙으로 만들고, 원본 id를 extension에 남긴다.
outcome: "DEFAULT"는 출발이 게이트웨이일 때만defaultFlowId로 승격한다(그 외에는 조건 없는 흐름).outcome: "YES"/"NO"는condition { type: "outcome" }으로 둔다.inboundTransition은message흐름으로 만들고 출발 L5를participants에 등록한다(중복 제거).- 배정 역할은
processNodeId로 Task에 연결한다(배정이 없으면performers: []). - 입력 순서에 의존하지 않는다(정렬은 id 기준).
9. 파일 구조
client/src/features/workflow-map/
domain/
bpmn.ts # 정규 모델 타입 + 확장 네임스페이스
normalize.ts # 기존 데이터 → 정규 모델
normalize.test.ts # 단위 테스트
index.ts # domain 공개 API
ui/ # (후속) 캔버스·카드·패널
index.ts # 모듈 공개 API10. 검증
pnpm check(tsc) 통과.domain/normalize.test.ts통과(신규).TaskFlowMap.test.tsx·IntegratedWorkflowMap.test.tsx그대로 통과(회귀 없음).
11. 후속 단계(이 문서 범위 밖)
ui/분리 — 표현 컴포넌트 추출(로직 변경 없이).TaskFlowMap.tsx(1,572줄) 분해 — 캔버스/카드/패널/연결..bpmnimport(XML → 정규 모델) + export와 왕복 테스트. (export는domain/bpmnSerialize.ts로 구현됨)bpmn-jsViewer PoC(렌더·PDF만) → 필요 시 Modeler 교체 결정.- 엔진 어댑터(
domain/engine.ts) — 스텁만 유지, 도입은 별도 프로젝트.
12. 구현 상태
| 항목 | 상태 | 위치 |
|---|---|---|
| 정규 모델 타입 | 완료 | domain/bpmn.ts |
| 정규화(기존 데이터 → 모델) | 완료 + 단위 테스트 10개 | domain/normalize.ts |
.bpmn export | 완료 + 단위 테스트 8개 | domain/bpmnSerialize.ts |
.bpmn import | 완료 + 왕복 단위 테스트 6개 | domain/bpmnParse.ts |
| 엔진 어댑터 | 스텁 | domain/engine.ts |
| 화면 적용(ui) | 미착수(화면은 기존 경로 유지) | — |
| L4/L5 레벨, isReturn | extension | 비표준 |