import { ComputedRef, InjectionKey, Ref } from 'vue'; import { EditorState } from '../command/types'; import { DiagramMeta } from '../adapter/diagramAdapter'; import { OpConflict } from '../command/op'; import { OpSyncTransport } from '../command/opSync'; import { FieldAuditRecord } from '../core/projectFieldAudit'; import { ResolvedDiagram } from '../core/resolve'; import { RoutingMode } from '../core/routing'; import { MergeDiagramSource } from '../core/mergeDiagram'; import { ValidationIssue } from '../core/validation'; import { UiLocale, Translator } from '../i18n'; import { AssociationEnd, Attribute, DisplayMode, EmbeddableCatalog, Entity, EntityIndex, Geo, GroupCodeCatalog, LangCode, LogicalGroup, ModelId, Multiplicity, Operation, Point, PredefinedCustomTypeCatalog, SuperClassCatalog } from '../core/types'; import * as cmd from '../command/commands'; /** 엔티티 내부 속성을 가리키는 주소 원자 — 선택·컨텍스트·재정렬·연결 등 속성 단위 기능의 공통 참조점. */ export type AttrRef = { entityId: ModelId; attrId: ModelId; }; /** 속성 행 클릭 시 수식어에 따른 선택 합성 방식 — 단일 진입점 selectAttribute의 디스패치 축. */ export type SelectMode = 'replace' | 'toggle' | 'range'; /** * 복사/붙여넣기 클립보드 내용 — 선택 종류에 따라 엔티티 통째 또는 속성 집합. * 단일 클립보드(마지막 copy가 덮어씀). 속성 variant는 출처 엔티티를 담지 않는다 * — 붙여넣기 대상은 paste 시점의 선택(현재 엔티티/부모)이 정하므로 출처 무관. */ export type ClipboardContent = { kind: 'entity'; entity: Entity; geo: Geo; layoutExtras?: cmd.EntityLayoutExtras; } | { kind: 'entities'; source: MergeDiagramSource; selectedIds: ModelId[]; } | { kind: 'attribute'; attributes: Attribute[]; }; export type Selection = { kind: 'entity' | 'association' | 'note' | 'group'; id: ModelId; } | { kind: 'attribute'; entityId: ModelId; ids: ModelId[]; anchor: ModelId; } | null; /** * (나) 필드 감사 조회 transport — 호스트가 주입하는 이력 조회 함수. diagram의 op 이력 스트림을 * seq 순 `FieldAuditRecord[]`로 반환한다. `sinceSeq`를 주면 그 이후만(증분 fetch, 기본 전량). * op emit의 `OpSyncTransport`와 대칭 — 쓰기는 op 배치, 읽기는 감사 레코드. */ export type HistoryTransport = (sinceSeq?: number) => Promise; /** 복원 결과 — 호스트 restore 라우트 응답의 lib-로컬 최소 형태(ok + 실패 사유). */ export interface RestoreOutcome { ok: boolean; reason?: string; } /** * 시점 복원 transport — 호스트가 주입하는 복원 함수(`restoreEntityDiagram` 라우트). toSeq 시점 상태로 * forward 보상 쓰기(diagram-wide). 성공 시 라이브 doc이 바뀌므로 controller가 reloadRequest로 재적재를 요청한다. */ export type RestoreTransport = (toSeq: number) => Promise; /** * 로컬 명령 적용 실패 — `run()`이 `stack.execute` 중 잡은 예외의 표면화 형태. * * ★`OpConflict`(서버 축)와 **다른 축**이다. 저쪽은 *로컬은 맞는데 서버가 못 따라온* 상태이고, * 이쪽은 *로컬이 부분 변형돼 틀렸고 서버는 깨끗한* 상태다 — `execute`가 apply → push → emit 순서라 * apply 중 throw하면 **op가 나가지 않는다**(서버 문서·이력·CAS 무영향). 그래서 두 상태의 복구가 * 갈린다: op 전송 실패는 재적재가 미저장 변경을 버리지만, 이쪽은 **버릴 것이 오염된 부분 변형뿐**이라 * 재적재에 손실이 없다(성공한 이전 편집은 이미 서버에 있다). * * ⚠️`OpConflict.kind`에 이 축을 얹지 않는 이유: 그 어휘(`missing`/`rev`/`layout`)는 CC-10 ⑥이 * *"호스트는 `rev`만 낸다 / `missing`은 클라 합성 전용 / `layout`은 미방출 예약값"*으로 정리한 * 서버 축 전용이고, 호스트 watch의 `else` 분기가 catch-all이라 새 kind가 **"다른 사용자가 변경했습니다"**로 * 오귀속된다. */ export interface CommandFailure { /** 실패한 명령의 label — **진단용**(ko 고정 문자열이라 UI 노출용이 아니다. 배너 문구는 i18n 카탈로그 소유). */ label: string; } export interface EditorController { state: EditorState; resolved: ComputedRef; /** * 설계 품질 검증 이슈(reactive 파생) — validateModel(state.logical). 순수 함수 + reactive state라 * 모델 변경 시 자동 재계산. 검증 패널과 인라인 배지가 공유하는 단일 출처(단일 호출). */ issues: ComputedRef; /** * targetRef → 그 요소의 이슈 목록. 인라인 배지가 행 단위로 룩업해 최고 심각도를 표시한다. * targetRef 없는 이슈(model 레벨)는 인덱스에서 제외(인라인 매칭 대상 아님). */ issuesByRef: ComputedRef>; /** 이 이슈에 결정적 quick-fix가 있는지(뷰가 '수정' 버튼 노출 여부 판단). 조회 모드에선 항상 false. */ hasQuickFix(issue: ValidationIssue): boolean; /** 이슈의 quick-fix 적용(단일 command·undo 가능). 적용됐으면 true, 불가(없음·조회·잠금)면 false. */ applyQuickFix(issue: ValidationIssue): boolean; /** * CHK-DSN-3(모델링된 관계 없는 엔티티) 활성 토글 — 사용자가 검증 패널에서 켠다(기본 off). * 비형식 FK 오탐 가능성이 있어 옵트인. read-only에서도 토글 가능(표시 제어라 editable 무관). */ flagEntitiesWithoutRelations: Ref; canUndo: Ref; canRedo: Ref; dirty: Ref; /** 모델 변경 신호 — 명령 적용마다 단조 증가. 외부 관찰자(감사 패널 등)가 재조회 트리거로 watch(디바운스 권장). */ revision: Ref; /** * 사용자에게 표시할 일시적 안내(차단된 동작 등) — 뷰가 구독해 토스트로 표시하고 자체 타이머로 dismiss. * `id`는 같은 `text` 반복 시에도 재표시를 트리거하기 위한 단조 증가 값(동일 값 재대입은 watch 미발화이므로). */ notice: Ref<{ text: string; id: number; } | null>; /** * 편집 가능 여부 — false(조회 모드)면 모든 모델 변경(추가·삭제·이동·편집·재정렬·연결·붙여넣기· * undo/redo·저장)이 차단된다. 선택·팬/줌·hover 강조·복사·export 같은 읽기성 동작은 유지. * 모든 mutation이 거치는 run()/undo/redo 한 곳에서 차단하므로 데이터 안전의 최종 방어선이다. * 호스트가 비관적 락 획득 후 setEditable(true)로 전환하는 진입점(락 미연동 시 순수 뷰 토글). 기본 true. */ editable: Ref; /** 조회/편집 전환. 편집 활성(true)은 op-mode에서만 유효 — 비-op doc은 무시(no-op). false는 항상 허용. */ setEditable(v: boolean): void; /** * 데이터 형태 세대(트랙 B 마커) — 1=legacy flat, 2=logical/layout(op API 대상). 부재 시 1. * `fromPersisted`가 doc 마커에서 읽어 host가 meta로 주입. */ schemaVersion: Ref; /** * op 쓰기 롤아웃 게이트(불변식: true ⟹ schemaVersion>=2). v2 && false면 read-only 프리즈(B-0-1) — * 생성 시 editable=false로 연동. full-replace 폴백이 아니라 쓰기 kill-switch. */ opWriteEnabled: Ref; /** * 문서 단위 「참고 전용」(v2 doc 루트 마커 — 호스트 소유). true 면 이 문서 전체가 산출·검증 대상 밖이다. * 엔티티 단위 `Entity.referenceOnly` 와 **같은 기준으로 수렴**시킨다(`applyDocumentReferenceOnly` 투영) * — 소비처마다 별도 단락 로직을 두면 엔티티 층 시맨틱과 어긋날 수 있다. */ referenceOnlyDocument: Ref; /** * op-mode 활성 여부 — `schemaVersion>=2 && opWriteEnabled`. op emit(B-2)의 게이트로, * 레거시 doc(v1)·read-only v2는 false. editable 게이트와 같은 단일 통과점 옆에 둔다(stack은 마커 무지). */ opMode: ComputedRef; /** * 마지막 op 충돌(또는 전송 실패) — 어댑터 `onFreeze`가 채운다. null이면 정상. 충돌 시 `editable`이 * 함께 false로 내려가고(하드 프리즈), 호스트가 이 값으로 "다른 사용자가 변경함 — 새로고침" UX를 띄운 뒤 * `loadDiagram`으로 재적재하면 해소된다(B-5). op-mode가 아니면 항상 null. */ opConflict: Ref; /** * 마지막 로컬 명령 적용 실패 — `run()`이 채운다. null이면 정상. 세팅 시 `editable`이 함께 false로 * 내려가고(로컬 프리즈), 오염된 상태 위 후속 편집이 차단된다. `loadDiagram` 재적재로 해소된다. * * ★호스트는 이 값을 watch해 **확인 없이 자동 재적재**하면 된다 — op가 방출되지 않았으므로 서버가 * 이미 정본이고 손실이 0이다(`opConflict.kind='missing'`이 손실 고지 모달을 받는 것과 갈리는 지점). * 미배선이어도 lib 내장 프리즈 배너가 상주 안내한다. */ commandFailure: Ref; /** * (나) 필드 감사 조회 transport — 호스트가 이력 조회 라우트(`entityDiagramHistory.get`) fetch 래퍼를 * 주입(opTransport 미러). 감사 패널이 호출해 이력 레코드를 얻고 `projectEntityAudit`로 투영한다. * 미주입(dev/test·비-op doc) 시 undefined → 감사 패널 비활성. */ historyTransport?: HistoryTransport; /** 감사 패널 사용 가능 여부 — op-mode + historyTransport 주입 양쪽 충족. */ historyAvailable: ComputedRef; /** 시점 복원 transport(호스트 주입) — 감사 패널 '이 시점으로 복원'이 호출. */ restoreTransport?: RestoreTransport; /** 복원 사용 가능 여부 — op-mode + editable + restoreTransport 주입(조회 모드·비-op은 불가). */ restoreAvailable: ComputedRef; /** * 특정 seq 시점으로 복원 요청 — restoreTransport 위임. 성공(ok) 시 reloadRequest를 올려 호스트 재적재를 유도한다. * transport 미주입/실패 시 { ok:false } 반환(패널이 사유 표시). editable=false면 no-op({ ok:false }). */ restoreToSeq(toSeq: number): Promise; /** * 재적재 요청 신호(단조 증가) — 복원처럼 라이브 doc이 서버에서 바뀐 뒤, 호스트가 이 값을 watch해 * 최신 doc을 refetch → loadDiagram으로 갈아끼운다(focusRequest nonce와 같은 신호 패턴). */ reloadRequest: Ref; /** * 재적재 **진행 중** 신호 — 위 `reloadRequest` 의 **역방향**이다(저쪽은 lib→호스트 「불러와 달라」, * 이쪽은 호스트→lib 「내가 지금 불러오는 중」). 호스트가 refetch 시작 시 true, 포기 시 false 로 쓴다. * 성공 경로는 `loadDiagram` 이 함께 내린다(재적재가 착지하면 창이 끝난 것이 정의다). * * ★소비처는 프리즈 배너 하나다 — 자동 복구 경로에서 *"다시 불러오세요"* 라는 **행동 요구**가 어긋나는 * 것을 막는다(누를 것도 할 일도 없다). ⚠️**백오프 재시도 대기도 진행 창이다** — 호스트가 재시도 사이에 * 내리면 배너가 원인 문구로 깜빡인다. * * ★**미배선이면 영원히 false** 이고 그때는 종전 문구가 **맞는 안내**다(그쪽은 자동 reload 가 없다). */ reloadInFlight: Ref; /** * 프리즈 후(또는 외부 변경 감지 시) 재적재 경로 — 호스트가 `fromPersisted`로 파싱한 state/meta를 주입한다 * (생성자와 대칭 시그니처). state를 in-place 교체하고 스택을 리셋하며, 마커(schemaVersion/opWriteEnabled/ * editable)를 재평가하고 op 어댑터를 재구성·재seed·재attach한다. undo 이력·선택·충돌 상태는 리셋된다. */ loadDiagram(state: EditorState, meta?: DiagramMeta): void; /** * 열린 coalesce 그룹(드래그 등)을 즉시 호스트로 내보내고 경계를 봉인한다. save·blur·beforeunload에서 호출. * op-mode가 아니면 no-op. 전송 완료까지 기다리려면 이어서 `whenOpsSettled()`를 await. */ flush(): void; /** op 큐가 비고 전송 중이 아닐 때 resolve(save 동기화·테스트용). op-mode가 아니면 즉시 resolve. */ whenOpsSettled(): Promise; displayMode: Ref; /** 관계선 라우팅 모드 — 직선(기본) / 직각 */ routingMode: Ref; /** 캔버스 그리드(배경 점) 표시 여부 — 시각 전용 환경설정 (영속·undo 대상 아님) */ showGrid: Ref; /** 미니맵 표시 여부 — 시각 전용 환경설정 (영속·undo 대상 아님) */ showMinimap: Ref; selection: Ref; /** 선택이 속성일 때 앵커(주 포커스) 속성 참조 (아니면 null). 다중선택 시 anchor를 가리킨다. */ selectedAttr: ComputedRef; /** 현재 속성 선택 집합의 속성 id 목록 (속성 선택이 아니면 빈 배열). 행 마커·배치 연산 대상. */ selectedAttrIds: ComputedRef; /** Inspector가 비출 엔티티 id — 'entity'·'attribute' 선택 양쪽에서 해석. */ inspectedEntityId: ComputedRef; /** * 캔버스 속성 행 선택 (엔티티 통째가 아닌 행 단위 포커스). * mode: replace=단일 교체(기본) / toggle=Ctrl 가감 / range=Shift 앵커~대상 범위. * 단일 엔티티 스코프 — 현재 선택과 다른 엔티티를 가리키면 mode 무관하게 replace로 폴백. */ selectAttribute(entityId: ModelId, attributeId: ModelId, mode?: SelectMode): void; /** * 키보드 ↑/↓ — 속성 선택의 anchor를 표시 순서상 인접 속성으로 단일 이동(replace). * attribute 선택이 아니면 false(미처리 → 호출부가 키를 소비하지 않음). 경계에서는 이동 없이 true. */ moveAttributeFocus(direction: 1 | -1): boolean; /** * 키보드 Alt+↑/↓ — 현재 속성 선택 집합을 한 칸 위/아래로 이동(reorderAttributes 위임, undo 원자). * 다중선택 집합은 블록으로 묶여 이동. attribute 선택이 아니면 false. 경계에서는 이동 없이 true. */ nudgeSelectedAttributes(direction: 1 | -1): boolean; /** * 키보드 Delete/Backspace — 현재 선택을 삭제. 속성 선택은 집합 배치 삭제(undo 원자), * 엔티티/노트/그룹/관계는 각 노드 삭제. 선택이 없으면 false(미처리). */ deleteSelection(): boolean; /** * 키보드 화살표 — 선택 노드(엔티티/노트/그룹, 다중 포함)를 dx/dy만큼 미세 이동(우리 명령으로 처리해 * state·undo 동기화). 그룹은 멤버·내부 waypoint 동반, 선택된 그룹의 멤버는 직접 이동에서 제외. * 연속 호출은 같은 노드 집합이면 하나의 undo로 병합. 이동 대상이 없으면 false. */ nudgeNodes(nodeIds: ModelId[], dx: number, dy: number): boolean; /** * 캔버스 포커스 이동 요청 신호 — Diagram Explorer 등 외부에서 특정 노드로 센터링 요청. * nonce는 동일 id를 연속 클릭해도 watch가 매번 발동하도록 하는 단조 증가 카운터. */ focusRequest: Ref<{ id: ModelId; nonce: number; } | null>; requestFocus(id: ModelId): void; undo(): void; redo(): void; markSaved(): void; /** * add류 intent 공통 반환 계약: 성공 시 생성된 ModelId, 실패(조회 모드·충돌 프리즈·잠금·가드 차단) 시 * null. 거짓 성공 id를 돌려주면 뷰가 존재하지 않는 대상을 select(빈 폼)하므로, 소비처는 null 가드 후 * 후속 동작(자동 선택 등)을 수행해야 한다. */ addEntity(name?: string, location?: Point): ModelId | null; renameEntity(id: ModelId, name: string): void; updateEntity(id: ModelId, patch: Partial): void; moveEntity(id: ModelId, location: Point): void; /** 엔티티 크기 변경 — 콘텐츠 실측 최소 이하 클램프는 렌더(CSS min-content)가 담당 */ resizeEntity(id: ModelId, size: { width: number; height: number; }): void; removeEntity(id: ModelId): void; /** 엔티티 헤더 바 색상 토큰 지정 (null이면 해제 → 기본 헤더) */ setEntitySwatch(id: ModelId, token: string | null): void; /** 엔티티 접기/펼치기 — 접으면 헤더만 표시하고 속성/메서드 칸을 숨긴다 */ setEntityCollapsed(id: ModelId, collapsed: boolean): void; /** 엔티티 편집 잠금 토글 — 잠그면 이동·크기·논리수정·삭제 차단(잠금 해제는 항상 허용) */ setEntityLocked(id: ModelId, locked: boolean): void; /** 엔티티 잠금 여부 조회 (레이아웃) */ isEntityLocked(id: ModelId): boolean; /** * 연관 잠금 여부 조회 — 양 끝 엔티티 중 한쪽이라도 잠겼으면 true(내부 게이트 isAssocLockedFn와 * 동일 의미론 단일 출처). 뷰가 관계 편집 어포던스(삭제 메뉴·인스펙터 폼)를 억제할 때 사용. */ isAssociationLocked(assocId: ModelId): boolean; /** 활성 다이어그램 전체 엔티티 접기/펼치기 — 사전 상태 캡처 단일 커맨드(1 undo 스텝) */ collapseAll(): void; expandAll(): void; /** 속성 강조 글자색 토큰 지정 (null이면 해제) */ setAttributeSwatch(entityId: ModelId, attributeId: ModelId, token: string | null): void; /** 그룹 이동 — 그룹 박스 + 멤버 절대좌표를 한 명령(composite)으로 함께 갱신 */ dragGroup(groupId: ModelId, location: Point, members: { id: ModelId; location: Point; }[]): void; resizeGroup(groupId: ModelId, size: { width: number; height: number; }): void; /** 그룹 필드 부분 수정 (name·description·packageName·excludeDDLGeneration) */ updateGroup(groupId: ModelId, patch: Partial): void; /** 그룹 박스 색상 토큰 지정 (null이면 해제 → 기본 그룹 배경) */ setGroupSwatch(groupId: ModelId, token: string | null): void; /** 그룹 편집 잠금 토글 — 잠그면 그룹 박스 이동·크기·그룹 논리수정·삭제 차단 */ setGroupLocked(groupId: ModelId, locked: boolean): void; /** 그룹 잠금 여부 조회 (레이아웃) */ isGroupLocked(groupId: ModelId): boolean; /** 선택 엔티티들로 새 그룹 생성 (멤버 bbox에 박스 배치) — 빈 입력·조회 모드 시 null */ createGroup(entityIds: ModelId[], name?: string): ModelId | null; /** 빈 그룹 박스 생성 (이후 드래그로 엔티티 편입) */ addEmptyGroup(location: Point, name?: string): ModelId | null; removeGroup(groupId: ModelId): void; /** 엔티티 이동 + 컨테인먼트에 따른 그룹 편입/해제를 한 명령으로 */ dropEntity(entityId: ModelId, location: Point, targetGroupId: ModelId | null): void; /** 엔티티를 현재 속한 그룹에서 제외 */ ungroupEntity(entityId: ModelId): void; /** * 노트 이동 + 컨테인먼트에 따른 그룹 편입/해제를 한 명령으로 (dropEntity 거울). * 멤버십 조회/기록 대상이 논리(memberEntityRefs)가 아닌 레이아웃(GroupLayout.memberNoteRefs). */ dropNote(noteId: ModelId, location: Point, targetGroupId: ModelId | null): void; /** * Predefined 임베더블 카탈로그(호스트 주입). 신규 EMBED_PREDEF 삽입 메뉴·빈 슬롯 표시 폴백의 소스. * dev는 프리셋을 주입하고, 완전 통합 시 호스트가 embeddableCatalog API 값을 주입한다. 비어 있으면 * predefined 삽입 메뉴가 노출되지 않는다(기능 게이트). */ embeddableCatalog: Ref; /** 카탈로그 교체(호스트 props 갱신·dev 토글). reactive하므로 메뉴·폴백이 즉시 반영된다. */ setEmbeddableCatalog(catalog: EmbeddableCatalog): void; /** * 공통 코드 그룹 카탈로그(호스트 주입). 속성 groupCode 선택 드롭다운의 소스. * embeddableCatalog와 동형 — dev는 프리셋, 완전 통합 시 호스트가 systemCode API 값을 주입한다. * 비어 있으면 groupCode 드롭다운이 노출되지 않는다(기능 게이트). */ groupCodeCatalog: Ref; /** 카탈로그 교체(호스트 props 갱신·dev 토글). reactive하므로 드롭다운이 즉시 반영된다. */ setGroupCodeCatalog(catalog: GroupCodeCatalog): void; /** * Predefined customtype 카탈로그(호스트 주입). 속성 타입 셀렉트에 병합 노출할 ③ 단일 컬럼 커스텀 타입의 * 소스. embeddableCatalog와 동형 — dev는 프리셋, 완전 통합 시 호스트가 프레임워크 공통분 + 프로젝트별 * 추가분을 주입한다. 비어 있으면 셀렉트에 추가 옵션이 노출되지 않는다(inert). */ predefinedCustomTypeCatalog: Ref; /** 카탈로그 교체(호스트 props 갱신·dev 토글). reactive하므로 셀렉트가 즉시 반영된다. */ setPredefinedCustomTypeCatalog(catalog: PredefinedCustomTypeCatalog): void; /** * 슈퍼클래스 카탈로그(호스트 주입) — `superClass`(예: BaseEntity) 상속 컬럼(감사필드)을 인덱스 대상 * picker/라벨에 파생 투영(projectInheritedColumns)하는 소스. embeddableCatalog와 동형 — dev는 프리셋, * 완전 통합 시 호스트가 superClassCatalog API 값을 주입한다. 비어 있으면 상속 컬럼이 투영되지 않는다 * (인덱스는 로컬 컬럼만 참조 — 기능 게이트). */ superClassCatalog: Ref; /** 카탈로그 교체(호스트 props 갱신·dev 토글). reactive하므로 인덱스 picker가 즉시 반영된다. */ setSuperClassCatalog(catalog: SuperClassCatalog): void; /** * 입력 가능 언어셋(호스트 주입) — 다국어 텍스트(설명·논리명·그룹명) 편집 UI가 노출할 언어 탭 목록·순서. * 비어 있으면 안 되며(입력 불가) 미주입 시 기본 4언어(ko/en/ja/zh). reactive하므로 .value 갱신 시 즉시 반영. * MultiLangText 키의 부분집합/순서만 — 키 자체 확장(타입·직렬화 영향)은 범위 밖. */ languages: Ref; /** * 접속 유저의 언어(호스트 주입) — 다국어 편집 UI의 초기 활성 탭. languages에 없으면 컴포넌트가 * languages[0]로 폴백. 미주입 시 'ko'. 라벨 우선 표시 등 확장 여지의 기준점. */ userLanguage: Ref; /** * 프로젝트 «기본» 언어(호스트 주입) — **산출물에 굳는** 언어를 고르는 축. 현재 소비처는 DDL * 내보내기의 `COMMENT ON` 로케일(`ddl.ts` `commentLocale`)이다. * * ★★**언어 축 셋과 «넷째»인 이유 — 딸린 대상이 다르다.** `languages`(입력셋)·`userLanguage` * (접속 유저)·`uiLocale`(툴 크롬) 셋은 전부 **보는 사람**에 딸린 값이라 산출물에 굳히면 *누가 * 뽑았느냐*에 따라 결과가 달라진다. DDL 주석처럼 **DB 에 남는** 값은 프로젝트에 딸린 이 축을 * 써야 한다 — 레거시 `AbstractSrcGen.getDefaultLanguage()`(= `systemSetting.language`)와 같은 * 자리이고, 호스트는 `server/utils/projectLocale.ts` 가 소유한다. * * ⚠️**`userLanguage` 를 여기 넘기지 말 것**(`ddl.ts` 의 같은 경고와 한 쌍이다). * * ★**미주입 = `undefined` 이고 폴백하지 않는다** — 형제 축들과 달리 기본값을 두지 않는 것은 * 「모르는 값 규약」이다. 프로젝트가 언어를 선언한 적이 없는데 `'ko'` 로 채우면 **한국어 주석이 * 달린 DDL 이 DB 에 굳는다**(호스트 `resolveProjectLocale` 가 레거시의 `"en"` 하드 폴백을 * 걷어낸 것과 같은 근거). 소비처는 미주입이면 그 축을 **끈다**(주석 미방출). */ projectLanguage: Ref; /** * 툴 크롬(버튼·라벨·툴팁·검증 메시지) 언어(호스트 주입) — 콘텐츠 축 `userLanguage`와 **별개**. * 미주입 시 'ko'(무회귀). reactive하므로 .value 갱신 시 UI 문자열이 즉시 재렌더. */ uiLocale: Ref; /** * UI 문자열 해석기 — `t('mode.edit')` 형태로 뷰가 소비. uiLocale.value를 읽으므로 템플릿에서 * 호출 시 로케일 변경에 반응한다. 파라미터는 `{name}` 형태로 치환. */ t: Translator; /** * dataType 셀렉트 후보 셋(CHK-TYPE-1과 공유하는 단일 출처) — dataType 드롭다운(dbTypeSelectItems)이 * 이 값을 후보로 쓴다. 검증 ctx.selectableDataTypes와 같은 ref라 어포던스(드롭다운)·검증이 어긋나지 * 않는다. 미주입(undefined)이면 드롭다운은 코어 SELECTABLE_DATA_TYPES로 폴백하고 CHK-TYPE-1은 skip. */ selectableDataTypes: Ref | undefined>; /** * Predefined 임베더블 삽입 — 카탈로그에서 embeddableType 항목을 찾아 EMBED_PREDEF 속성을 추가한다. * dbAttr 슬롯은 레거시 `initAttributeDatabaseAttribute` 동형으로 **빈 골격**(physicalName/dataType * 미설정)만 시드하고, 표시·저장 시 카탈로그 메타모델로 폴백한다(`SVG:976` 대응). 참조가 아니므로 * association은 만들지 않는다(entity.attributes 직접 보유 — 레거시 NORMAL 부류). 카탈로그에 없거나 * 대상 엔티티가 없으면 no-op(null). */ addPredefinedEmbed(entityId: ModelId, embeddableType: string): ModelId | null; /** * 기존 속성의 타입을 **자리 보존 전환** — 타입 셀렉트가 «컬럼 개수 축이 갈리는» 값을 낼 때의 착지점. * `addPredefinedEmbed`가 꼬리에 **새 행**을 만드는 것과 달리 이쪽은 그 자리에서 형상을 바꾼다(사용처 * 순서를 어긋내지 않는다). `targetType`이 ② 임베더블 카탈로그에 있으면 `EMBED_PREDEF`로, 없으면 * `NORMAL`로 착지한다 — 즉 **되돌림도 이 경로다**(단방향이 아니다). * 대상이 없거나 전환 불가(FK 파생 · 임베더블 착지인데 식별자 · `EMBED_OWN`/`EMBED_REF`/`RELATION_*`/ * `DIVIDER` 출발 · `groupCode` 결합 · 같은 타입 재선택)면 no-op(null). */ convertAttributeType(entityId: ModelId, attributeId: ModelId, targetType: string): ModelId | null; addAttribute(entityId: ModelId, partial?: Partial): ModelId | null; /** * 시각 구획 행(DIVIDER) 추가 — 거대 엔티티에서 속성 묶음을 끊는 구분선. DB 컬럼·타입·식별자 없이 * 말미에 추가되며(이후 grip 드래그로 위치 조정), `name`은 선택적 섹션 라벨. divider 기본값을 단일 * 출처로 보장하려고 addAttribute에 위임한다. */ addDivider(entityId: ModelId): ModelId | null; updateAttribute(entityId: ModelId, attributeId: ModelId, patch: Partial): void; /** * ⑤ 파생 타입 결합 — 속성 groupCode를 설정/해제하며 type을 동반 조정한다(레거시 정합). * groupCode 지정 시 type=GroupCodeEnum(파생), 해제 시 기본 스칼라(String)로 환원. 이 불변식을 * 단일 출처로 보장해 어느 진입점(인스펙터·와이드·향후 메뉴/op)에서 호출해도 일관되게 한다. * 저수준 updateAttribute는 op-replay·undo 경로를 공유하므로 이 강제를 박지 않는다(역연산 보존). */ setAttributeGroupCode(entityId: ModelId, attributeId: ModelId, groupCode: string | undefined): void; /** * 펼친 임베드(EMBED_OWN/EMBED_PREDEF) 컬럼 묶음을 교체한다(@AttributeOverride 편집). EMBED_OWN이 * 로드 출신(association end에 legacyEmbedRaw verbatim 스냅샷 보유)이면 그 스냅샷을 함께 폐기해 저장이 * 마커-재구성(assocToEmbed, source 대비 override diff) 경로로 승격되게 한다 — 안 그러면 assocTo가 * 원본을 verbatim 환원해 컬럼 편집이 소실된다. EMBED_PREDEF는 association이 없어 컬럼 교체만 수행. * 스냅샷 폐기와 컬럼 교체는 한 composite로 묶어 undo 원자성을 보장한다. */ setEmbedColumns(entityId: ModelId, attributeId: ModelId, dbAttrs: Attribute['dbAttrs']): void; removeAttribute(entityId: ModelId, attributeId: ModelId): void; /** 여러 속성을 한 명령(composite)으로 삭제 — 다중선택 배치 삭제의 undo 원자성 보장. */ removeAttributes(entityId: ModelId, attributeIds: ModelId[]): void; /** 여러 속성에 같은 강조 색 토큰을 한 명령으로 지정 (null이면 해제) — 다중선택 배치 색상. */ setAttributeSwatches(entityId: ModelId, attributeIds: ModelId[], token: string | null): void; /** * 속성 행 재정렬 — 대상(attrIds)을 표시 순서 유지한 채 toIndex로 이동(단일 명령, undo 원자). * toIndex = 대상 제거 후 배열(rest) 기준 삽입 위치. 다중선택 집합 동반 이동 시 집합 전체 전달. */ reorderAttributes(entityId: ModelId, attrIds: ModelId[], toIndex: number): void; addIndex(entityId: ModelId, index: EntityIndex): void; updateIndex(entityId: ModelId, indexId: ModelId, patch: Partial): void; removeIndex(entityId: ModelId, indexId: ModelId): void; /** 메서드(operation) 추가 — 신규는 visibility=PACKAGE, order=말미. 생성된 modelId 반환. */ addOperation(entityId: ModelId, partial?: Partial): ModelId | null; updateOperation(entityId: ModelId, operationId: ModelId, patch: Partial): void; removeOperation(entityId: ModelId, operationId: ModelId): void; /** 메서드 행 재정렬 — toIndex = 대상 제거 후 배열 기준 삽입 위치(단일 명령, undo 원자). */ reorderOperations(entityId: ModelId, operationIds: ModelId[], toIndex: number): void; /** 메서드 강조 색 토큰 지정 (null이면 해제). */ setOperationSwatch(entityId: ModelId, operationId: ModelId, token: string | null): void; addAssociation(sourceEntityId: ModelId, targetEntityId: ModelId, sourceColumnRef?: ModelId, targetColumnRef?: ModelId): ModelId | null; /** * 임베드 생성 — 소유자에 EMBED_OWN 표시 속성을 물질화(임베더블 source 컬럼 복사 + `embedded` 마커)하고 * EMBED association(owner EXACTLY_ONE / embeddable EXACTLY_ONE+composition)을 한 composite로 추가한다. * 가드(결정 2 단일): 대상이 JPA_EMBEDDABLE이 아니거나 소유자가 이미 그 임베더블을 임베드했으면 no-op. * 반환은 생성된 association id, 가드 차단 시 null. */ addEmbedAssociation(ownerEntityId: ModelId, embeddableEntityId: ModelId): ModelId | null; /** * 직렬화 타입 «사용» — 소비 엔티티에 `type=<타입명>` 속성을 만든다(캔버스 연결 제스처의 착지점). * ★**관계를 만들지 않는다** — 직렬화 타입은 `type` 참조로 지목되고(2단계 결정 2-ⓐ), 캔버스 점선은 * `planSerializedTypeEdges` 가 그 참조에서 파생하므로 속성만 만들면 선이 자동으로 따라온다. * 반환은 생성된 속성 id, 가드(조회 모드·소유자 잠금·대상이 직렬화 타입 아님) 차단 시 null. */ addSerializedTypeAttribute(ownerEntityId: ModelId, typeEntityId: ModelId): ModelId | null; /** * 임베드 카디널리티 설정 — 임베더블 end multiplicity를 지정 값으로 바꾼다. 컬렉션(`@ElementCollection`) * 여부는 이 multiplicity에서 파생되는 값(권위=multiplicity, 결정 3): `*_MORE`(0..N / 1..N)면 컬렉션, * 그 외(1·0..1)면 단일 `@Embedded`(0..1은 nullable). single↔collection **경계를 넘을 때만** 표시 속성 * 평탄화 컬럼 재구성 + collection 메타 set/clear를 **한 composite**로 묶어 모순 상태(컬렉션인데 평탄화 * 컬럼 잔존)를 구조적으로 차단한다. 단일 내부 전이(1↔0..1)는 평탄화 불변이라 순수 multiplicity patch다. * 컬렉션→단일 복귀 시 owner 슬롯 `@AttributeOverride`는 버린다(결정 3, load-only 철학). * assocId가 EMBED association이 아니거나 이미 해당 multiplicity면 no-op. */ setEmbedCardinality(assocId: ModelId, multiplicity: Multiplicity): void; removeAssociation(assocId: ModelId): void; /** * 식별 관계 토글 — 자식 FK의 identifier 플래그를 동기화하고, 식별자 집합 변동이 * 후손으로 번지면 연쇄 reconcile까지 한 composite로 묶는다(undo/redo 원자). * self 관계는 엔진이 비식별을 강제(INV-3)하므로 토글해도 FK는 비식별로 유지된다. */ setAssociationIdentifying(assocId: ModelId, identifying: boolean): void; updateAssociationEnd(assocId: ModelId, which: 'end1' | 'end2', patch: Partial): void; moveWaypoint(assocId: ModelId, index: number, point: Point): void; addWaypoint(assocId: ModelId, index: number, point: Point): void; removeWaypoint(assocId: ModelId, index: number): void; clearWaypoints(assocId: ModelId): void; addNote(memo?: string, location?: Point): ModelId | null; removeNote(noteId: ModelId): void; moveNote(noteId: ModelId, location: Point): void; resizeNote(noteId: ModelId, size: { width: number; height: number; }): void; setNoteMemo(noteId: ModelId, memo: string): void; /** 노트 색상 토큰 지정 (null이면 해제 → 기본 노트 배경) */ setNoteSwatch(noteId: ModelId, token: string | null): void; /** 노트 → 대상(엔티티) 연결선 추가/삭제 + 꺾은점 편집 */ addNoteConnection(noteId: ModelId, targetRef: ModelId): void; removeNoteConnection(noteId: ModelId, targetRef: ModelId): void; moveNoteConnectionWaypoint(noteId: ModelId, targetRef: ModelId, index: number, point: Point): void; addNoteConnectionWaypoint(noteId: ModelId, targetRef: ModelId, index: number, point: Point): void; removeNoteConnectionWaypoint(noteId: ModelId, targetRef: ModelId, index: number): void; clearNoteConnectionWaypoints(noteId: ModelId, targetRef: ModelId): void; /** 겹침 해소 자동 배치 (그룹 멤버는 박스 안, 미그룹은 아래 격자) */ autoLayout(): void; /** * 복사/붙여넣기 클립보드. 엔티티 선택 → 엔티티 통째(새 modelId·offset), * 속성 선택 → 선택 집합(붙여넣기는 현재 선택 엔티티/부모에 append, 동명 충돌 시 _copy), * 명시 엔티티 집합(다중 선택·그룹 멤버 확장) → 서브그래프(유도 관계·그룹 포함, mergeDiagram 재사용). */ clipboard: Ref; /** * @param entityIds 서브그래프로 복사할 엔티티 id 집합(뷰가 다중 선택·그룹 멤버 확장으로 판단해 전달). * 비어 있거나 미전달이면 현재 선택(단일 엔티티/속성 집합) 복사. 단일 엔티티 복사는 뷰가 무인자로 호출. */ copy(entityIds?: ModelId[]): void; /** @returns 붙여넣은 엔티티 modelId 목록(뷰가 캔버스 다중선택 반영에 사용). 속성 붙여넣기·무동작 시 빈 배열. */ paste(): ModelId[]; /** * 타 다이어그램(비즈모듈)의 선택 서브그래프를 현재 다이어그램에 병합 임포트(G1). * 깊은 복사(새 id) + 경계 강등 + 물리명 충돌 suffix는 순수 `mergeDiagram`이 수행하고, * 여기서는 결과를 기존 다이어그램 우측 빈 영역에 배치해 단일 composite로 실행한다(재전파 없음 — D6). */ importEntities(source: MergeDiagramSource, selectedIds: ModelId[], opts?: { includeRelated?: boolean; /** 이름/물리명 충돌 접미사(mergeDiagram 위임, 기본 `_imported`). 클립보드 붙여넣기는 `_copy`. */ collisionSuffix?: string; /** * 배치 전략. 기본 `'append-right'`(기존 다이어그램 우측 빈 영역 — G1 import). * `{ offset }` 이면 서브그래프를 그 오프셋만큼 통째 시프트(클립보드 붙여넣기 — 원본 근처). */ placement?: 'append-right' | { offset: Point; }; }): ModelId[]; } export declare const EDITOR: InjectionKey; export declare function emptyState(modelId?: string, diagramId?: string): EditorState; export declare function createEditorController(initial?: EditorState, options?: { embeddableCatalog?: EmbeddableCatalog; groupCodeCatalog?: GroupCodeCatalog; predefinedCustomTypeCatalog?: PredefinedCustomTypeCatalog; superClassCatalog?: SuperClassCatalog; languages?: LangCode[]; userLanguage?: LangCode; /** * 프로젝트 기본 언어 — 산출물에 «굳는» 언어(DDL `COMMENT ON` 로케일). 보는 사람에 딸린 * `userLanguage` 를 넘기지 말 것. **미주입 시 폴백하지 않는다**(= 주석 미방출). */ projectLanguage?: LangCode; /** 툴 크롬 언어(콘텐츠 축 `userLanguage`와 별개). 미주입 시 'ko'(무회귀). */ uiLocale?: UiLocale; /** `fromPersisted` 산출 meta(트랙 B 마커 + revSeed). schemaVersion·opWriteEnabled로 opMode·read-only 판정. */ meta?: DiagramMeta; /** * 호스트 op 서비스로 배치를 보내는 transport(`applyEntityDiagramOps` fetch 래퍼). op-mode + 주입 시에만 * 어댑터가 활성(미주입 = dev/test → 어댑터 비활성, full-replace 저장 경로 유지). */ opTransport?: OpSyncTransport; /** * (나) 필드 감사 조회 transport(`entityDiagramHistory.get` fetch 래퍼). 주입 + op-mode 시에만 감사 * 패널 활성(미주입 = dev/test·비-op doc → 패널 비활성). opTransport(쓰기)와 대칭인 읽기 채널. */ historyTransport?: HistoryTransport; /** 시점 복원 transport(`restoreEntityDiagram` 라우트). 주입 + op-mode + editable 시 감사 패널 '복원' 활성. */ restoreTransport?: RestoreTransport; /** * op 전송 실패(4xx/5xx/네트워크로 transport가 throw) 통지 — 진단용(opt-in). 이 콜백은 소실될 원 * 에러를 호스트가 로깅/리포팅하도록 넘긴다. 미주입이어도 어댑터가 콘솔에 남긴다(정상 rev 충돌은 * 이 경로가 아님 — throw만). * ⚠️ **발화 ≠ 프리즈**: 어댑터가 제한 재시도하므로 회복되는 시도도 통지된다(시도마다 1회). * 하드 프리즈는 재시도 소진 후에만 발생하며 `opConflict.kind='missing'`으로 별도 노출된다. */ onOpError?: (err: unknown) => void; /** * 사용자 안내 통지(차단된 동작 등) — 주입 시 호스트가 자신의 토스트/알림 시스템으로 표시하고, * lib 내장 토스트는 표시하지 않는다(호스트에서 서버 에러 토스트 등과 UX 일원화). 미주입(dev * 하니스·미배선 호스트)이면 lib이 `notice` ref로 자체 토스트를 표시한다. `onOpError`와 동형 seam. */ onNotice?: (message: string) => void; /** * 검증 컨텍스트 — 카탈로그/정책 의존 규칙을 깨우는 호스트 주입(미주입 시 해당 규칙 skip). * selectableDataTypes(CHK-TYPE-1)는 코어 상수(SELECTABLE_DATA_TYPES)라 dev/호스트가 즉시 주입 가능. * reservedWords(CHK-NAME-6)·largeEntityThreshold(CHK-DSN-4)는 다이얼렉트·정책 의존이라 호스트 통합 시 주입(현재 보류). */ reservedWords?: ReadonlySet; selectableDataTypes?: ReadonlySet; largeEntityThreshold?: number; /** CHK-DSN-3 초기 활성 여부 — 기본 false. 런타임은 flagEntitiesWithoutRelations로 사용자가 토글. */ flagEntitiesWithoutRelations?: boolean; /** * 검증(validateModel) 재실행 디바운스 창(ms, 기본 250). validateModel은 전체 모델 스캔이라 키 입력마다 * 동기 재실행하면 대형 다이어그램에서 입력 지연의 주요 축(VE-1) — 기본은 변경 신호(revision) 기반 * 트레일링 디바운스. `0` 이하 주입 시 현행 동기 computed 그대로(변경 직후 즉시 일관성이 필요한 * 테스트·소비자용 escape hatch). */ validationDebounceMs?: number; }): EditorController;