@simplysm/angular 14.0.48 → 14.0.51

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.
Files changed (144) hide show
  1. package/README.md +234 -226
  2. package/dist/controls/select/sd-select.js +3 -3
  3. package/dist/index.d.ts +0 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +0 -2
  6. package/dist/layout/dock/sd-dock-container.js +1 -1
  7. package/dist/styles.css +9 -0
  8. package/docs/bootstrap/provide-sd-angular.md +37 -0
  9. package/docs/bootstrap/sd-angular-config-provider.md +16 -0
  10. package/docs/directives/sd-command-directive.md +30 -0
  11. package/docs/directives/sd-events.md +25 -0
  12. package/docs/directives/sd-intersection-directive.md +36 -0
  13. package/docs/directives/sd-invalid.md +24 -0
  14. package/docs/directives/sd-resize-directive.md +42 -0
  15. package/docs/directives/sd-ripple.md +23 -0
  16. package/docs/directives/sd-router-link.md +38 -0
  17. package/docs/directives/sd-show-effect.md +18 -0
  18. package/docs/directives/sd-typed-template.md +69 -0
  19. package/docs/features/sd-address-search-modal.md +50 -0
  20. package/docs/features/sd-permission-table.md +20 -0
  21. package/docs/features/sd-shared-data-components.md +158 -0
  22. package/docs/features/sd-tiptap-editor.md +26 -0
  23. package/docs/{pipes.md → pipes/format-pipe.md} +14 -5
  24. package/docs/plugins/sd-global-error-handler.md +23 -0
  25. package/docs/{plugins.md → plugins/sd-option-event-plugin.md} +9 -12
  26. package/docs/provider-types/sd-menu.md +65 -0
  27. package/docs/provider-types/sd-modal-content-def.md +148 -0
  28. package/docs/provider-types/sd-toast-content-def.md +73 -0
  29. package/docs/provider-types/shared-data-base.md +59 -0
  30. package/docs/providers/sd-activated-modal-provider.md +34 -0
  31. package/docs/providers/sd-app-structure-provider.md +81 -0
  32. package/docs/providers/sd-busy-provider.md +18 -0
  33. package/docs/providers/sd-file-dialog-provider.md +40 -0
  34. package/docs/providers/sd-local-storage-provider.md +20 -0
  35. package/docs/providers/sd-modal-provider.md +67 -0
  36. package/docs/providers/sd-navigate-window-provider.md +18 -0
  37. package/docs/providers/sd-print-provider.md +25 -0
  38. package/docs/providers/sd-service-client-factory-provider.md +43 -0
  39. package/docs/providers/sd-shared-data-provider.md +64 -0
  40. package/docs/providers/sd-system-config-provider.md +46 -0
  41. package/docs/providers/sd-system-log-provider.md +18 -0
  42. package/docs/providers/sd-theme-provider.md +38 -0
  43. package/docs/providers/sd-toast-provider.md +65 -0
  44. package/docs/recipes/_common-rules.md +244 -0
  45. package/docs/recipes/crud-detail/extension-a-edit-save.md +230 -0
  46. package/docs/recipes/crud-detail/extension-b-delete-restore.md +142 -0
  47. package/docs/recipes/crud-detail/extension-c-modal-view.md +214 -0
  48. package/docs/recipes/crud-detail/extension-d-control-view.md +103 -0
  49. package/docs/recipes/crud-detail/extension-e-auxiliary.md +87 -0
  50. package/docs/recipes/crud-detail/extension-f-complex-detail.md +234 -0
  51. package/docs/recipes/crud-detail.md +215 -722
  52. package/docs/recipes/crud-list/extension-a-inline-edit.md +410 -0
  53. package/docs/recipes/crud-list/extension-b-selection.md +226 -0
  54. package/docs/recipes/crud-list/extension-c-inline-delete.md +87 -0
  55. package/docs/recipes/crud-list/extension-d-select-modal.md +207 -0
  56. package/docs/recipes/crud-list/extension-e-readonly-modal.md +165 -0
  57. package/docs/recipes/crud-list/extension-f-modal-edit.md +177 -0
  58. package/docs/recipes/crud-list/extension-g-excel.md +157 -0
  59. package/docs/recipes/crud-list.md +293 -626
  60. package/docs/recipes/data-select-button.md +194 -101
  61. package/docs/recipes/page-modal-container.md +168 -86
  62. package/docs/styling/classes.md +149 -0
  63. package/docs/styling/mixins.md +100 -0
  64. package/docs/styling/themes.md +35 -0
  65. package/docs/styling/variables.md +147 -0
  66. package/docs/{type-utilities.md → type-utilities/directive-input-signals.md} +17 -35
  67. package/docs/ui-data/sd-list.md +37 -0
  68. package/docs/ui-data/sd-sheet.md +227 -0
  69. package/docs/ui-form/sd-additional-button.md +26 -0
  70. package/docs/ui-form/sd-anchor.md +31 -0
  71. package/docs/ui-form/sd-button.md +105 -0
  72. package/docs/ui-form/sd-checkbox-group.md +39 -0
  73. package/docs/ui-form/sd-checkbox.md +81 -0
  74. package/docs/ui-form/sd-date-range-picker.md +27 -0
  75. package/docs/ui-form/sd-form.md +89 -0
  76. package/docs/ui-form/sd-modal-select-button.md +54 -0
  77. package/docs/ui-form/sd-numpad.md +26 -0
  78. package/docs/ui-form/sd-range.md +26 -0
  79. package/docs/ui-form/sd-select.md +68 -0
  80. package/docs/ui-form/sd-shared-data-select.md +52 -0
  81. package/docs/ui-form/sd-state-preset.md +37 -0
  82. package/docs/ui-form/sd-switch.md +27 -0
  83. package/docs/ui-form/sd-textarea.md +33 -0
  84. package/docs/ui-form/sd-textfield.md +145 -0
  85. package/docs/ui-layout/sd-dock-container.md +64 -0
  86. package/docs/ui-layout/sd-dock.md +37 -0
  87. package/docs/ui-layout/sd-gap.md +26 -0
  88. package/docs/{ui-layout.md → ui-layout/sd-kanban-board.md} +41 -85
  89. package/docs/ui-layout/sd-kanban-lane.md +34 -0
  90. package/docs/ui-layout/sd-kanban.md +29 -0
  91. package/docs/ui-navigation/sd-collapse.md +35 -0
  92. package/docs/ui-navigation/sd-pagination.md +26 -0
  93. package/docs/ui-navigation/sd-sidebar-container.md +49 -0
  94. package/docs/ui-navigation/sd-sidebar-menu.md +22 -0
  95. package/docs/ui-navigation/sd-sidebar-user.md +43 -0
  96. package/docs/ui-navigation/sd-tab.md +51 -0
  97. package/docs/ui-navigation/sd-topbar-container.md +97 -0
  98. package/docs/ui-navigation/sd-topbar-menu.md +23 -0
  99. package/docs/ui-navigation/sd-topbar-user.md +38 -0
  100. package/docs/ui-navigation/sd-topbar.md +30 -0
  101. package/docs/ui-overlay/sd-busy-container.md +69 -0
  102. package/docs/ui-overlay/sd-confirm-modal.md +30 -0
  103. package/docs/ui-overlay/sd-dropdown.md +40 -0
  104. package/docs/ui-overlay/sd-modal.md +34 -0
  105. package/docs/ui-overlay/sd-prompt-modal.md +30 -0
  106. package/docs/ui-overlay/sd-toast.md +35 -0
  107. package/docs/ui-visual/sd-barcode.md +36 -0
  108. package/docs/ui-visual/sd-calendar.md +34 -0
  109. package/docs/ui-visual/sd-echarts.md +32 -0
  110. package/docs/ui-visual/sd-label.md +24 -0
  111. package/docs/ui-visual/sd-note.md +23 -0
  112. package/docs/ui-visual/sd-progress.md +23 -0
  113. package/docs/utils/inject-routing-signals.md +161 -0
  114. package/docs/utils/inject-sd-system-config-resource.md +35 -0
  115. package/docs/utils/mark.md +43 -0
  116. package/docs/utils/selection-managers.md +96 -0
  117. package/docs/utils/set-safe-style.md +19 -0
  118. package/docs/utils/setup-functions.md +93 -0
  119. package/package.json +7 -7
  120. package/scss/commons/_styles.scss +12 -0
  121. package/src/controls/select/sd-select.ts +3 -3
  122. package/src/core/modal/sd-modal.provider.ts +1 -1
  123. package/src/core/modal/sd-modal.ts +1 -1
  124. package/src/core/routing/menu-utils.ts +1 -1
  125. package/src/core/shared-data/sd-shared-data.provider.ts +7 -7
  126. package/src/data/shared-data/sd-shared-data-select.ts +2 -2
  127. package/src/index.ts +0 -3
  128. package/src/layout/dock/sd-dock-container.ts +1 -1
  129. package/dist/data/getOrmDataEditToastErrorMessage.d.ts +0 -2
  130. package/dist/data/getOrmDataEditToastErrorMessage.d.ts.map +0 -1
  131. package/dist/data/getOrmDataEditToastErrorMessage.js +0 -8
  132. package/docs/bootstrap.md +0 -38
  133. package/docs/directives.md +0 -236
  134. package/docs/features.md +0 -169
  135. package/docs/provider-types.md +0 -283
  136. package/docs/providers.md +0 -379
  137. package/docs/styling.md +0 -222
  138. package/docs/ui-data.md +0 -333
  139. package/docs/ui-form.md +0 -502
  140. package/docs/ui-navigation.md +0 -273
  141. package/docs/ui-overlay.md +0 -157
  142. package/docs/ui-visual.md +0 -127
  143. package/docs/utils.md +0 -244
  144. package/src/data/getOrmDataEditToastErrorMessage.ts +0 -10
