@g1cloud/entity-modeler-next 5.1.10 → 5.2.1

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.
@@ -96,6 +96,22 @@ export type ResolveError = {
96
96
  opIndex: number;
97
97
  handle: AssocHandle;
98
98
  }
99
+ /**
100
+ * `association.add` 의 end 중 하나가 `SERIALIZED_TYPE` — **거부 사유가 임베더블과 다르다**.
101
+ *
102
+ * 임베더블은 *「EMBED 여야 하는데 이 경로에 EMBED 어휘가 없다」* 는 **한시적** 미지원이라 어휘가 생기면
103
+ * (AA-5 S3) 방출로 바뀔 자리다. 직렬화 타입은 그렇지 않다 — 직렬화 컬렉션의 요소 타입은 **관계로 연결되지
104
+ * 않고** 소비 속성의 `type` 참조로 지목된다(2단계 결정 2-ⓐ·3-①). 즉 만들어야 할 관계가 *존재하지
105
+ * 않으므로* 어휘가 생겨도 통과시킬 것이 없다 ⇒ **항구적** 거부다.
106
+ *
107
+ * ⇒ 같은 코드로 묶지 않는다. 코드가 다르면 AI 가 **고칠 방향**을 구분할 수 있다:
108
+ * 임베더블 = 「EMBED 로 표현할 것(현재 GUI 소관)」 / 직렬화 타입 = 「관계 말고 소비 속성 `type` 을 쓸 것」.
109
+ */
110
+ | {
111
+ code: 'serialized-relation-invalid';
112
+ opIndex: number;
113
+ handle: AssocHandle;
114
+ }
99
115
  /**
100
116
  * `entity.update` patch 의 `stereotype` 이 **`JPA_EMBEDDABLE` 경계를 넘음** — 이 전환은 단일 필드 쓰기가
101
117
  * 아니라 **동반 명령을 가진 composite** 다(`editor/entity.ts` becomingEmbeddable/leavingEmbeddable):
@@ -113,6 +129,23 @@ export type ResolveError = {
113
129
  from: string;
114
130
  to: string;
115
131
  }
132
+ /**
133
+ * `entity.update` patch 의 `stereotype` 이 **`SERIALIZED_TYPE` 경계를 넘음** — 위 임베더블 전환과 **같은 사유**다.
134
+ * 직렬화 타입도 값 타입이라 진입 시 자기 PK 해제 + 보유 FK 제거·자식 cascade reconcile 이, 이탈 시 PK 자동
135
+ * 부여가 동반된다(`editor/entity.updateEntity` 의 `enteringValueType`/`leavingValueType`). 맨 patch 만
136
+ * 흘리면 PK·FK 잔재(`CHK-JPA-2` error)가 남는다.
137
+ *
138
+ * ★코드를 **분리**한 것은 사유가 달라서가 아니라 `stereotype-embeddable-transition-unsupported` 가
139
+ * 이미 게시된 공개 계약(`ResolveError`)이고 호스트가 그 문자열을 소비하기 때문이다 — 조건을 넓히면서
140
+ * 이름이 거짓말을 하게 두는 대신, **추가**로 정직한 이름을 낸다(개명은 breaking).
141
+ */
142
+ | {
143
+ code: 'stereotype-serialized-transition-unsupported';
144
+ opIndex: number;
145
+ entity: Handle;
146
+ from: string;
147
+ to: string;
148
+ }
116
149
  /** `type:'GroupCodeEnum'` 인데 `groupCode` 미동반 — groupCode 바인딩 없는 깨진 파생 타입 방지(B3). */
