@g1cloud/entity-modeler-next 5.0.0-alpha.1 → 5.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,42 +1,47 @@
1
1
  # entity-modeler-next
2
2
 
3
- Vue 3 기반 비주얼 엔티티 모델링 / 클래스 다이어그램 에디터.
4
- 레거시 GWT/Vaadin 7 모듈(`bluework-entitymodeler`) 후속이며, `bluework4-tool`(Nuxt/Vue/Mongo)에 임베드된다.
3
+ Vue 3 visual entity-modeling / class-diagram editor.
4
+ Distributed as a library package (`@g1cloud/entity-modeler-next`) and embedded in `bluework4-tool` (Nuxt/Vue/Mongo).
5
5
 
6
- ## 스택
6
+ ## Stack
7
7
 
8
- TypeScript · Vue 3 · **Vue Flow** · Pinia(비강제) · Vite(library mode) · Vitest · view는 `@g1cloud/open-bluesea-core`(BS) 컴포넌트 기반
8
+ TypeScript · Vue 3 · **Vue Flow** · Pinia (opt-in) · Vite (library mode) · Vitest · views built on `@g1cloud/open-bluesea-core` (BS) components
9
9
 
10
- ## 구조
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add @g1cloud/entity-modeler-next
14
+ ```
15
+
16
+ Requires the peer dependencies `vue ^3.5.0` and `@g1cloud/open-bluesea-core` (installed by the host).
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { EntityModeler } from '@g1cloud/entity-modeler-next'
22
+ import '@g1cloud/entity-modeler-next/style.css' // required — components render unstyled without it
23
+ ```
24
+
25
+ ## Structure
11
26
 
12
27
  ```
13
28
  src/
14
- core/ 논리/레이아웃 타입(분리)·resolve(정합성)·routing·autolayout·propagation·validation·타입 카탈로그. 프레임워크 무관
15
- command/ Command(do/undo)·CommandStack·op/opSync(시맨틱 op emit + CAS 동시성)
16
- editor/ reactive controller (CommandStack 래핑 컴포저블, EDITOR inject )
17
- view/ Vue Flow 컴포넌트 (DiagramCanvas·EntityNode·GroupNode·AssociationEdge·PropertyPanel·ValidationPanel )
18
- adapter/ 레거시 PDiagram 매핑 (persisted v1/v2·fromPersisted/toPersisted/toPersistedV2 라운드트립)
19
- agent/ 자연어→op 트랙 (symbolicOp·resolver·schema·buildAgentBatch)
20
- dev/ 데모 하니스 (라이브러리 빌드에서 제외)
29
+ core/ logical/layout types (separated) · resolve (consistency) · routing · autolayout · propagation · validation · type catalogs. Framework-agnostic
30
+ command/ Command (do/undo) · CommandStack · op/opSync (semantic op emit + CAS concurrency)
31
+ editor/ reactive controller (composable wrapping CommandStack, EDITOR inject key)
32
+ view/ Vue Flow components (DiagramCanvas · EntityNode · GroupNode · AssociationEdge · PropertyPanel · ValidationPanel, etc.)
33
+ adapter/ storage-schema mapping (persisted v1/v2 · fromPersisted/toPersisted/toPersistedV2 round-trip)
34
+ agent/ natural-language → op track (symbolicOp · resolver · schema · buildAgentBatch)
35
+ dev/ demo harness (excluded from the library build)
21
36
  ```
22
37
 
23
- 핵심 설계: **레이아웃이 논리 모델을 `modelId`로 참조(단방향)**. 논리 모델엔 geo/waypoint/style이 없다.
24
- **현재 구조·상태·문서 인덱스의 정본은 루트 `CLAUDE.md`** (저장 스키마 영향은 `docs/M0-schema-impact.md`).
38
+ Core design: **layout references the logical model by `modelId` (one-way)**. The logical model carries no geo/waypoint/style.
25
39
 
26
- ## 스크립트
40
+ ## Scripts
27
41
 
28
42
  ```bash
29
- pnpm dev # 스파이크/데모 dev 서버
30
- pnpm test # Vitest (core 단위 테스트)
31
- pnpm typecheck # vue-tsc
32
- pnpm build # 라이브러리 빌드 (dist/)
43
+ pnpm dev # demo harness dev server (src/dev)
44
+ pnpm test # Vitest
45
+ pnpm typecheck # vue-tsc --noEmit
46
+ pnpm build # library build (dist/)
33
47
  ```