@@ -1,43 +1,44 @@
1
- # Recipe: 모달 기반 선택 버튼 직접 조립
1
+ # Recipe: 모달 기반 선택 버튼
2
2
 
3
- 소비 화면이 `<sd-modal-select-button>` 표준 컴포넌트를 **직접** 사용하거나, **컴포지션**(wrap)으로 도메인별 선택 버튼을 만든다. 과거 `SdDataSelectButton` / `SdDataSelectButtonBase` 추상화는 제거되었다.
3
+ 모달을 띄워 항목을 선택하고, 결과를 `value`(key) ↔ `selectedItems`(표시용 객체) 양방향 바인딩으로 수신하는 버튼 컴포넌트를 조립한다. 표준 `<sd-modal-select-button>`을 직접 사용하거나, 도메인별 데이터 로딩을 감싼 wrapper 컴포넌트로 컴포지션한다.
4
4
 
5
- ## 1. Overview
6
-
7
- - 제거된 추상화: `SdDataSelectButton`(컴포넌트) / `SdDataSelectButtonBase`(추상 클래스)
8
- - 대체 컴포넌트:
9
- - `<sd-modal-select-button>` — 표준 모달 선택 버튼. 모달을 띄워 선택 결과를 `value` model로 받는다
10
- - `<sd-shared-data-select-button>` — 메모리 공유 데이터(`SharedDataBase`) 기반 선택 버튼. 내부에서 `<sd-modal-select-button>` 컴포지션
11
- - 사용자 정의 select-button — `<sd-modal-select-button>` 컴포지션 + 비동기 `load(keys)` effect로 도메인별 표시 데이터 채우기
12
- - 유지되는 조력자:
13
- - `SdSelectModal<T>` 인터페이스 (`packages/angular/src/controls/button/sd-modal-select-button.ts:30`) — 선택 모달 컴포넌트가 구현
14
- - `SdSelectModalInfo<T>` 타입 — 모달 정보 객체
15
- - `SelectModalOutputResult<T>` (`packages/angular/src/core/select-modal-output-result.ts`) — `{ selectedItemKeys, selectedItems }` 모달 반환 형식
16
- - `SdModalProvider` — 프로그래밍 방식 모달 호출
17
- - `SdItemOfTemplate` — 항목 템플릿 컨텍스트 디렉티브
18
-
19
- ## 2. 언제 사용하는가
5
+ ## When to use / When NOT to use
20
6
 