117
150
  | {
118
151
  code: 'groupcode-required';
@@ -46,7 +46,7 @@ export interface InheritedColumn {
46
46
  * 해석해야 상속 컬럼이 점등된다. default가 지정 안 된 카탈로그면 빈 값은 여전히 빈 배열(opt-in 보존).
47
47
  * - 명시적 `superClass='Object'`(자바 `extends Object` = default 상속 opt-out 트릭)는 non-empty라
48
48
  * default 폴백에 안 걸리고 카탈로그 미스 → 빈 배열. opt-out이 자연 보존된다.
49
- * - D7 스킵: `stereotype === 'JPA_EMBEDDABLE'`(임베더블은 엔티티 상속 ), 엔티티 자신이
49
+ * - D7 스킵: **값 타입**(`isValueTypeStereotype` 임베더블·직렬화 타입은 엔티티 상속에 참여하지 않는다), 엔티티 자신이
50
50
  * 카탈로그 base(= 상속의 원천)면 스킵(자기 자신에게 투영하는 순환 방지).
51
51
  * - 각 투영 컬럼에 합성 modelId(§4.3)를 부여한다.
52
52
  * - 초기엔 1단 투영만(대부분 BaseEntity 단일 단계). base가 또 다른 base를 상속하는 다단 체인은
@@ -1,4 +1,4 @@
1
- import { Association, Attribute, DisplayMode, PredefinedCustomTypeCatalog } from './types';
1
+ import { Entity, Association, Attribute, DisplayMode, PredefinedCustomTypeCatalog } from './types';
2
2
  export interface JavaTypeDef {
3
3
  /** 저장값 = 레거시 enum 상수명. Attribute.type에 그대로 들어간다. */
4
4
  value: string;
@@ -61,7 +61,16 @@ export declare const SELECTABLE_JAVA_TYPES: readonly JavaTypeDef[];
61
61
  * 레거시 값)이면 라벨과 함께 맨 앞에 합성해 현 상태를 보존 표시한다 — 자유 선택지가 아니라 "현재 보유 값"
62
62
  * 표시용(groupCode 비활성 셀렉트의 GroupCodeEnum, ③ 카탈로그 미주입 시 customtype-typed 속성 포함).
63
63
  */
64
- export declare function typeSelectItems(currentType: string, predefinedCustomTypes?: PredefinedCustomTypeCatalog): JavaTypeDef[];
64
+ /**
65
+ * @param serializedTypes 문서 안 **직렬화 타입 엔티티 이름**(`core/serializedTypeRef.serializedTypeCatalogOf`
66
+ * 의 키). 소비 속성은 `type = <타입명>` 으로 그 타입을 지목하므로(2단계 결정 2-ⓐ) **목록에 있어야 고를
67
+ * 수 있다**.
68
+ * ⚠️★없으면 현재값 폴백 합성(아래)으로 «지금 그 값일 때만» 보이고, **다른 타입으로 바꾸는 순간 목록에서
69
+ * 사라져 되돌아갈 수 없다**(2026-08-29 사용자 실사용 보고: *「타입을 변경하면 연결이 끊어지고 다시 그
70
+ * 타입으로 돌릴수 없는 상태」*). 폴백은 **밖에서 들어온 미지 타입**을 잃지 않기 위한 안전망이지
71
+ * «선택 가능한 타입»의 출처가 아니다 — 그 둘을 섞으면 편집이 한 방향으로만 흐른다.
72
+ */
73
+ export declare function typeSelectItems(currentType: string, predefinedCustomTypes?: PredefinedCustomTypeCatalog, serializedTypes?: readonly string[]): JavaTypeDef[];
65
74
  /**
66
75
  * EMBED 표시 속성의 컬렉션 모드(`@ElementCollection`)를 판정한다 — 컬렉션이면 `collectionType`(기본
67
76
  * LIST), 단일(`@Embedded`)이거나 EMBED가 아니면 undefined. 판정 권위는 임베더블을 가리키는 end의
@@ -79,4 +88,11 @@ export declare function embedCollectionType(attr: Attribute, associations: Assoc
79
88
  * CustomType 마커는 실제 타입명(customType)을, 임베드 참조 헤더는 임베더블 타입명을 보인다
80
89
  * (임베드의 물리 컬럼 타입은 펼침 서브행이 담당). Diagram 노드·Explorer 트리 공용.
81
90
  */
82
- export declare function attrTypeCell(mode: DisplayMode, attr: Attribute, collectionType?: 'LIST' | 'SET', relationTargetName?: string): string;
91
+ /**
92
+ * @param serializedTarget 이 속성이 지목하는 **직렬화 타입 엔티티**(호출부가 `serializedTypeRefOf` 로
93
+ * 해소해 넘긴다 — 뷰가 규칙을 재기술하지 않는 관용구는 `relationTargetName`·`collectionType` 과 같다).
94
+ * 주면 CLASS 칸을 `List<X>`/`Map<K, X>` 로 감싼다 — 컨테이너는 «타입» 이 갖기 때문에(결정 3)
95
+ * 속성의 `type` 만 보면 컨테이너를 바꿔도 화면이 그대로다(2026-08-29 실사용 보고).
96
+ * ⚠️PHYSICAL 계열은 감싸지 않는다 — 그 칸의 답은 **컬럼 타입**(CLOB)이고 실제로 한 컬럼이다.
97
+ */
98
+ export declare function attrTypeCell(mode: DisplayMode, attr: Attribute, collectionType?: 'LIST' | 'SET', relationTargetName?: string, serializedTarget?: Entity): string;
@@ -34,6 +34,34 @@ export type ConverterAxis = keyof typeof CONVERTER_FQN;
34
34
  export type ConverterCatalog = {
35
35
  readonly [K in ConverterAxis]?: string | null;
36
36
  };
37
+ /**
38
+ * ④ `CustomType` 해소 카탈로그 항목 — `Attribute.customType`(자유 입력 타입명)을 **필드 타입 표현 +
39
+ * `@Convert` 컨버터 + import** 로 푼다. ③ `PredefinedCustomTypeEntry`(호스트 주입, 단일 컬럼)와 같은
40
+ * 관용구지만 그쪽은 **구조 없는 프레임워크 공통 타입**(StoredFile·MultiLangString)이고 이쪽은
41
+ * **프로젝트 로컬 POJO**다 — 실물은 구조체 요소 컬렉션을 `@Convert(AttributeConverter<List<X>, String>)`
42
+ * 로 **한 컬럼에 JSON 직렬화**한다(실측 21자리 / POJO 9종).
43
+ *
44
+ * ★**컬렉션성을 왜 속성이 아니라 타입에 두는가**: 실측에서 **요소 타입당 컨테이너가 하나로 고정**됐다
45
+ * (`OrderBrand`→List 3자리 · `OrderSeries`→List 6자리 · `PromoTargets`→`Map<PromoType, …>` 1자리 —
46
+ * 같은 요소 타입이 자리마다 다른 컨테이너를 쓰는 사례 **0**). per-usage 축을 두면 그 자유도가 근거 없는
47
+ * 발명이 된다(Money currency 의 per-usage 판단과 반대 결론이고, 근거는 양쪽 다 실측이다).
48
+ *
49
+ * ★미주입·미등록이면 **종전 동작 그대로**(verbatim 타입명 + `CustomType 확인 필요` fillIn) — inert.
50
+ */
51
+ export interface CustomTypeEntry {
52
+ /** 필드 타입 표현. 컬렉션이면 컨테이너 포함(`List<CartGift>`). 생략 시 `customType` 값 그대로. */
53
+ fieldType?: string;
54
+ /**
55
+ * `AttributeConverter` FQN. `null`·공백은 *"이 프로젝트는 이 타입의 컨버터를 쓰지 않는다"* 를 침묵이 아니라
56
+ * 신호로 남기는 표기다(리포 규약: `null` ≡ 부재) ⇒ `@Convert` 를 생략하고 `fillIn` 으로 표면화한다.
57
+ * 미지정(undefined)도 방출할 FQN 이 없으므로 같은 처리이고 **문구로 둘을 구분**한다.
58
+ */
59
+ converterFqn?: string | null;
60
+ /** 방출할 import FQN — 요소 타입·`Map` 키 타입 등. 컨테이너(java.util 3종)는 `fieldType` 에서 자동 유도. */
61
+ imports?: readonly string[];
62
+ }
63
+ /** ④ CustomType 카탈로그 — 키는 `Attribute.customType` 값(단순명 또는 FQN). */
64
+ export type CustomTypeCatalog = Readonly<Record<string, CustomTypeEntry>>;
37
65
  export interface ScaffoldOptions {
38
66
  /**
39
67
  * **산출 불가 대상 자동 제외**(기본 `true`) — 엔티티명 미지정 / `@Id` 없는 JPA 엔티티는 파일을 만들지
@@ -55,6 +83,12 @@ export interface ScaffoldOptions {
55
83
  * `javaTypeCatalog` 와 같은 관용구다. 축별 3-상태·조건 비주입 근거는 {@link ConverterCatalog}.
56
84
  */
57
85
  converterCatalog?: ConverterCatalog;
86
+ /**
87
+ * ④ `CustomType`(자유 입력 `Attribute.customType`) 해소 카탈로그 — 직렬화 컬렉션/프로젝트 로컬 직렬화 타입 축.
88
+ * `javaTypeCatalog`·`converterCatalog` 와 같은 **호스트 주입** 관용구이고 미주입 시 종전 폴백(inert).
89
+ * 계약·근거는 {@link CustomTypeEntry}.
90
+ */
91
+ customTypeCatalog?: CustomTypeCatalog;
58
92
  /**
59
93
  * `extends <superClass>` + 상속 컬럼 인덱스 해소용. 핸드오프 O3 — `SuperClassDef.packageName`이
60
94
  * "코드젠 forward 슬롯"으로 예약돼 있고 이 emitter가 그 첫 소비처다. 미주입 시 상속 컬럼/extends 미방출.
@@ -0,0 +1,49 @@
1
+ import { Attribute, Entity, LogicalModel } from './types';
2
+ /**
3
+ * 문서 안 직렬화 타입 엔티티 색인(이름 → 엔티티).
4
+ *
5
+ * ⚠️★★**캐시하지 않는다.** 종전엔 `WeakMap<LogicalModel, …>` 으로 캐시하면서 *「모델이 바뀌면 새
6
+ * 객체라 자연히 캐시가 갈린다」* 고 적었는데 **그 전제가 틀렸다** — 코드젠(불변 스냅샷 1회 순회)에서는
7
+ * 참이지만 **리액티브 에디터에서는 `state.logical` 이 정체성을 유지**한 채 내용만 바뀌는 구간이 있어
8
+ * 최초에 만든 **빈 카탈로그가 영구히 고정**됐다. 결과: 캔버스에서 직렬화 타입을 만들고 연결해도
9
+ * `planSerializedTypeEdges` 가 **항상 `[]`** 를 돌려줘 점선이 나오지 않았다(2026-08-29 사용자 실사용
10
+ * 보고: *「serialized 상태에서 연결하면 선은 안 나오고 필드만 추가된다」*).
11
+ *
12
+ * ★비용은 **한 pass 당 O(n)** 이고 소비처는 둘 다 pass 진입에서 한 번만 부른다
13
+ * (`planSerializedTypeEdges` · 코드젠 `resolveSerializedTypeUsage`). 골든 791파일 실측에서 유의미한
14
+ * 차이가 없었다 ⇒ **정확성을 캐시보다 앞에 둔다**. 캐시가 필요해지면 키는 *논리 모델* 이 아니라
15
+ * **불변이 보장되는 스냅샷**이어야 한다.
16
+ */
17
+ /**
18
+ * 문서 안 직렬화 타입 색인 — **이름 → 후보 «목록»**.
19
+ *
20
+ * ⚠️★★목록인 것이 핵심이다. 종전엔 `Map<string, Entity>` 라 **동명이면 마지막 것이 이기고 나머지가
21
+ * 조용히 사라졌다** — 소비 속성이 그 이름을 지목하면 선이 **임의의 한 쪽**으로 그려지고(사용자 실사용
22
+ * 보고 ⑬) 코드젠도 패키지·컨테이너를 임의로 골랐다.
23
+ *
24
+ * ★**동명은 이 리포에서 정당한 관행**이다 — 사용자가 캔버스 선 길이 때문에 소비처 가까이 재선언한다
25
+ * (memory `project-equals-persistence-unit`: 실물에 `BuyerMember`×2·`OrderCompany`×3). 그러므로
26
+ * 「중복 = 오류」로 접으면 안 되고 **패키지로 갈라 해소**해야 한다.
27
+ *
28
+ * ⚠️캐시하지 않는다 — 리액티브 에디터에서 `state.logical` 이 정체성을 유지한 채 내용만 바뀌는 구간이
29
+ * 있어 캐시가 stale 로 고정됐던 선례가 있다(보고 ⑤).
30
+ */
31
+ export declare function serializedTypeCatalogOf(logical: LogicalModel): ReadonlyMap<string, Entity[]>;
32
+ /**
33
+ * 지목 해소 결과 — **「모른다」를 「이것이다」로 바꾸지 않는다.**
34
+ *
35
+ * `ambiguous` 를 별도 갈래로 둔 이유는 **무시할 수 없게** 하기 위해서다. `Entity | undefined` 로 돌리면
36
+ * 호출부가 「없음」과 「못 고름」을 구분하지 못해 조용히 틀린 쪽을 그린다 — 이 축이 반복해서 당한 형상이다.
37
+ */
38
+ export type SerializedTypeRef = {
39
+ kind: 'none';
40
+ } | {
41
+ kind: 'resolved';
42
+ entity: Entity;
43
+ } | {
44
+ kind: 'ambiguous';
45
+ name: string;
46
+ candidates: readonly Entity[];
47
+ };
48
+ export declare function serializedTypeLabel(typeName: string, entity: Entity | undefined): string;
49
+ export declare function serializedTypeRefOf(a: Attribute, catalog: ReadonlyMap<string, Entity[]>, consumer?: Entity, logical?: LogicalModel): SerializedTypeRef;
@@ -1,2 +1,42 @@
1
1
  import { ClassStereotype } from './types';
2
2
  export declare function stereotypeLabel(s: ClassStereotype): string;
3
+ /**
4
+ * **자체 테이블·PK·엔티티 상속을 갖지 않는 «값 타입» 스테레오타입** — 임베더블·직렬화 타입.
5
+ *
6
+ * 임베더블은 owner 테이블로 **평탄화**되고 직렬화 타입은 한 컬럼에 **직렬화**된다. 경로가 다르지만 «자기 테이블이
7
+ * 없다» 는 결론이 같아, 아래 넷이 오늘 같은 조건식을 쓴다:
8
+ *
9
+ * | 소비처 | 실제로 묻는 것 |
10
+ * |---|---|
11
+ * | `validation.registersEntityName` | 퍼시스턴스 유닛 이름 레지스트리 등록(패키지 달라도 충돌하는가) |
12
+ * | `validation.tableOwningEntities` · `ddl.tableEntities` | 자체 테이블 소유(CHK-NAME-9 · DDL 방출) |
13
+ * | `inheritedColumns.projectInheritedColumns` | 엔티티 상속 참여(superClass 컬럼 투영) |
14
+ * | `validation` CHK-JPA-2 | 식별자(PK) 보유 가능 |
15
+ *
16
+ * ★**집합만 단일 출처로 두고 각 자리의 지역 이름은 보존**한다 — 한 이름(`isTableless…`)으로 넷을 덮으면
17
+ * 이름-등록을 판정하는 자리에서 이름이 거짓말을 한다. 여기 모으는 목적은 **멤버십 재기술 누락 예방**이고
18
+ * (선례: `excludeDDLGeneration` 이 소비처 넷에서 각자 재기술돼 산출 경로에서 통째 누락됐다), 자리마다
19
+ * 무엇을 묻는지는 지역 이름이 계속 말해야 한다.
20
+ *
21
+ * ⚠️**이 집합을 임베드 소비 경로에 쓰지 말 것** — 임베더블은 컬럼을 평탄화하지만 직렬화 타입은 컬럼이 없다
22
+ * (`sharedColumn.ts` 의 「같은 컬럼을 가진 엔티티들」 · FK 전파 · EMBED 관계 전환 등). 그 자리는
23
+ * `=== 'JPA_EMBEDDABLE'` 이 정확하다.
24
+ */
25
+ export declare function isValueTypeStereotype(s: ClassStereotype): boolean;
26
+ /**
27
+ * **물리 컬럼을 아예 갖지 않는 스테레오타입** — `SERIALIZED_TYPE` 뿐이다.
28
+ *
29
+ * ⚠️위 `isValueTypeStereotype` 와 **다른 축**이다. 값 타입 둘은 「자체 테이블이 없다」에서 같지만
30
+ * **컬럼에서는 갈린다**: 임베더블은 owner 테이블에 컬럼을 **평탄화**해 실제로 컬럼을 갖고(그래서 물리명·
31
+ * dataType 편집이 정당하다), 직렬화 타입은 한 컬럼에 통째로 직렬화되므로 **자기 필드에 대응하는 컬럼이
32
+ * 없다**. 두 술어를 섞으면 임베더블의 정당한 컬럼 편집을 막거나, 직렬화 타입에 무의미한 컬럼을 만든다.
33
+ *
34
+ * ★소비처는 **편집 어포던스와 시드**다 — 속성 추가 시 `dbAttrs` 를 비우고(`editor/attribute`),
35
+ * 스테레오타입 전환 시 기존 컬럼·테이블명을 정리하며(`editor/entity`), 인스펙터·와이드 뷰가 컬럼 입력을
36
+ * 감춘다. 이 셋이 없으면 모델이 **도달 불가능한 상태를 요구**하게 된다(문서상 수용 기준은
37
+ * `dbAttrs.length === 0` 인데 편집 경로가 매번 컬럼을 만들던 결함 — 2026-08-29 사용자 실사용 보고).
38
+ *
39
+ * ★단일 스테레오타입이라 `=== 'SERIALIZED_TYPE'` 로 써도 되지만 **이름을 둔다** — 소비처 5곳이 각자
40
+ * 재기술하면 어긋나고, 그 실패는 이 축에서 이미 두 번 났다(`excludeDDLGeneration` · §6 오분류).
41
+ */
42
+ export declare function isColumnlessStereotype(s: ClassStereotype): boolean;
@@ -14,7 +14,21 @@ export type ModelId = string;
14
14
  export type MultiLangText = Partial<Record<'ko' | 'en' | 'ja' | 'zh', string>>;
15
15
  /** MultiLangText가 지원하는 언어 코드. 입력 가능 언어셋·유저 언어 주입의 단위. */
16
16
  export type LangCode = keyof MultiLangText;
17
- export type ClassStereotype = 'UNDEFINED' | 'JPA_ENTITY' | 'JPA_EMBEDDABLE' | 'JPA_MULTI_LANGUAGE_ENTITY';
17
+ /**
18
+ * 클래스 스테레오타입.
19
+ *
20
+ * ★`SERIALIZED_TYPE` = **직렬화 컬렉션 요소 타입**(2단계 신설). 실물은 구조체 요소 컬렉션을
21
+ * `@Convert(AttributeConverter<List<X>, String>)` 로 **한 컬럼에 JSON 직렬화**하는데(실측 21자리 / 9종),
22
+ * 그 요소 타입은 **구조를 갖지만 물리 컬럼을 갖지 않는다** — ② `JPA_EMBEDDABLE`(구조 있으나 컬럼 N개)와
23
+ * ③ predef customtype(1컬럼이나 구조 없음) 사이의 빈 칸이었다. 사용자 정의: **「임베더블 − 물리컬럼」**.
24
+ *
25
+ * ⚠️`SERIALIZED_TYPE` 와 `JPA_EMBEDDABLE` 은 **「자체 테이블·PK·엔티티 상속 없음」에서만 같다**
26
+ * (단일 출처 = `core/stereotype.ts` 의 `isValueTypeStereotype`). **다른 축에서는 갈린다** —
27
+ * 임베더블은 owner 테이블에 **컬럼을 평탄화**하고 EMBED 관계에 참여하지만 직렬화 타입은 **컬럼이 없고**
28
+ * 관계를 쓰지 않는다(`type` 참조로만 지목). 그래서 임베드 소비 경로(`sharedColumn`·`propagation`·
29
+ * `diagramAdapter`·`editor/association` 등)는 `=== 'JPA_EMBEDDABLE'` 를 **그대로 유지**해야 한다.
30
+ */
31
+ export type ClassStereotype = 'UNDEFINED' | 'JPA_ENTITY' | 'JPA_EMBEDDABLE' | 'JPA_MULTI_LANGUAGE_ENTITY' | 'SERIALIZED_TYPE';
18
32
  export type Multiplicity = 'NO_INSTANCE_OR_ONE_INSTANCE' | 'EXACTLY_ONE_INSTANCE' | 'ZERO_OR_MORE_INSTANCES' | 'ONE_OR_MORE_INSTANCES';
19
33
  export type JpaCascadeType = 'ALL' | 'PERSIST' | 'MERGE' | 'REMOVE' | 'REFRESH' | 'DETACH';
20
34
  export type JpaFetchType = 'LAZY' | 'EAGER';
@@ -337,6 +351,26 @@ export interface Entity {
337
351
  */
338
352
  referenceOnly?: boolean;
339
353
  declarationOfInterfaces?: string[];
354
+ /**
355
+ * 직렬화 축 — **이 타입이 소비될 때의 컨테이너**(`stereotype === 'SERIALIZED_TYPE'` 일 때만 의미).
356
+ *
357
+ * ★**왜 속성이 아니라 타입이 소유하는가**: 실측에서 **요소 타입당 컨테이너가 하나로 고정**됐다
358
+ * (`OrderBrand`→List 3자리 · `OrderSeries`→List 6자리 · `PromoTargets`→`Map<PromoType, …>` 1자리 —
359
+ * 같은 요소 타입이 자리마다 다른 컨테이너를 쓰는 사례 **0**). per-usage 축을 두면 그 자유도가 근거 없는
360
+ * 발명이 된다(Money currency 의 per-usage 판단과 반대 결론이고, 근거는 양쪽 다 실측이다).
361
+ *
362
+ * ★**컨버터 FQN 은 여기 없다**(결정 3) — `AttributeConverter` 구현체는 *구현 레벨*이라 호스트가
363
+ * `ScaffoldOptions.customTypeCatalog` 로 주입한다. 모델이 갖는 것은 **모델러가 편집해야 하는 것**
364
+ * (구조·컨테이너)뿐이고, 그 경계는 Lombok·`Serializable` 을 모델 밖에 둔 판정과 같은 축이다.
365
+ *
366
+ * 미지정 = 컬렉션이 아닌 단일 값(한 컬럼에 그 구조 하나를 직렬화).
367
+ */
368
+ serialization?: {
369
+ /** 컨테이너 종류. 코퍼스 실재는 java.util 3종뿐이라 그만 표현한다(미지 컨테이너는 발명하지 않는다). */
370
+ collectionType?: 'LIST' | 'SET' | 'MAP';
371
+ /** `MAP` 의 키 타입(단순명 또는 FQN). `collectionType === 'MAP'` 일 때만 의미. */
372
+ mapKeyType?: string;
373
+ };
340
374
  attributes: Attribute[];
341
375
  operations?: Operation[];
342
376
  indexes?: EntityIndex[];
@@ -22,4 +22,4 @@ export interface AttributeContext {
22
22
  /** 삭제된 행을 선택이 가리키지 않도록 해제하는 쓰기(`removeAttributes`). */
23
23
  selection: Ref<Selection>;
24
24
  }
25
- export declare function createAttributeApi(ctx: AttributeContext): Pick<EditorController, 'addPredefinedEmbed' | 'addAttribute' | 'addDivider' | 'updateAttribute' | 'setAttributeGroupCode' | 'setEmbedColumns' | 'removeAttribute' | 'removeAttributes' | 'setAttributeSwatch' | 'setAttributeSwatches' | 'reorderAttributes'>;
25
+ export declare function createAttributeApi(ctx: AttributeContext): Pick<EditorController, 'addPredefinedEmbed' | 'addSerializedTypeAttribute' | 'addAttribute' | 'addDivider' | 'updateAttribute' | 'setAttributeGroupCode' | 'setEmbedColumns' | 'removeAttribute' | 'removeAttributes' | 'setAttributeSwatch' | 'setAttributeSwatches' | 'reorderAttributes'>;
@@ -431,6 +431,13 @@ export interface EditorController {
431
431
  * 반환은 생성된 association id, 가드 차단 시 null.
432
432
  */
433
433
  addEmbedAssociation(ownerEntityId: ModelId, embeddableEntityId: ModelId): ModelId | null;
434
+ /**
435
+ * 직렬화 타입 «사용» — 소비 엔티티에 `type=<타입명>` 속성을 만든다(캔버스 연결 제스처의 착지점).
436
+ * ★**관계를 만들지 않는다** — 직렬화 타입은 `type` 참조로 지목되고(2단계 결정 2-ⓐ), 캔버스 점선은
437
+ * `planSerializedTypeEdges` 가 그 참조에서 파생하므로 속성만 만들면 선이 자동으로 따라온다.
438
+ * 반환은 생성된 속성 id, 가드(조회 모드·소유자 잠금·대상이 직렬화 타입 아님) 차단 시 null.
439
+ */
440
+ addSerializedTypeAttribute(ownerEntityId: ModelId, typeEntityId: ModelId): ModelId | null;
434
441
  /**
435
442
  * 임베드 카디널리티 설정 — 임베더블 end multiplicity를 지정 값으로 바꾼다. 컬렉션(`@ElementCollection`)
436
443
  * 여부는 이 multiplicity에서 파생되는 값(권위=multiplicity, 결정 3): `*_MORE`(0..N / 1..N)면 컬렉션,
@@ -1,5 +1,6 @@
1
1
  import { Ref } from 'vue';
2
2
  import { Command, EditorState } from '../command/types';
3
+ import { Translator } from '../i18n';
3
4
  import { EditorController } from './controller';
4
5
  import { ModelId } from '../core/types';
5
6
  /** 이 축이 팩토리 클로저에서 쓰는 것 전부. 늘어나면 여기 선언이 먼저 늘어난다(= 의존 인벤토리). */
@@ -15,5 +16,9 @@ export interface EntityContext {
15
16
  /** 순수 잠금 조회 — 공개 질의의 위임 대상. **안내를 내지 않는다**(위 ⚠️ 참고). */
16
17
  isEntityLockedFn: (id: ModelId) => boolean;
17
18
  isAssocLockedFn: (assocId: ModelId) => boolean;
19
+ /** 번역 — 아래 구조 차단의 이유 안내에 쓴다(속성 축의 FK 삭제 안내와 같은 짝). */
20
+ t: Translator;
21
+ /** 구조 차단 안내(dedup 경로) — 무음 차단은 "고장"으로 읽힌다(VE-2 규율). */
22
+ emitGateNotice: (text: string) => void;
18
23
  }
19
24
  export declare function createEntityApi(ctx: EntityContext): Pick<EditorController, 'addEntity' | 'renameEntity' | 'updateEntity' | 'moveEntity' | 'resizeEntity' | 'removeEntity' | 'setEntitySwatch' | 'setEntityCollapsed' | 'setEntityLocked' | 'isEntityLocked' | 'isAssociationLocked' | 'collapseAll' | 'expandAll'>;