34
-
35
- ## M0 스파이크 검증 포인트 (브라우저)
36
-
37
- `pnpm dev` 후 다음을 확인:
38
- - 엔티티 노드가 속성 행·PK 마커와 함께 렌더되는가
39
- - 관계선이 **직각 라우팅**으로 그려지는가
40
- - 파란 **waypoint**를 드래그하면 경로가 갱신되는가
41
- - 세그먼트 중점(회색 원)을 **더블클릭**하면 waypoint가 추가되는가
42
- - 관계선 끝이 특정 **컬럼 행(앵커)**에 붙는가 (Order.id ↔ OrderLine.orderId)
@@ -47,6 +47,10 @@ export interface PAttribute {
47
47
  identifier: boolean;
48
48
  notNull: boolean;
49
49
  transient_?: boolean;
50
+ /** Hibernate @NaturalId(자연키/비즈니스 키). 속성 레벨. v1 레거시엔 없는 신규 옵션. */
51
+ naturalId?: boolean;
52
+ /** @NaturalId(mutable=true). undefined/false=불변(Hibernate 기본). */
53
+ naturalIdMutable?: boolean;
50
54
  description?: MultiLangText;
51
55
  attributeGroup?: string;
52
56
  groupCode?: string;
@@ -129,6 +133,7 @@ export interface PEntity {
129
133
  geo?: PGeo;
130
134
  style?: PStyle;
131
135
  collapsed?: boolean;
136
+ locked?: boolean;
132
137
  }
133
138
  export interface PIndex {
134
139
  modelId: string;
@@ -165,6 +170,7 @@ export interface PGroup {
165
170
  notes?: PNote[];
166
171
  geo?: PGeo;
167
172
  style?: PStyle;
173
+ locked?: boolean;
168
174
  }
169
175
  /**
170
176
  * 레거시 NoteModel 직렬화 형태. 연결선은 `ownedConnections`(end1/end2/locations 구조)로 저장되고
@@ -1,4 +1,4 @@
1
- import { LogicalModel, ModelId } from '../core/types';
1
+ import { EmbeddableCatalog, LogicalModel, ModelId } from '../core/types';
2
2
  import { OpShape } from '../command/op';
3
3
  import { AssocHandle, Handle, SymbolicOp } from './symbolicOp';
4
4
  /** 해소 실패 — 어느 심볼릭 op(`opIndex`)의 어떤 핸들이 0개/복수 매칭인지. */
@@ -35,6 +35,100 @@ export type ResolveError = {
35
35
  code: 'self-identifying';
36
36
  opIndex: number;
37
37
  handle: AssocHandle;
38
+ }
39
+ /** `type:'GroupCodeEnum'` 인데 `groupCode` 미동반 — groupCode 바인딩 없는 깨진 파생 타입 방지(B3). */
40
+ | {
41
+ code: 'groupcode-required';
42
+ opIndex: number;
43
+ entity: Handle;
44
+ handle: Handle;
45
+ }
46
+ /** index.add 컬럼 핸들이 엔티티 내 dbAttr와 0개 매칭. */
47
+ | {
48
+ code: 'column-not-found';
49
+ opIndex: number;
50
+ entity: Handle;
51
+ handle: Handle;
52
+ }
53
+ /** index.add 컬럼 핸들이 복수 dbAttr와 매칭(예: 속성명 지정인데 다중 컬럼 임베드/Money). */
54
+ | {
55
+ code: 'column-ambiguous';
56
+ opIndex: number;
57
+ entity: Handle;
58
+ handle: Handle;
59
+ matches: ModelId[];
60
+ }
61
+ /** index.update/remove 핸들이 엔티티 내 인덱스와 0개/복수 매칭. */
62
+ | {
63
+ code: 'index-not-found';
64
+ opIndex: number;
65
+ entity: Handle;
66
+ handle: Handle;
67
+ } | {
68
+ code: 'index-ambiguous';
69
+ opIndex: number;
70
+ entity: Handle;
71
+ handle: Handle;
72
+ matches: ModelId[];
73
+ }
74
+ /** operation.update/remove 핸들이 엔티티 내 operation과 0개/복수 매칭. */
75
+ | {
76
+ code: 'operation-not-found';
77
+ opIndex: number;
78
+ entity: Handle;
79
+ handle: Handle;
80
+ } | {
81
+ code: 'operation-ambiguous';
82
+ opIndex: number;
83
+ entity: Handle;
84
+ handle: Handle;
85
+ matches: ModelId[];
86
+ }
87
+ /**
88
+ * index.update patch에 `columns` 키 — columnRef(dbAttr modelId)는 사람 핸들로 표현 불가라 패스스루 시
89
+ * 깨진 인덱스가 된다. 컬럼 변경은 index.remove + index.add(컬럼 핸들 해소 구현)로 유도(거부 권고안).
90
+ */
91
+ | {
92
+ code: 'index-columns-patch-unsupported';
93
+ opIndex: number;
94
+ entity: Handle;
95
+ handle: Handle;
96
+ }
97
+ /**
98
+ * 같은 배치에서 `entity.add`로 추가되는 엔티티를 자식/관계 op가 핸들로 참조(A 백스톱 가드).
99
+ * mongo arrayFilter는 update 선이미지에 평가되므로 갓 push된 엔티티에 닿는 child push가 silent no-op이 된다.
100
+ * 신규 엔티티의 자식은 `attribute.add`/`index.add`/`operation.add` 분리가 아니라 `entity.add` spec에 inline fold할 것
101
+ * (관계는 inline 불가 → 엔티티 배치 확정 후 별도 배치). entity-not-found 대신 의도를 드러내는 명시 에러.
102
+ */
103
+ | {
104
+ code: 'pending-entity-ref';
105
+ opIndex: number;
106
+ handle: Handle;
107
+ }
108
+ /**
109
+ * embeddableOverrides가 있으나 `type`이 주입된 임베더블 카탈로그의 어떤 엔트리와도 매칭되지 않음
110
+ * (카탈로그 미주입이거나 embeddable 아닌 type). 서브컬럼 오버라이드 silent drop 방지.
111
+ */
112
+ | {
113
+ code: 'embeddable-type-unresolved';
114
+ opIndex: number;
115
+ entity: Handle;
116
+ handle: Handle;
117
+ }
118
+ /** EMBED_PREDEF SHARED_REF 서브컬럼의 공유 대상 물리명이 소속 엔터티 내 컬럼과 0개 매칭. */
119
+ | {
120
+ code: 'shared-column-target-not-found';
121
+ opIndex: number;
122
+ entity: Handle;
123
+ handle: Handle;
124
+ }
125
+ /** EMBED_PREDEF SHARED_REF 공유 대상 물리명이 복수 컬럼과 매칭(모호). */
126
+ | {
127
+ code: 'shared-column-target-ambiguous';
128
+ opIndex: number;
129
+ entity: Handle;
130
+ handle: Handle;
131
+ matches: ModelId[];
38
132
  };
39
133
  export type ResolveResult = {
40
134
  ok: true;
@@ -46,6 +140,12 @@ export type ResolveResult = {
46
140
  export interface ResolverOptions {
47
141
  /** modelId 발급기(주입 시 테스트 결정성). 기본 newId. */
48
142
  mkId?: () => ModelId;
143
+ /**
144
+ * predefined 임베더블 카탈로그(호스트 주입). `attribute.add`/inline fold의 `spec.type`이 엔트리 `type`과
145
+ * 매칭되면 EMBED_PREDEF(다중 dbAttr)로 확장한다. 미주입(기본 [])이면 확장 없이 NORMAL — 카탈로그 없이는
146
+ * type이 embeddable인지 판정 불가하므로 graceful degrade(현행 동작 유지). 서버 resolve 경로가 주입 책임.
147
+ */
148
+ embeddableCatalog?: EmbeddableCatalog;
49
149
  }
50
150
  /**
51
151
  * 심볼릭 op 배치를 OpShape 배치로 해소한다.
@@ -1,4 +1,4 @@
1
- import { ClassStereotype, Multiplicity } from '../core/types';
1
+ import { ClassStereotype, MultiLangText, Multiplicity, OperationVisibility } from '../core/types';
2
2
  /**
3
3
  * 요소 지정 핸들 — modelId 대신 이름/물리명.
4
4
  * 엔티티: 물리명(`table.physicalName`) 우선, 논리명(`name`) 보조 — 논리명은 모델 내 중복 허용(CHK-NAME-3)이라 모호 가능.
@@ -19,21 +19,53 @@ export interface AssocHandle {
19
19
  from: Handle;
20
20
  to: Handle;
21
21
  }
22
- /** entity.add 페이로드 — resolver가 modelId 발급 + 빈 골격 조립. */
22
+ /**
23
+ * entity.add 페이로드 — resolver가 modelId 발급 + 골격 조립.
24
+ *
25
+ * **inline fold**: 신규 엔티티의 자식(속성·인덱스·operation)은 별도 `attribute.add` 등으로 보내지 않고
26
+ * 여기에 inline 배열로 담는다. resolver가 완성형 엔티티를 한 `entity.add`로 조립해 단일 `$push`로 들어간다.
27
+ * (별도 child op는 mongo arrayFilter 선이미지 한계로 *같은 배치 신규 엔티티*에 닿지 못해 silent no-op이 된다 —
28
+ * 그래서 신규 엔티티 자식은 op 분리가 아니라 inline이 정공법. 분리 emit은 resolver가 `pending-entity-ref`로 거부.)
29
+ * `attribute.add`/`index.add`/`operation.add` op은 *기존* 엔티티에 자식을 추가하는 용도로 남는다.
30
+ */
23
31
  export interface EntitySpec {
24
32
  name: string;
25
33
  /** 기본 'JPA_ENTITY'. */
26
34
  stereotype?: ClassStereotype;
27
35
  /** 설정 시 `table.physicalName`. */
28
36
  physicalName?: string;
37
+ /**
38
+ * JPA `@Version`(낙관적 락) 사용 여부 → `Entity.jpaAttrs.useVersion`. 코드의 `@Version` 필드는 별도 속성으로
39
+ * 모델링하지 않고 이 엔티티 레벨 플래그로 흡수한다(버전 컬럼은 호스트 코드젠 소유). resolver가 true일 때만
40
+ * `jpaAttrs.useVersion:true`를 세팅(false/미설정은 생략 — round-trip diff 방지). *기존* 엔티티에 켤 땐 이 스펙이
41
+ * 아니라 `entity.update` patch의 **dotted key** `{'jpaAttrs.useVersion': true}`를 써야 형제 jpaAttrs 플래그를
42
+ * 통째 $set로 덮어쓰지 않는다(호스트 opInterpreter는 patch를 per-key $set).
43
+ */
44
+ useVersion?: boolean;
45
+ /** inline 속성 — 배열 순서가 곧 `order`. GroupCodeEnum+groupCode 규칙은 `attribute.add`와 동일하게 강제. */
46
+ attributes?: AttrSpec[];
47
+ /** inline 인덱스 — 컬럼 핸들은 *이 엔티티가 inline으로 만드는* dbAttr 내에서 해소된다(속성 inline 동반 전제). */
48
+ indexes?: IndexSpec[];
49
+ /** inline 도메인 메서드 — 배열 순서가 곧 `order`. */
50
+ operations?: OperationSpec[];
29
51
  }
30
52
  /** attribute.add 페이로드 — resolver가 modelId 발급 + dbAttr 골격 조립. */
31
53
  export interface AttrSpec {
32
54
  name: string;
33
- /** Java 타입(예: 'Long', 'String'). */
55
+ /**
56
+ * Java 타입(예: 'Long', 'String'). 카탈로그 저장값.
57
+ * `'GroupCodeEnum'`은 파생 타입이라 **`groupCode` 동반 필수** — 없으면 resolver가 거부(groupCode 바인딩
58
+ * 없는 깨진 파생 타입 방지). groupCode 지정 시 코드성 속성으로 해소된다.
59
+ */
34
60
  type: string;
35
61
  identifier?: boolean;
36
62
  notNull?: boolean;
63
+ /**
64
+ * JPA `@Transient` — 비영속 속성. true면 `Attribute.transient:true`(속성 레벨 플래그). 컬럼을 만들지 않는
65
+ * 속성이므로 physicalName/dataType 등 컬럼 필드는 함께 주지 않는다(주면 dbAttr가 생성됨). false/미설정은
66
+ * 생략(round-trip diff 방지). *기존* 속성에 켤 땐 `attribute.update` patch `{transient:true}`로도 가능.
67
+ */
68
+ transient?: boolean;
37
69
  /** 설정 시 단일 dbAttr 컬럼 생성(미설정이면 name을 물리명으로). */
38
70
  physicalName?: string;
39
71
  /** dbAttr 물리 타입(예: 'BIGINT'). physicalName/dataType/length/scale 중 하나라도 있으면 dbAttr 생성. */
@@ -42,6 +74,59 @@ export interface AttrSpec {
42
74
  length?: number;
43
75
  /** dbAttr 소수 자릿수(DECIMAL/NUMERIC scale). length 동반이 일반적. 0/미설정은 미지정. */
44
76
  scale?: number;
77
+ /**
78
+ * 코드성 속성의 group code 바인딩(레거시 AttributeModel.groupCode). `type:'GroupCodeEnum'`이면 필수.
79
+ * 모델→모델 복사·코드 동기화에서 GroupCodeEnum 속성을 온전히 재현하는 핵심 필드(없으면 깨진 파생 타입).
80
+ */
81
+ groupCode?: string;
82
+ /** 속성 기본값(레거시 AttributeModel.defaultValue, 문자열 패스스루). */
83
+ defaultValue?: string;
84
+ /** 속성 분류 그룹(레거시 attributeGroup, 예: 'code'·'amt'). 표시·분류 전용 패스스루. */
85
+ attributeGroup?: string;
86
+ /** 속성 설명(다국어, 레거시 AttributeModel.description). */
87
+ description?: MultiLangText;
88
+ /**
89
+ * dbAttr 컬럼 논리명(다국어, 예: {ko:'체크아웃유형'}). 한글 업무명이 모델→모델 복사에서 보존되는 위치 —
90
+ * 속성 자체는 단일 식별자 `name`만 갖고, 다국어 업무명은 컬럼에 붙는다. 컬럼 생성 시에만 의미.
91
+ */
92
+ columnLogicalName?: MultiLangText;
93
+ /** dbAttr UNIQUE 제약. */
94
+ unique?: boolean;
95
+ /** dbAttr 갱신 가능 여부(JPA @Column(updatable=)). */
96
+ updatable?: boolean;
97
+ /**
98
+ * predefined 임베더블 서브컬럼 오버라이드 — `type`이 호스트 주입 임베더블 카탈로그 엔트리와 매칭될 때만
99
+ * 의미. resolver가 카탈로그 fields를 그대로 펼친 뒤(EMBED_PREDEF, 다중 dbAttr) 이 배열을 **필드명**으로
100
+ * 매칭해 서브컬럼을 덮어쓴다(JPA `@AttributeOverride` 대응). 미설정 서브컬럼은 카탈로그 기본을 상속한다.
101
+ * 카탈로그 미매칭 type에 이 배열을 주면 resolver가 `embeddable-type-unresolved`로 거부(silent drop 방지).
102
+ */
103
+ embeddableOverrides?: EmbeddableColumnOverride[];
104
+ }
105
+ /**
106
+ * predefined 임베더블(EMBED_PREDEF)의 개별 서브컬럼 오버라이드. 매칭 키는 카탈로그 필드명
107
+ * (`EmbeddableCatalogField.name`, 예: 'amount'·'currency') — 순서 독립. 미설정 필드는 오버라이드 없음(카탈로그 상속).
108
+ */
109
+ export interface EmbeddableColumnOverride {
110
+ /** 대상 카탈로그 필드명(EmbeddableCatalogField.name). 예: 'amount', 'currency'. */
111
+ field: string;
112
+ /** 서브컬럼 물리명 오버라이드(@AttributeOverride column name). 미설정 시 카탈로그 기본 컬럼명 상속. */
113
+ physicalName?: string;
114
+ /** 서브컬럼 길이 오버라이드. */
115
+ length?: number;
116
+ /** 서브컬럼 소수 자릿수 오버라이드. */
117
+ scale?: number;
118
+ /** JPA @Column(insertable=). OWN 컬럼에만 저장(SHARED_REF는 타깃 파생). */
119
+ insertable?: boolean;
120
+ /** JPA @Column(updatable=). OWN 컬럼에만 저장(SHARED_REF는 타깃 파생). */
121
+ updatable?: boolean;
122
+ /**
123
+ * SHARED_REF — 이 서브컬럼이 자체 물리 컬럼을 갖지 않고 같은 엔터티의 다른 컬럼(**물리명**)을 공유해
124
+ * read-only 투영됨(대표: Money의 currency가 별도 스칼라 통화 컬럼 공유). 설정 시 resolver가 그 물리명을
125
+ * 소속 엔터티 내 기존 컬럼 modelId로 해소해 `DbColumn.sharedColumnRef`로 바인딩하고 physicalName·
126
+ * insertable·updatable은 저장하지 않는다(타깃 파생). 타깃 컬럼은 해소 시점 존재해야 한다(부재=거부).
127
+ * JPA 코드에서 currency 서브컬럼의 `insertable=false && updatable=false`가 이 모드의 시그니처.
128
+ */
129
+ sharedColumnPhysicalName?: string;
45
130
  }
46
131
  /** association.add 페이로드 — from/to 엔티티는 resolver가 해소, modelId 발급 + end 조립. */
47
132
  export interface AssocSpec {
@@ -58,6 +143,66 @@ export interface AssocSpec {
58
143
  /** end2 composition(부모가 자식 생명주기 소유). */
59
144
  composition?: boolean;
60
145
  }
146
+ /**
147
+ * group.add 페이로드 — 엔티티 멤버를 묶는 논리 그룹(레거시 LogicalGroup). resolver가 modelId 발급 +
148
+ * 멤버 핸들을 entity modelId(`memberEntityRefs`)로 해소. 그룹 멤버십은 값 포함이 아닌 **id 참조**(엔티티는
149
+ * 그룹과 독립 존재). 멤버는 *기존* 엔티티여야 한다 — 같은 배치 신규 엔티티(entity.add)는 닿지 못하므로
150
+ * (top-level 그룹은 inline fold 불가) resolver가 `pending-entity-ref`로 거부, 엔티티 배치 확정 후 별도 배치로.
151
+ * 그룹 박스 좌표(groupLayout)는 호스트가 멤버 엔티티 레이아웃을 감싸 incidental로 첨부(C1 동형).
152
+ */
153
+ export interface GroupSpec {
154
+ /** 그룹 논리명(다국어, 예: {ko:'주문'}). 패키지 기반 그룹이면 보통 패키지 leaf의 업무명. */
155
+ name?: MultiLangText;
156
+ /** 자바 패키지명 등 그룹의 물리 식별(레거시 LogicalGroup.packageName). */
157
+ packageName?: string;
158
+ /** 그룹 설명(자유 텍스트 — 모듈/도메인 경계 의도 메모). */
159
+ description?: string;
160
+ /** DDL 생성 제외 여부(기본 false). */
161
+ excludeDDLGeneration?: boolean;
162
+ /** 멤버 엔티티 핸들(물리명 우선/논리명 보조). resolver가 entity modelId로 해소. */
163
+ members: Handle[];
164
+ }
165
+ /**
166
+ * index.add 컬럼 지정 — 인덱스 컬럼은 dbAttr(컬럼) modelId(`IndexColumn.columnRef`)를 참조하나,
167
+ * 핸들은 사람이 지정 가능한 물리명/속성명이다. resolver가 소속 엔티티 내에서 dbAttr modelId로 해소한다.
168
+ */
169
+ export interface IndexColumnSpec {
170
+ /**
171
+ * 컬럼 핸들 — 컬럼 물리명(`dbAttrs[].physicalName`) 우선, 속성명(`name`) 보조.
172
+ * 속성명으로 지정 시 그 속성이 단일 컬럼이어야 한다(다중 dbAttr=임베드/Money는 모호 → resolver 거부).
173
+ */
174
+ column: Handle;
175
+ /** 내림차순 정렬 컬럼(기본 false=오름차순). */
176
+ descending?: boolean;
177
+ }
178
+ /** index.add 페이로드 — resolver가 modelId 발급 + 컬럼 핸들을 dbAttr modelId로 해소. */
179
+ export interface IndexSpec {
180
+ name: string;
181
+ /** UNIQUE 인덱스 여부(기본 false). */
182
+ unique?: boolean;
183
+ /** 인덱스 컬럼(순서 의미 있음). 비어 있으면 컬럼 없는 인덱스(허용 — 값 품질은 검증 엔진). */
184
+ columns: IndexColumnSpec[];
185
+ /** 인덱스 설명(레거시 IndexModel.description, 평문). */
186
+ description?: string;
187
+ /** 인덱스 파라미터(레거시 IndexModel.parameters, 패스스루). */
188
+ parameters?: string;
189
+ }
190
+ /**
191
+ * operation.add 페이로드 — 도메인 메서드(레거시 OperationModel). resolver가 modelId 발급 + order 누적.
192
+ * 파라미터·반환 타입은 별도 모델 필드가 아니라 `sourceCode`(Java 메서드 본문 텍스트)에 담긴다.
193
+ * legacyRaw(소스 엔티티 parent back-ref·properties)는 복사 시 stale이 되므로 op로 다루지 않는다.
194
+ */
195
+ export interface OperationSpec {
196
+ name: string;
197
+ /** 가시성(기본 미설정 → 호스트/렌더 기본). */
198
+ visibility?: OperationVisibility;
199
+ /** Java 메서드 본문(멀티라인, 파라미터·반환 시그니처 포함). */
200
+ sourceCode?: string;
201
+ /** 메서드 설명(다국어). 레거시 직렬화는 평문이라 어댑터가 평문↔{ko} 변환(types.ts Operation 주석). */
202
+ description?: MultiLangText;
203
+ /** 개인정보 속성 유형(레거시 personalInfoAttributeType 패스스루). */
204
+ personalInfoAttributeType?: string;
205
+ }
61
206
  /**
62
207
  * LLM이 산출하는 단일 심볼릭 op. `kind`는 op.ts 어휘와 1:1(로케이터만 심볼릭).
63
208
  * `patch`는 OpShape.patch와 동형(부분 patch, 그대로 전달 — 값 품질은 검증 엔진이 사후 경고).
@@ -100,10 +245,39 @@ export type SymbolicOp = {
100
245
  association: AssocHandle;
101
246
  end: 'from' | 'to';
102
247
  patch: Record<string, unknown>;
248
+ } | {
249
+ kind: 'index.add';
250
+ entity: Handle;
251
+ spec: IndexSpec;
252
+ } | {
253
+ kind: 'index.update';
254
+ entity: Handle;
255
+ index: Handle;
256
+ patch: Record<string, unknown>;
257
+ } | {
258
+ kind: 'index.remove';
259
+ entity: Handle;
260
+ index: Handle;
261
+ } | {
262
+ kind: 'operation.add';
263
+ entity: Handle;
264
+ spec: OperationSpec;
265
+ } | {
266
+ kind: 'operation.update';
267
+ entity: Handle;
268
+ operation: Handle;
269
+ patch: Record<string, unknown>;
270
+ } | {
271
+ kind: 'operation.remove';
272
+ entity: Handle;
273
+ operation: Handle;
274
+ } | {
275
+ kind: 'group.add';
276
+ spec: GroupSpec;
103
277
  };
104
278
  /**
105
279
  * v1이 다루는 심볼릭 op 종류의 닫힌 집합(단일 출처). resolver·JSON Schema(`schema.ts`)가 공유한다.
106
280
  * 아래 컴파일타임 단언이 이 튜플과 `SymbolicOp['kind']`의 일치를 강제 — 한쪽만 늘리면 타입 에러.
107
281
  */
108
- export declare const SYMBOLIC_OP_KINDS: readonly ["entity.add", "entity.update", "entity.remove", "attribute.add", "attribute.update", "attribute.remove", "association.add", "association.remove", "association.update", "associationEnd.update"];
282
+ export declare const SYMBOLIC_OP_KINDS: readonly ["entity.add", "entity.update", "entity.remove", "attribute.add", "attribute.update", "attribute.remove", "association.add", "association.remove", "association.update", "associationEnd.update", "index.add", "index.update", "index.remove", "operation.add", "operation.update", "operation.remove", "group.add"];
109
283
  export type SymbolicOpKind = (typeof SYMBOLIC_OP_KINDS)[number];
@@ -21,6 +21,8 @@ export declare function resizeEntity(entityId: ModelId, size: {
21
21
  export declare function setEntitySwatch(entityId: ModelId, token: string | null): Command;
22
22
  /** 엔티티 접기/펼치기 (레이아웃만 변경 — 속성/메서드 칸 렌더 게이트) */
23
23
  export declare function setEntityCollapsed(entityId: ModelId, collapsed: boolean): Command;
24
+ /** 엔티티 편집 잠금 토글 (레이아웃만 변경 — setEntityCollapsed 동형, LWW) */
25
+ export declare function setEntityLocked(entityId: ModelId, locked: boolean): Command;
24
26
  /**
25
27
  * 활성 다이어그램 전체 엔티티 접기/펼치기 (D2) — 사전 per-entity collapsed를 캡처한 단일 커맨드.
26
28
  * N개 개별 커맨드가 아니라 한 번의 undo로 각 엔티티의 직전 상태를 그대로 복원한다(혼재 상태 보존).
@@ -40,6 +42,10 @@ export declare function resizeGroup(groupId: ModelId, size: {
40
42
  width: number;
41
43
  height: number;
42
44
  }): Command;
45
+ /** 그룹 색상 변경 (레이아웃만 변경 — setEntitySwatch와 동형) */
46
+ export declare function setGroupSwatch(groupId: ModelId, token: string | null): Command;
47
+ /** 그룹 편집 잠금 토글 (레이아웃만 변경 — setEntityLocked 거울, LWW) */
48
+ export declare function setGroupLocked(groupId: ModelId, locked: boolean): Command;
43
49
  export declare function addAttribute(entityId: ModelId, attribute: Attribute): Command;
44
50
  /** 속성 필드 부분 수정 (name·type·dataType·dbAttrs·identifier·notNull 등). 얕은 patch. */
45
51
  export declare function updateAttribute(entityId: ModelId, attributeId: ModelId, patch: Partial<Attribute>): Command;
@@ -89,6 +95,8 @@ export declare function resizeNote(noteId: ModelId, size: {
89
95
  height: number;
90
96
  }): Command;
91
97
  export declare function setNoteMemo(noteId: ModelId, memo: string): Command;
98
+ /** 노트 색상 변경 (레이아웃만 변경 — setEntitySwatch와 동형) */
99
+ export declare function setNoteSwatch(noteId: ModelId, token: string | null): Command;
92
100
  /**
93
101
  * 노트를 그룹 박스 멤버로 편입(레이아웃 GroupLayout.memberNoteRefs). 이미 멤버면 no-op.
94
102
  * addEntityToGroup과 거울이나 대상이 논리(memberEntityRefs)가 아닌 레이아웃이다 — 노트가 순수
@@ -10,3 +10,48 @@ export declare function computeAutoLayout(logical: LogicalModel, layout: Diagram
10
10
  id: ModelId;
11
11
  location: Point;
12
12
  }[];
13
+ /**
14
+ * 높이 인지(height-aware) 박스 패킹 — 신규 엔티티 다수를 겹침 없이 격자 배치 (순수 함수, 엔진 무관).
15
+ *
16
+ * `computeAutoLayout`(고정 셀 격자)는 모든 엔티티가 같은 높이라는 가정이라, 속성 수가 많은 박스가
17
+ * 아랫줄을 침범한다. 이 함수는 박스마다 **속성 수에 비례한 높이를 추정**하고, 열별로 누적 Y(`colBottom`)를
18
+ * 추적해 다음 박스를 그 아래에 둔다 → 어떤 박스 높이 분포에서도 겹침이 구조적으로 발생하지 않는다.
19
+ *
20
+ * 배치 순서(좌→우, 위→아래 라운드로빈)는 입력 순서를 보존한다. 높이 추정은 의도적으로 넉넉히
21
+ * (간격이 뜨는 건 무해하나 겹침은 깨져 보임) — EntityNode가 `min-height: min-content`로 콘텐츠 구동이라
22
+ * 픽셀 정확이 아닌 선형 추정으로 충분하다.
23
+ *
24
+ * 좌표는 절대좌표. 기존 레이아웃 하단 아래에서 시작하려면 `baseY`를 넘긴다(호출자가 계산).
25
+ */
26
+ export interface EntityBoxSpec {
27
+ id: ModelId;
28
+ /** 엔티티의 속성(attribute) 개수 — 높이 추정 입력. */
29
+ attributeCount: number;
30
+ }
31
+ export interface PackedEntityBox {
32
+ id: ModelId;
33
+ location: Point;
34
+ size: {
35
+ width: number;
36
+ height: number;
37
+ };
38
+ }
39
+ export interface PackBoxesOptions {
40
+ cols: number;
41
+ boxWidth: number;
42
+ /** 박스 간(가로·세로) 여백. */
43
+ gap: number;
44
+ baseX: number;
45
+ baseY: number;
46
+ /** 헤더(스테레오타입+이름) 추정 높이. */
47
+ headerHeight: number;
48
+ /** 속성 1행 추정 높이. */
49
+ rowHeight: number;
50
+ /** 헤더·행 외 하단 여유(연산/인덱스 섹션 등) 추정 높이. */
51
+ footerHeight: number;
52
+ /** 박스 최소 높이(속성 0개여도 이 높이는 확보). */
53
+ minHeight: number;
54
+ }
55
+ /** 속성 수 → 박스 추정 높이 (넉넉히 over-reserve). */
56
+ export declare function estimateEntityHeight(attributeCount: number, opts?: Partial<PackBoxesOptions>): number;
57
+ export declare function packEntityBoxes(boxes: EntityBoxSpec[], options?: Partial<PackBoxesOptions>): PackedEntityBox[];
@@ -31,6 +31,15 @@ export declare function dbTypeLabel(value: string): string;
31
31
  * TEXT류(LONGVARCHAR·CLOB·BLOB)·고정 폭 타입(INTEGER·DATE·BOOLEAN 등)은 길이 무의미라 제외.
32
32
  */
33
33
  export declare const LENGTH_REQUIRED_DATA_TYPES: ReadonlySet<string>;
34
+ /**
35
+ * scale(소수점 이하 자릿수)이 의미를 갖는 타입 — 정확 수치형(NUMERIC·DECIMAL)뿐이다. 여기서 length는
36
+ * precision(전체 자릿수), scale은 그중 소수부. VARCHAR·CHAR 등 문자/이진 길이 타입과 정수·날짜·불리언
37
+ * 같은 고정 폭 타입엔 scale이 무의미하고, 근사 수치형(FLOAT·REAL·DOUBLE)은 precision만 가질 뿐 scale이 없다.
38
+ *
39
+ * 편집 UI가 이 집합 밖 타입에서 scale 입력을 비활성화하는 어포던스의 단일 출처(타입 메타데이터는 이 카탈로그
40
+ * 소유 — LENGTH_REQUIRED_DATA_TYPES와 대칭). 타입을 이 집합 밖으로 바꾸면 무의미해진 scale은 자동 환원한다.
41
+ */
42
+ export declare const SCALE_APPLICABLE_DATA_TYPES: ReadonlySet<string>;
34
43
  /**
35
44
  * dataType 셀렉트 items — 실 컬럼 후보 + 현재값 폴백 합성.
36
45
  *
@@ -0,0 +1,8 @@
1
+ import { LogicalModel } from './types';
2
+ /**
3
+ * 논리 모델 내 전 레거시 Money 속성을 신규 Money embeddable로 승격한 **새 모델**을 반환(순수).
4
+ * 대상이 없으면 입력 참조를 그대로 반환(no-op). 레이아웃은 논리 modelId만 참조하고 dbAttr을 참조하지
5
+ * 않으므로 무변경 — 호출자(useMigrateEntityDiagram)는 `{ ...state, logical: migrateLegacyMoney(state.logical) }`.
6
+ * amount 슬롯이 레거시 컬럼 modelId를 보존하므로 인덱스(IndexColumn.columnRef) 참조도 안전.
7
+ */
8
+ export declare function migrateLegacyMoney(logical: LogicalModel): LogicalModel;
@@ -0,0 +1,41 @@
1
+ import { ModelId } from './types';
2
+ import { FieldAuditRecord } from './projectFieldAudit';
3
+ import { EntityAuditSubjectKind } from './projectEntityAudit';
4
+ export type ActionSubjectKind = EntityAuditSubjectKind | 'association' | 'associationEnd' | 'group';
5
+ /** 무엇이 바뀐 대상인가. entityRef는 자식(속성/연산/인덱스) subject의 부모(이름 조회·복원 스코프). */
6
+ export interface ActionSubject {
7
+ kind: ActionSubjectKind;
8
+ ref: ModelId;
9
+ entityRef?: ModelId;
10
+ end?: 'end1' | 'end2';
11
+ }
12
+ /** 한 대상의 한 필드 변경(old→new / tombstone). */
13
+ export interface ActionFieldChange {
14
+ field: string;
15
+ value: unknown;
16
+ oldValue?: unknown;
17
+ removed?: boolean;
18
+ }
19
+ /** 한 seq에서 한 대상에 일어난 액션 — verb + 변경 필드들(born=추가 요약, update=필드별). */
20
+ export interface SubjectAction {
21
+ subject: ActionSubject;
22
+ verb: 'add' | 'update' | 'remove';
23
+ fields: ActionFieldChange[];
24
+ }
25
+ /** 한 seq(= 한 op 배치 = 한 사용자 액션) 그룹. */
26
+ export interface ActionGroup {
27
+ seq: number;
28
+ ts?: string | number | Date;
29
+ origin?: string;
30
+ author?: string;
31
+ kind?: 'edit' | 'restore';
32
+ restoredFromSeq?: number;
33
+ actions: SubjectAction[];
34
+ }
35
+ /**
36
+ * op 이력 스트림을 seq(액션) 단위 그룹으로 투영한다(diagram-wide).
37
+ *
38
+ * @param records seq 정렬 이력 레코드.
39
+ * @returns seq **내림차순**(최신 먼저) 액션 그룹. 각 그룹은 subject별 액션(verb+필드)으로 구성.
40
+ */
41
+ export declare function projectActionLog(records: readonly FieldAuditRecord[]): ActionGroup[];
@@ -0,0 +1,22 @@
1
+ import { ModelId } from './types';
2
+ import { FieldAuditRecord, FieldAuditEntry } from './projectFieldAudit';
3
+ /** 감사 대상 종류 — 엔티티 자신 또는 자식 컬렉션. */
4
+ export type EntityAuditSubjectKind = 'entity' | 'attribute' | 'operation' | 'index';
5
+ /** 무엇이 바뀐 대상인가 — 피드에서 필드 값과 함께 표시. */
6
+ export interface EntityAuditSubject {
7
+ kind: EntityAuditSubjectKind;
8
+ /** 대상 ref. kind='entity'면 엔티티 자신(entityRef와 동일). */
9
+ ref: ModelId;
10
+ }
11
+ /** 엔티티 감사 피드 한 엔트리 — projectFieldAudit 엔트리 + 어느 대상인지(subject). */
12
+ export interface EntityAuditEntry extends FieldAuditEntry {
13
+ subject: EntityAuditSubject;
14
+ }
15
+ /**
16
+ * 한 엔티티의 감사 피드(엔티티 자신 + 모든 자식 속성/연산/인덱스의 필드 변경)를 seq 순으로 반환한다.
17
+ *
18
+ * @param records seq 정렬 이력 레코드(projectFieldAudit와 동일 입력). 필요 시 방어적 재정렬은 하위가 수행.
19
+ * @param entityRef 대상 엔티티.
20
+ * @returns seq 오름차순 병합 피드(각 엔트리에 subject 태그). 동일 seq는 원 삽입 순서 유지(안정 정렬).
21
+ */
22
+ export declare function projectEntityAudit(records: readonly FieldAuditRecord[], entityRef: ModelId): EntityAuditEntry[];