21
7
  | 상황 | 적용 패턴 |
22
- |---|---|
23
- | 외부 대형 테이블에서 모달로 선택, 결과를 key로 저장 | 패턴 3: 사용자 정의 select-button (load 비동기) |
24
- | 메모리에 로드된 공유 데이터(`SharedDataBase`)에서 선택 | 패턴 2: `<sd-shared-data-select-button>` |
8
+ |------|-----------|
25
9
  | 1회성 모달을 직접 띄워 선택 (도메인별 wrapper 불필요) | 패턴 1: `<sd-modal-select-button>` 직접 |
26
- | 단순 enum 정적 옵션 선택 | `<sd-select>` + `<sd-select-item>` (본 레시피 범위 외) |
27
- | 공유 데이터 드롭다운(검색 포함) | `<sd-shared-data-select>` (`features.md` 참조) |
28
- | 공유 데이터 목록형(페이지네이션) | `<sd-shared-data-select-list>` (`features.md` 참조) |
10
+ | 메모리 상주 공유 데이터(`SharedDataBase`)에서 선택 | 패턴 2: `<sd-shared-data-select-button>` |
11
+ | key만 저장하고 표시용 데이터는 ORM 등에서 비동기 조회 | 패턴 3: 사용자 정의 wrapper |
12
+
13
+ - ❌ 단순 enum 정적 옵션 — 대신 `<sd-select>` + `<sd-select-item>` 사용
14
+ - ❌ 공유 데이터 드롭다운(검색 포함) — 대신 `<sd-shared-data-select>` 사용
15
+ - ❌ 공유 데이터 목록형(페이지네이션) — 대신 `<sd-shared-data-select-list>` 사용
16
+
17
+ ## 전제조건
18
+
19
+ - `provideSdAngular({ clientName })`이 앱 bootstrap에 등록되어 있다
20
+ - 모달 컴포넌트가 `SdSelectModal<T>` 인터페이스를 구현한다 (`packages/angular/src/controls/button/sd-modal-select-button.ts:30`)
21
+ - 표시용 객체(`selectedItems: T[]`)와 선택 key(`value`)는 분리하여 관리한다
22
+ - 선택 결과는 `SelectModalOutputResult<T>` 형식으로 반환된다 (`packages/angular/src/core/select-modal-output-result.ts:4`)
29
23
 
