Ontology GUI Modeler — M2 종결 (Step 1~5)
본 가이드는 Epic #993 의 M2 Step 1 시점에서 활성화된 온톨로지 진입점을 다룹니다. M1 (#1020/#1023/#1045/#1049/#1053) 에서 백엔드 모델·LinkML loader·SHACL validator·ArangoDB instance repo 까지 완성된 위에, M2 Step 1 은 라우터 wire-up + 사이드바 메뉴 stub 으로 기능을 사용자에게 보이게 만듭니다.
범위 한정: M2 Step 1 은 "기능이 보이는 상태" 까지가 목적입니다. React Flow 기반 Designer 캔버스, Property 인라인 편집, Relation 편집, AQL traversal, 자동 매핑 LLM 은 M2 Step 2 (#1097) ~ M2 Step 5 (#1100) 에서 점진적으로 활성화됩니다.
진입 경로
| 경로 | 설명 |
|---|---|
| 사이드바 → Platform → 온톨로지 | 사용자 진입점. M1 종결 후 가시성 0 이던 갭 해소. |
GET /api/v1/ontology/classes | Class 목록. CLI / MCP / Agent 가 같은 경로 사용. |
| MCP 도구 (M3 예정) | LLM Agent 가 온톨로지 컨텍스트를 가져오는 경로. |
핵심 동작 (M2 Step 1)
Class CRUD
- 목록:
GET /api/v1/ontology/classes?layer=L2&workspace_id=...&pack_id=... - 생성:
POST /api/v1/ontology/classes- 필수:
layer,name - 선택:
label_ko,label_en,description,parent_class_id,abstract,version_id,data_source - ADR-006 Layer scope (L1/L2 → pack/ws 양쪽 NULL, L3 → pack 만, L4 → workspace 만) Pydantic 단계에서 422 차단
- 동일 4-tuple (
layer,pack_id,workspace_id,name) UniqueConstraint 충돌 → 409- PG 의 NULL-in-UNIQUE 갭은 본 PR 에서 partial unique index 3종 (
uq_ontology_classes_l1l2_name,..._l3_pack_name,..._l4_ws_name) 으로 보강
- PG 의 NULL-in-UNIQUE 갭은 본 PR 에서 partial unique index 3종 (
- 필수:
- 조회:
GET /api/v1/ontology/classes/{id}— 404 명확 분리 - 수정:
PATCH /api/v1/ontology/classes/{id}extra='forbid'— 알 수 없는 필드 명시 시 422- explicit
null거부 ([[feedback_pydantic_patch_null_guard]]) —{"description": null}도 422. 필드 누락 (omit) 만 허용
- 삭제:
DELETE /api/v1/ontology/classes/{id}— 연결 Property 는ondelete=CASCADE로 자동 정리
Property 조회 (read-only)
GET /api/v1/ontology/classes/{id}/properties— Designer 가 Class 선택 시 호출. 정렬: name asc.- 풀 CRUD 는 M2 Step 2 의 nested PUT 으로 통합 예정.
Instance CRUD (SHACL fail-closed)
POST /api/v1/ontology/instances/{class_id}— payload 는 동적 (ArangoDB document)._key권장.- SHACL 검증 실패 → 422 + GenD
errorsextension (구조화된 violation 배열)UI 의{"status": 422,"detail": "SHACL validation failed for Equipment","code": "ontology_shacl_violation","errors": [{ "loc": "instance", "msg": "tag_id is required (sh:minCount)", "type": "shacl_violation" }]}_extractDetail+errors배열 렌더링이 자동 처리. - ArangoDB 미가용 / 일반 오류 → 503 일관 변환
GET /api/v1/ontology/instances/{class_id}/{key}— 단건. 없으면 404.
RBAC
| 동작 | 최소 권한 |
|---|---|
| Class / Instance / Property 조회 | viewer 이상 |
| Class 생성·수정·삭제 / Instance 생성 | analyst 이상 |
| Class 단위 ABAC (M3) | OntologyClassGrant 도입 후 별도 |
UI 동작 (/ontology)
- landing card: Class 목록 (layer 별 배지), 빈 상태 메시지.
- Layer 필터: ALL / L1 / L2 / L3 / L4.
- "Class 생성" 다이얼로그: L2 만 우선 활성화 (L3/L4 는 pack/workspace 선택 워크플로우 도입 후 enable).
- 개별 Class 삭제: AlertDialog 확인 + sonner toast.
오류 표시:
- API 실패 시 카드 형태 inline 알림 (
text-red-800). - mutation 결과는 sonner toast.
다음 단계 (M2 Step 2~5)
| Step | 범위 | 이슈 | 상태 |
|---|---|---|---|
| M2 Step 2 | React Flow 캔버스 + Class/Property 편집 + Designer 탭 | #1097 | MERGED ✅ |
| M2 Step 3 | Relation CRUD API + Instances 탭 + AQL traversal | #1098 | MERGED ✅ |
| M2 Step 4 | 자동 매핑 LLM + 카탈로그 ↔ Class 양방향 링크 | #1099 | MERGED ✅ |
| M2 Step 5 | 버전 스냅샷 + 풀스택 e2e + 문서 완성 (M2 종결) | #1100 | 이번 PR ✅ |
M2 Step 2 — Designer (Class/Property GUI)
/ontology 목록에서 Class 의 "Edit" 버튼을 누르면 /ontology/{classId} Designer 로 진입.
Designer 구성
- 상단 탭: Model (활성) / Mapping (Step 4) / Instances (Step 3) / Versions (Step 5)
- 좌측 캔버스: React Flow — 현재는 단일 Class 노드 표시. M2 Step 3 에서 Relation edge 추가 예정.
- 우측 패널: Class metadata (label_ko/en, description) + Property 리스트 (range_type, cardinality, is_identifier, unit, pii_type)
- 저장:
PUT /api/v1/ontology/classes/{id}/with-properties단일 transaction- Class metadata + Property 리스트 함께 갱신
- Property 는
name기반 diff (INSERT / UPDATE / DELETE) - 동일
name중복 → 409 - PG IntegrityError (range_type / cardinality CHECK) → 409 (sanitized detail)
- 일반 실패 → 500 + 서버 로그
- 변경 표시:
dirty플래그 (zustand designer store) — 저장 버튼은 dirty 일 때만 활성
M2 Step 3 — Relation + Instances + AQL traversal
API
GET /api/v1/ontology/relations(옵션: source_class_id, target_class_id, layer)POST /api/v1/ontology/relations(source/target Class 존재 가드 → 404, layer scope 422, dup 409)DELETE /api/v1/ontology/relations/{id}(404 / 204)GET /api/v1/ontology/instances/{class_id}?limit=&offset=(페이지네이션)GET /api/v1/ontology/instances/{class_id}/{key}/neighbors?depth=&max_results=(AQL traversal —onto_<class>내_from/_to매치 document follow. edge collection + GRAPH traversal 은 M3 에서 확장)
UI Designer — Instances 탭 (M2 Step 3 활성화)
- 좌측 테이블: 인스턴스 페이지네이션 (PAGE_SIZE=25)
- 우측 패널: 선택 인스턴스의 neighbor traversal 결과 (depth=1, max_results=50)
- 인스턴스 생성/수정은 외부 ingestion + SHACL gate 가 담당 — 본 탭은 read-only
Relation 인라인 편집 UI (React Flow edge 드래그) 는 후속 PR. M2 Step 3 의 GUI scope 는 Instances 탭 우선.
M2 Step 4 — 자동 매핑 LLM (사람 검토 게이트)
API
POST /api/v1/ontology/mapper/suggestbody{table_fqn, top_n?}— LLM (Anthropic Claude Sonnet 4) 가 활성 Class 카탈로그 위에서 매핑 후보 N개 반환. confidence desc 정렬. prompt caching 적용 (system 메시지 + Class 카드 블록 모두 ephemeral cache_control).POST /api/v1/ontology/mapper/confirmbody{class_id, table_fqn}— 사람 검토 후Class.data_source갱신.- 429/529/5xx → 지수 백오프 3회 재시도. 응답 parsing 실패 /
stop_reason != end_turn→ 빈 후보 (UI 가 "제안 없음" 표시). - catalog 메타데이터 자동 fetch 는 M2 Step 4 stub (후속 PR 에서
catalog_service.describe+query_service.sample통합).
UI — Mapping 탭
- 활성화:
Designer > Mapping - 사용자가 table FQN 입력 → 후보 카드 N개 표시 (confidence bar + rationale + column → property 매핑 표)
- 신뢰도 < 0.7 경고 (Epic #993 본문의 "무인 적용 금지" 가이드라인 — 노란 배지). 무인 적용은 시스템 자체가 차단 — 사용자가 항상 "Confirm" 버튼을 명시 클릭해야 함.
- Confirm 시 현재 Class 와 후보 class_name 일치 검증 (mismatch → toast error).
보안 / 거버넌스
require_analyst— LLM 호출은 토큰 비용 발생이라 viewer 차단.- LLM 후보는 즉시 적용되지 않음 —
Class.data_source갱신은 두 번째 명시 POST (/mapper/confirm) 만으로. - API 키는
settings.llm_anthropic_api_key(config.py의 표준 키, services/llm_proxy_service 와 정합) lazy load — 미설정 시MapperLLMError→ 503.
M2 Step 5 — 버전 스냅샷 + diff (M2 종결)
API
POST /api/v1/ontology/versions/snapshotbody{label, description?, workspace_id?, pack_id?}— 현재 Class/Property/Relation 트리를 JSON snapshot 으로 immutable 저장GET /api/v1/ontology/versions?workspace_id=&pack_id=&limit=— snapshot 메타데이터 목록 (snapshot JSON 미포함, 페이로드 절감)GET /api/v1/ontology/versions/{id}— snapshot detail (JSON 트리 포함)GET /api/v1/ontology/versions/{before_id}/diff?to={after_id}— 두 snapshot 사이의 diff entry 리스트services/ontology/version_snapshot.py: build_snapshot / diff_snapshots (set 기반 added/removed, item 별 changed_fields)- workspace_id / pack_id 모두 nullable — 전체 카탈로그 snapshot 도 허용 (M3 layer 별 분리)
UI Designer — Versions 탭 (M2 Step 5 활성화 — M2 종결)
- 좌측: snapshot 목록 (라벨, 생성자, class/property/relation 카운트 배지, description)
- 우측: 두 snapshot 선택 후 "선택한 2개 Diff" 버튼 → 추가(녹색)/삭제(빨강)/변경(노랑) 색상 구분 테이블
- 하단: "새 Snapshot" 버튼 → label 입력 (description 선택) → POST snapshot
- 두 snapshot 선택은 toggle 방식 — 3번째 선택 시 가장 먼저 선택된 항목이 빠짐 (작성 순서 기준, snapshot 시간 기준 아님)
- diff 호출 시 older→newer 자동 정렬 (created_at)
거버넌스
- snapshot 생성은
require_analyst— read 는require_viewer - 모든 endpoint 가 사용자 명시 호출만 처리 — 자동 snapshot 없음 (M3 daily cron 옵션)
created_by= TokenPayload.safe_username — audit 추적
회귀 가드
| 파일 | 범위 |
|---|---|
apps/api/tests/test_ontology_router.py | M1 빈 GET + M2 Step 1 CRUD / 422 layer scope / 409 conflict / PATCH explicit null / Instance 422+503 + M2 Step 2 nested PUT (insert/diff/dup-name/404/extra) |
ui/src/stores/ontologyDesignerStore.test.ts | M2 Step 2 designer store: load class+properties / add/update/remove property / save PUT |
apps/api/tests/test_ontology_mapper.py | M2 Step 4 mapper service: clamp / format / parse (stop_reason / JSON / clamp / sort) / retry (429 → success / exhausts) + 라우터 통합 (suggest 503/200/empty / confirm 200/404) |
apps/api/tests/test_ontology_versions.py | M2 Step 5 version snapshot: diff_kind / diff_snapshots / 라우터 (create / list / detail / diff added·changed·removed) + 422 / 404 |
apps/api/tests/test_protected_routers_registration.py | _protected_routers 등록 보장 |
apps/api/tests/test_ontology_model.py | M1 Step 2 known gap → M2 Step 1 partial unique index 도입 시 회귀 가드 |
ui/src/stores/ontologyStore.test.ts | store loadClasses / createClass / deleteClass / setLayerFilter 단위 |
ui/src/components/layout/Sidebar.test.tsx | Platform 섹션에 "Ontology" 링크 존재 |
ui/tests/ontology-e2e.spec.ts | landing 페이지 + API smoke + layer scope 422 |
관련
- ADR: ADR-006 Ontology Layered Packaging
- Epic: #993 Ontology Layer
- M2 Step 1 PR: this commit
- 메모리: [[project_ontology_packaging_status]], [[feedback_pydantic_patch_null_guard]], [[feedback_fastapi_detail_shape_normalize]], [[feedback_zustand_no_immer]], [[feedback_react_refresh_export]]