30
- ## 3. 패턴 1: `<sd-modal-select-button>` 직접 사용
24
+ ## 기본 레시피 (패턴 1: `<sd-modal-select-button>` 직접 사용)
31
25
 
32
- ### 3.1 모달 컴포넌트 구현
26
+ ### 1. 모달 컴포넌트
33
27
 
34
- 선택 모달은 `SdSelectModal<T>` 인터페이스를 구현한다.
28
+ 모달은 `SdSelectModal<T>`를 `implements`한다. `selectMode` / `selectedItemKeys` input과 `close` output을 모두 구현한다.
35
29
 
36
30
  ```typescript
37
- import { Component, input, output, signal, ViewEncapsulation } from "@angular/core";
31
+ import {
32
+ Component,
33
+ input,
34
+ output,
35
+ signal,
36
+ ViewEncapsulation,
37
+ type InputSignal,
38
+ } from "@angular/core";
38
39
  import {
39
40
  SdSelectModal,
40
- SelectModalOutputResult,
41
+ type SelectModalOutputResult,
41
42
  } from "@simplysm/angular";
42
43
 
43
44
  interface IItem {
@@ -51,7 +52,9 @@ interface IItem {
51
52
  encapsulation: ViewEncapsulation.None,
52
53
  template: `
53
54
  <div class="p-default">
54
- <!-- 항목 리스트, 검색, 페이지네이션 -->
55
+ @for (item of _items(); track item.id) {
56
+ <div (click)="onItemClick(item)">{{ item.name }}</div>
57
+ }
55
58
  <button (click)="onConfirm()">확인</button>
56
59
  <button (click)="close.emit(undefined)">취소</button>
57
60
  </div>
@@ -60,14 +63,22 @@ interface IItem {
60
63
  export class ItemSelectModal implements SdSelectModal<IItem> {
61
64
  initialized = signal(true);
62
65
  close = output<SelectModalOutputResult<IItem> | undefined>();
63
- selectMode = input<"single" | "multi" | undefined>("single");
64
- selectedItemKeys = input<any[]>([]);
66
+ selectMode: InputSignal<"single" | "multi" | undefined> = input<
67
+ "single" | "multi" | undefined
68
+ >("single");
69
+ selectedItemKeys: InputSignal<any[]> = input<any[]>([]);
70
+
71
+ protected readonly _items = signal<IItem[]>([]);
72
+ private readonly _picked = signal<IItem[]>([]);
65
73
 
66
- // 내부에서 선택된 항목 관리 (예시)
67
- private readonly _selectedItems = signal<IItem[]>([]);
74
+ onItemClick(item: IItem): void {
75
+ this._picked.update((prev) =>
76
+ this.selectMode() === "multi" ? [...prev, item] : [item],
77
+ );
78
+ }
68
79
 
69
80
  onConfirm(): void {
70
- const items = this._selectedItems();
81
+ const items = this._picked();
71
82
  this.close.emit({
72
83
  selectedItemKeys: items.map((it) => it.id),
73
84
  selectedItems: items,
@@ -76,14 +87,27 @@ export class ItemSelectModal implements SdSelectModal<IItem> {
76
87
  }
77
88
  ```
78
89
 
79
- ### 3.2 호출 측
90
+ **핵심**:
91
+ - `close.emit(undefined)`는 취소(value 변경 없음), `close.emit({...})`는 확정 반환
92
+ - `initialized = signal(true)`는 `SdModalContentDef` 계약 필드로, 모달 초기화 완료 신호
93
+
94
+ ### 2. 호출 측 컴포넌트
80
95
 
81
96
  ```typescript
82
97
  import { Component, signal } from "@angular/core";
83
- import { SdModalSelectButton, SdSelectModalInfo } from "@simplysm/angular";
98
+ import {
99
+ SdModalSelectButton,
100
+ type SdSelectModalInfo,
101
+ } from "@simplysm/angular";
102
+ import { ItemSelectModal } from "./item-select.modal";
103
+
104
+ interface IItem {
105
+ id: number;
106
+ name: string;
107
+ }
84
108
 
85
109
  @Component({
86
- selector: "app-foo",
110
+ selector: "app-foo-view",
87
111
  standalone: true,
88
112
  imports: [SdModalSelectButton],
89
113
  template: `
@@ -92,6 +116,7 @@ import { SdModalSelectButton, SdSelectModalInfo } from "@simplysm/angular";
92
116
  [(selectedItems)]="selectedItems"
93
117
  [modal]="modalInfo"
94
118
  [selectMode]="'single'"
119
+ [required]="true"
95
120
  >
96
121
  @if (selectedItems().length > 0) {
97
122
  {{ selectedItems()[0].name }}
@@ -101,7 +126,7 @@ import { SdModalSelectButton, SdSelectModalInfo } from "@simplysm/angular";
101
126
  </sd-modal-select-button>
102
127
  `,
103
128
  })
104
- export class FooPage {
129
+ export class FooView {
105
130
  value = signal<number | undefined>(undefined);
106
131
  selectedItems = signal<IItem[]>([]);
107
132
 
@@ -113,35 +138,38 @@ export class FooPage {
113
138
  }
114
139
  ```
115
140
 
116
- **핵심:**
117
- - `value`는 key (number/string), `selectedItems`는 표시용 객체. 둘 다 `model<>` 양방향
118
- - 모달이 닫힐 반환한 `selectedItemKeys`가 `value`로, `selectedItems`가 `selectedItems`로 자동 반영
119
- - 사용자가 모달을 띄우려면 검색 버튼을 누른다 (`<sd-modal-select-button>` 내장)
120
- - erase 버튼은 `disabled=false && required=false && value 존재` 자동 표시
141
+ **핵심 동작**:
142
+ - `value`는 key(number/string), `selectedItems`는 표시용 객체. 둘 다 `model<>`로 양방향 바인딩
143
+ - 검색 버튼 클릭 시 내부적으로 `SdModalProvider.showAsync`를 호출해 모달을 연다. 모달이 반환한 `selectedItemKeys`가 `value`에, `selectedItems`가 `selectedItems`에 자동 반영된다 (`packages/angular/src/controls/button/sd-modal-select-button.ts:193`)
144
+ - erase 버튼(초기화)은 `!disabled() && !required() && value 존재` 시 자동 표시된다 (`packages/angular/src/controls/button/sd-modal-select-button.ts:54`). `required=true`면 erase가 노출되지 않아 사용자가 값을 비울 수 없다
145
+ - `required=true`인 상태에서 `value`가 비어 있으면 "선택된 항목이 없습니다." invalid 메시지가 붙는다 (`packages/angular/src/controls/button/sd-modal-select-button.ts:179`)
121
146
 
122
- ## 4. 패턴 2: `<sd-shared-data-select-button>` (공유 데이터)
147
+ ## 변형 (Variation)
123
148
 
124
- 메모리에 이미 로드된 `SharedDataBase` 기반 데이터에서 선택할 때 사용한다. `value` 변경 시 `items.filter(by __valueKey)`로 표시 데이터가 자동 채워진다 — 별도 `load()` 호출 불필요.
149
+ ### 패턴 2: `<sd-shared-data-select-button>` (메모리 상주 공유 데이터)
150
+
151
+ 메모리에 이미 로드된 `SharedDataBase` 기반 데이터에서 선택한다. `value`(key) 또는 `items` 변경 시 표시용 `_selectedItems`가 내부 effect에서 자동 재계산되므로 외부 로딩이 불필요하다 (`packages/angular/src/data/shared-data/sd-shared-data-select-button.ts:79`).
125
152
 
126
153
  ```typescript
127
154
  import { Component, signal } from "@angular/core";
128
155
  import {
129
156
  SdSharedDataSelectButton,
130
157
  SdItemOfTemplate,
131
- SdSelectModalInfo,
132
- SharedDataBase,
158
+ type SdSelectModalInfo,
159
+ type SharedDataBase,
133
160
  } from "@simplysm/angular";
161
+ import { ShopSelectModal } from "./shop-select.modal";
134
162
 
135
163
  interface IShop extends SharedDataBase<number> {
136
- __valueKey: number;
137
- __searchText: string;
138
- __isHidden: boolean;
164
+ __valueKey: number; // SharedDataBase 필수 식별자
165
+ __searchText: string; // 검색 매칭 대상
166
+ __isHidden: boolean; // 숨김 여부
139
167
  name: string;
140
168
  code: string;
141
169
  }
142
170
 
143
171
  @Component({
144
- selector: "app-bar",
172
+ selector: "app-bar-view",
145
173
  standalone: true,
146
174
  imports: [SdSharedDataSelectButton, SdItemOfTemplate],
147
175
  template: `
@@ -157,7 +185,7 @@ interface IShop extends SharedDataBase<number> {
157
185
  </sd-shared-data-select-button>
158
186
  `,
159
187
  })
160
- export class BarPage {
188
+ export class BarView {
161
189
  shopId = signal<number | undefined>(undefined);
162
190
  shops = signal<IShop[]>([]); // SdSharedDataProvider에서 로드
163
191
 
@@ -169,17 +197,18 @@ export class BarPage {
169
197
  }
170
198
  ```
171
199
 
172
- **핵심:**
173
- - `items`가 source of truth. `value`(key) 또는 `items` 변경 표시되는 항목이 자동 재계산됨
174
- - `<ng-template [itemOf]="items()" let-item>`은 항목 템플릿. multi 모드에서는 ", " 구분자로 자동 나열
200
+ **핵심**:
201
+ - `items`가 source of truth. `selectedItems`는 외부로 노출되지 않고 내부 signal로 관리된다
202
+ - `<ng-template [itemOf]="items()" let-item>` 컨텍스트 디렉티브로 항목 템플릿을 정의한다. multi 모드에서는 선택된 항목들이 `, ` 구분자로 자동 나열된다
175
203
  - `selectMode="multi"`이면 `value`는 `number[]`
176
204
 
177
- ## 5. 패턴 3: 사용자 정의 select-button (LotSelectButton 패턴)
205
+ ### 패턴 3: 사용자 정의 wrapper (도메인별 ORM 조회)
178
206
 
179
- 도메인별로 자주 쓰는 모달 선택 버튼은 `<sd-modal-select-button>`을 컴포지션하여 wrapper 컴포넌트로 만든다. value(key)만 저장하고 표시용 데이터는 비동기 `load(keys)`로 ORM에서 조회한다.
207
+ 도메인별로 자주 쓰는 모달 선택 버튼은 `<sd-modal-select-button>`을 컴포지션한 wrapper 작성한다. `value`(key)만 외부에 노출하고, 표시용 데이터는 `effect`에서 비동기로 조회한다.
180
208
 
181
209
  ```typescript
182
210
  import {
211
+ booleanAttribute,
183
212
  ChangeDetectionStrategy,
184
213
  Component,
185
214
  computed,
@@ -189,14 +218,13 @@ import {
189
218
  model,
190
219
  signal,
191
220
  ViewEncapsulation,
192
- booleanAttribute,
193
221
  } from "@angular/core";
194
222
  import {
195
223
  SdModalSelectButton,
196
- SdSelectModalInfo,
224
+ type SdSelectModalInfo,
197
225
  } from "@simplysm/angular";
198
- import { AppOrmProvider } from "../app-orm.provider";
199
226
  import { expr } from "@simplysm/orm-common";
227
+ import { AppOrmProvider } from "../app-orm.provider";
200
228
  import { LotSelectModal } from "./lot-select.modal";
201
229
 
202
230
  interface ILot {
@@ -242,30 +270,29 @@ export class LotSelectButton {
242
270
 
243
271
  protected readonly modalInfo = computed<SdSelectModalInfo<LotSelectModal>>(() => ({
244
272
  type: LotSelectModal,
245
- title: "LOT조회",
273
+ title: "LOT 조회",
246
274
  inputs: this.modalInputs(),
247
275
  }));
248
276
 
249
277
  constructor() {
250
- // value 변경 시 비동기 load → _selectedItems 갱신
251
278
  effect(() => {
252
279
  const v = this.value();
253
280
  if (v == null) {
254
281
  this._selectedItems.set([]);
255
282
  return;
256
283
  }
257
- void this._loadAsync([v]);
258
- });
259
- }
260
284
 
261
- private async _loadAsync(keys: number[]): Promise<void> {
262
- const items = await this._appOrm.connectAsync(async (db) =>
263
- db.lot()
264
- .where((it) => [expr.in(it.id, keys)])
265
- .select((it) => ({ id: it.id, code: it.code }))
266
- .execute(),
267
- );
268
- this._selectedItems.set(items);
285
+ // effect 콜백은 동기여야 하므로 void IIFE로 비동기 격리
286
+ void (async () => {
287
+ const items = await this._appOrm.connectAsync(async (db) =>
288
+ db.lot()
289
+ .where((it) => [expr.in(it.id, [v])])
290
+ .select((it) => ({ id: it.id, code: it.code }))
291
+ .execute(),
292
+ );
293
+ this._selectedItems.set(items);
294
+ })();
295
+ });
269
296
  }
270
297
  }
271
298
  ```
@@ -280,40 +307,106 @@ export class LotSelectButton {
280
307
  />
281
308
  ```
282
309
 
283
- **핵심:**
284
- - `<sd-modal-select-button>`이 모달 호출/erase/invalid 로직 담당 wrapper는 비동기 load만 추가
285
- - `_selectedItems`는 내부 signal. 외부에서는 `value`만 set
286
- - `effect()` 내부에서 비동기 작업은 `void this._loadAsync(...)` 패턴 (effect 콜백은 동기여야 함)
287
- - multi 모드를 지원하려면 `value = model<number[] | undefined>()`로 변경 + `selectMode = input<"single"|"multi">("single")` 추가 + effect에서 배열 처리 분기
310
+ **확장 지점**:
311
+ - multi 모드 지원: `value = model<number[] | undefined>()`, `selectMode = input<"single" | "multi">("single")`, effect의 `expr.in(it.id, [v])`를 배열 전체(`expr.in(it.id, v)`)로 바꾸고 `v.length === 0` 분기를 추가한다
312
+ - 모달 inputs 주입: 외부에서 `[modalInputs]="{ filter: ... }"` 형태로 전달하면 `computed` 합성을 통해 `modalInfo`에 반영된다
288
313
 
289
- ### 5.1 시트 안에 삽입
314
+ **근거**: `effect` 콜백에 `async`를 선언하면 cleanup 시점이 반환 Promise 해소 시점과 어긋난다. 관련 규칙: [_common-rules.md input 의존 로딩 규칙](./_common-rules.md#input-의존-데이터-로딩에-void-this_initasync를-사용하지-않는다).
290
315
 
291
- `[inset]="true"` + `[size]="'sm'"`로 시트 셀에 자연스럽게 녹아든다 (관용 규칙):
316
+ ### 시트 안에 삽입
317
+
318
+ `[inset]="true" [size]="'sm'"` 규칙은 공통 규칙을 따른다 — [_common-rules.md — 시트 셀 내부 컨트롤 규칙](./_common-rules.md#시트-셀-내부-컨트롤에-insettrue-sizesm을-명시한다) 참조.
292
319
 
293
320
  ```html
294
- <sd-sheet-column [key]="'lotId'" [header]="'LOT'">
295
- <ng-template [cell]="items()" let-item="item">
296
- <app-lot-select-button
297
- [inset]="true"
298
- [size]="'sm'"
299
- [(value)]="item.lotId"
300
- (valueChange)="mark(items)"
301
- />
302
- </ng-template>
303
- </sd-sheet-column>
321
+ <app-lot-select-button [inset]="true" [size]="'sm'" [(value)]="item.lotId" (valueChange)="mark(items)" />
322
+ ```
323
+
324
+ ## 🚫 흔한 실수 (Anti-patterns)
325
+
326
+ ### 1. `SdDataSelectButton` / `SdDataSelectButtonBase` 재도입
327
+
328
+ ```typescript
329
+ // ❌ 공통 부모 클래스 상속으로 도메인별 select button 생성
330
+ export class MySelectButton extends SdDataSelectButtonBase<IItem> {
331
+ /* ... */
332
+ }
333
+
334
+ // ✅ <sd-modal-select-button> 컴포지션 wrapper (패턴 3)
335
+ @Component({
336
+ selector: "app-my-select-button",
337
+ imports: [SdModalSelectButton],
338
+ template: `<sd-modal-select-button [(value)]="value" [modal]="modalInfo()" ... />`,
339
+ })
340
+ export class MySelectButton { /* value = model<...>(), effect로 load */ }
341
+ ```
342
+
343
+ **근거**: 공통 부모를 재도입하면 도메인별 분기가 상속 트리에 묶여 변경 전파가 불투명해진다. 컴포지션은 각 wrapper가 독립적으로 소멸·교체 가능하다.
344
+
345
+ ### 2. 조회 전용 모달에 `SdSelectModal<T>` 반사적 구현
346
+
347
+ ```typescript
348
+ // ❌ 조회만 하는 모달인데 선택 계약까지 구현
349
+ export class OrderHistoryModal implements SdSelectModal<IOrder> {
350
+ initialized = signal(true);
351
+ close = output<SelectModalOutputResult<IOrder> | undefined>();
352
+ selectMode = input<"single" | "multi" | undefined>("single"); // 불필요
353
+ selectedItemKeys = input<any[]>([]); // 불필요
354
+ }
355
+
356
+ // ✅ 조회 전용은 SdModalContentDef만 구현, close는 undefined로 emit
357
+ export class OrderHistoryModal implements SdModalContentDef<undefined> {
358
+ initialized = signal(true);
359
+ close = output<undefined>();
360
+ }
304
361
  ```
305
362
 
306
- ## 6. 주의사항
363
+ **근거**: `SdSelectModal<T>`는 선택 결과 반환 계약(`SelectModalOutputResult<T>`)을 강제한다. 조회만 하는 모달에 붙이면 미사용 입력이 누적되어 의도가 흐려진다. 선택 모달 쪽 상세: [`./crud-list/extension-d-select-modal.md`](./crud-list/extension-d-select-modal.md).
364
+
365
+ ### 3. `effect` 콜백을 `async`로 선언
366
+
367
+ ```typescript
368
+ // ❌ cleanup 시점이 반환 Promise와 어긋남
369
+ effect(async () => {
370
+ const v = this.value();
371
+ const items = await this._load([v]);
372
+ this._selectedItems.set(items);
373
+ });
374
+
375
+ // ✅ void IIFE로 비동기 격리 (동기 콜백 유지)
376
+ effect(() => {
377
+ const v = this.value();
378
+ if (v == null) { this._selectedItems.set([]); return; }
379
+ void (async () => {
380
+ const items = await this._load([v]);
381
+ this._selectedItems.set(items);
382
+ })();
383
+ });
384
+ ```
385
+
386
+ **근거**: `effect`는 cleanup/재실행 시점을 동기 반환을 기준으로 계산한다. async 콜백은 반환 Promise 해소 전에 다음 tick이 돌면서 경합을 일으킨다.
387
+
388
+ ### 4. `<sd-shared-data-select-button>`에 `[(selectedItems)]` 외부 바인딩
389
+
390
+ ```html
391
+ <!-- ❌ selectedItems는 외부 바인딩 지점이 없음 -->
392
+ <sd-shared-data-select-button
393
+ [(value)]="shopId"
394
+ [(selectedItems)]="shops"
395
+ [items]="allShops()"
396
+ [modal]="shopModalInfo"
397
+ />
398
+
399
+ <!-- ✅ value + items만 바인딩. 외부 set이 필요하면 패턴 3 wrapper로 전환 -->
400
+ <sd-shared-data-select-button
401
+ [(value)]="shopId"
402
+ [items]="allShops()"
403
+ [modal]="shopModalInfo"
404
+ />
405
+ ```
307
406
 
308
- - **`SdDataSelectButton` / `SdDataSelectButtonBase`는 삭제됨.** 기존 `extends SdDataSelectButtonBase` 코드는 패턴 3(사용자 정의 select-button) 형태로 마이그레이션한다.
309
- - **신규 추상화 클래스를 만들지 말 것.** `SelectButtonBase` 같은 공통 부모 클래스를 다시 만들면 본 WBS가 제거한 패턴이 되살아난다. 도메인별 wrapper 컴포넌트를 각자 직접 작성한다.
310
- - **`SdSelectModal<T>` 인터페이스는 모달 컴포넌트가 직접 `implements`한다.** `selectMode`/`selectedItemKeys` `InputSignal`과 `close` `output<SelectModalOutputResult<T>>`를 모두 구현해야 한다.
311
- - **`<sd-shared-data-select-button>`의 `selectedItems`는 외부 노출되지 않는다.** 내부 signal로 자동 관리되므로 외부에서는 `value` + `items`만 set한다. 직접 set이 필요하면 패턴 3로 wrapper를 작성한다.
312
- - **`effect()` 내부의 비동기 호출은 `void` 키워드 또는 별도 메서드 호출**로 처리한다. effect 콜백을 `async`로 만들면 cleanup 시점이 어긋난다.
407
+ **근거**: `SdSharedDataSelectButton._selectedItems`는 `protected readonly signal`로, `items` + `value`로부터 내부 effect가 자동 파생한다 (`packages/angular/src/data/shared-data/sd-shared-data-select-button.ts:79`). 외부에서 set하면 자동 파생 값과 즉시 덮어쓰기 경합이 발생한다.
313
408
 
314
- ## 7. Cross-reference
409
+ ## 관련 Entry
315
410
 
316
- - 선택 모달이 CRUD 리스트와 동일한 컴포넌트일 때 — [recipes/crud-list.md](./crud-list.md) "변형 2: 선택 모달 뷰" 섹션 참조
317
- - 공유 데이터 드롭다운(`SdSharedDataSelect`) / 목록형 선택(`SdSharedDataSelectList`) — [features.md](../features.md) 참조
318
- - `SdModalSelectButton` 자체 API — `packages/angular/src/controls/button/sd-modal-select-button.ts:148`
319
- - `SharedDataBase` / `SdSharedDataProvider` — [features.md](../features.md), `packages/angular/src/core/shared-data/sd-shared-data.provider.ts`
411
+ - [`crud-list/extension-d-select-modal.md`](./crud-list/extension-d-select-modal.md) 차이: 본 레시피는 **호출 측** 조립을 다루고, 해당 확장은 **모달 쪽** `SdSelectModal<T>` 계약 구현(선택 누적, CRUD 리스트 공용 모달화)을 다룬다
412
+ - [`_common-rules.md`](./_common-rules.md) 시트 `[inset]`/`[size]`, effect 내 비동기 처리, signal 필드 초기값 제약 등 횡단 규칙