@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.
- package/README.md +234 -226
- package/dist/controls/select/sd-select.js +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/layout/dock/sd-dock-container.js +1 -1
- package/dist/styles.css +9 -0
- package/docs/bootstrap/provide-sd-angular.md +37 -0
- package/docs/bootstrap/sd-angular-config-provider.md +16 -0
- package/docs/directives/sd-command-directive.md +30 -0
- package/docs/directives/sd-events.md +25 -0
- package/docs/directives/sd-intersection-directive.md +36 -0
- package/docs/directives/sd-invalid.md +24 -0
- package/docs/directives/sd-resize-directive.md +42 -0
- package/docs/directives/sd-ripple.md +23 -0
- package/docs/directives/sd-router-link.md +38 -0
- package/docs/directives/sd-show-effect.md +18 -0
- package/docs/directives/sd-typed-template.md +69 -0
- package/docs/features/sd-address-search-modal.md +50 -0
- package/docs/features/sd-permission-table.md +20 -0
- package/docs/features/sd-shared-data-components.md +158 -0
- package/docs/features/sd-tiptap-editor.md +26 -0
- package/docs/{pipes.md → pipes/format-pipe.md} +14 -5
- package/docs/plugins/sd-global-error-handler.md +23 -0
- package/docs/{plugins.md → plugins/sd-option-event-plugin.md} +9 -12
- package/docs/provider-types/sd-menu.md +65 -0
- package/docs/provider-types/sd-modal-content-def.md +148 -0
- package/docs/provider-types/sd-toast-content-def.md +73 -0
- package/docs/provider-types/shared-data-base.md +59 -0
- package/docs/providers/sd-activated-modal-provider.md +34 -0
- package/docs/providers/sd-app-structure-provider.md +81 -0
- package/docs/providers/sd-busy-provider.md +18 -0
- package/docs/providers/sd-file-dialog-provider.md +40 -0
- package/docs/providers/sd-local-storage-provider.md +20 -0
- package/docs/providers/sd-modal-provider.md +67 -0
- package/docs/providers/sd-navigate-window-provider.md +18 -0
- package/docs/providers/sd-print-provider.md +25 -0
- package/docs/providers/sd-service-client-factory-provider.md +43 -0
- package/docs/providers/sd-shared-data-provider.md +64 -0
- package/docs/providers/sd-system-config-provider.md +46 -0
- package/docs/providers/sd-system-log-provider.md +18 -0
- package/docs/providers/sd-theme-provider.md +38 -0
- package/docs/providers/sd-toast-provider.md +65 -0
- package/docs/recipes/_common-rules.md +244 -0
- package/docs/recipes/crud-detail/extension-a-edit-save.md +230 -0
- package/docs/recipes/crud-detail/extension-b-delete-restore.md +142 -0
- package/docs/recipes/crud-detail/extension-c-modal-view.md +214 -0
- package/docs/recipes/crud-detail/extension-d-control-view.md +103 -0
- package/docs/recipes/crud-detail/extension-e-auxiliary.md +87 -0
- package/docs/recipes/crud-detail/extension-f-complex-detail.md +234 -0
- package/docs/recipes/crud-detail.md +215 -722
- package/docs/recipes/crud-list/extension-a-inline-edit.md +410 -0
- package/docs/recipes/crud-list/extension-b-selection.md +226 -0
- package/docs/recipes/crud-list/extension-c-inline-delete.md +87 -0
- package/docs/recipes/crud-list/extension-d-select-modal.md +207 -0
- package/docs/recipes/crud-list/extension-e-readonly-modal.md +165 -0
- package/docs/recipes/crud-list/extension-f-modal-edit.md +177 -0
- package/docs/recipes/crud-list/extension-g-excel.md +157 -0
- package/docs/recipes/crud-list.md +293 -626
- package/docs/recipes/data-select-button.md +194 -101
- package/docs/recipes/page-modal-container.md +168 -86
- package/docs/styling/classes.md +149 -0
- package/docs/styling/mixins.md +100 -0
- package/docs/styling/themes.md +35 -0
- package/docs/styling/variables.md +147 -0
- package/docs/{type-utilities.md → type-utilities/directive-input-signals.md} +17 -35
- package/docs/ui-data/sd-list.md +37 -0
- package/docs/ui-data/sd-sheet.md +227 -0
- package/docs/ui-form/sd-additional-button.md +26 -0
- package/docs/ui-form/sd-anchor.md +31 -0
- package/docs/ui-form/sd-button.md +105 -0
- package/docs/ui-form/sd-checkbox-group.md +39 -0
- package/docs/ui-form/sd-checkbox.md +81 -0
- package/docs/ui-form/sd-date-range-picker.md +27 -0
- package/docs/ui-form/sd-form.md +89 -0
- package/docs/ui-form/sd-modal-select-button.md +54 -0
- package/docs/ui-form/sd-numpad.md +26 -0
- package/docs/ui-form/sd-range.md +26 -0
- package/docs/ui-form/sd-select.md +68 -0
- package/docs/ui-form/sd-shared-data-select.md +52 -0
- package/docs/ui-form/sd-state-preset.md +37 -0
- package/docs/ui-form/sd-switch.md +27 -0
- package/docs/ui-form/sd-textarea.md +33 -0
- package/docs/ui-form/sd-textfield.md +145 -0
- package/docs/ui-layout/sd-dock-container.md +64 -0
- package/docs/ui-layout/sd-dock.md +37 -0
- package/docs/ui-layout/sd-gap.md +26 -0
- package/docs/{ui-layout.md → ui-layout/sd-kanban-board.md} +41 -85
- package/docs/ui-layout/sd-kanban-lane.md +34 -0
- package/docs/ui-layout/sd-kanban.md +29 -0
- package/docs/ui-navigation/sd-collapse.md +35 -0
- package/docs/ui-navigation/sd-pagination.md +26 -0
- package/docs/ui-navigation/sd-sidebar-container.md +49 -0
- package/docs/ui-navigation/sd-sidebar-menu.md +22 -0
- package/docs/ui-navigation/sd-sidebar-user.md +43 -0
- package/docs/ui-navigation/sd-tab.md +51 -0
- package/docs/ui-navigation/sd-topbar-container.md +97 -0
- package/docs/ui-navigation/sd-topbar-menu.md +23 -0
- package/docs/ui-navigation/sd-topbar-user.md +38 -0
- package/docs/ui-navigation/sd-topbar.md +30 -0
- package/docs/ui-overlay/sd-busy-container.md +69 -0
- package/docs/ui-overlay/sd-confirm-modal.md +30 -0
- package/docs/ui-overlay/sd-dropdown.md +40 -0
- package/docs/ui-overlay/sd-modal.md +34 -0
- package/docs/ui-overlay/sd-prompt-modal.md +30 -0
- package/docs/ui-overlay/sd-toast.md +35 -0
- package/docs/ui-visual/sd-barcode.md +36 -0
- package/docs/ui-visual/sd-calendar.md +34 -0
- package/docs/ui-visual/sd-echarts.md +32 -0
- package/docs/ui-visual/sd-label.md +24 -0
- package/docs/ui-visual/sd-note.md +23 -0
- package/docs/ui-visual/sd-progress.md +23 -0
- package/docs/utils/inject-routing-signals.md +161 -0
- package/docs/utils/inject-sd-system-config-resource.md +35 -0
- package/docs/utils/mark.md +43 -0
- package/docs/utils/selection-managers.md +96 -0
- package/docs/utils/set-safe-style.md +19 -0
- package/docs/utils/setup-functions.md +93 -0
- package/package.json +7 -7
- package/scss/commons/_styles.scss +12 -0
- package/src/controls/select/sd-select.ts +3 -3
- package/src/core/modal/sd-modal.provider.ts +1 -1
- package/src/core/modal/sd-modal.ts +1 -1
- package/src/core/routing/menu-utils.ts +1 -1
- package/src/core/shared-data/sd-shared-data.provider.ts +7 -7
- package/src/data/shared-data/sd-shared-data-select.ts +2 -2
- package/src/index.ts +0 -3
- package/src/layout/dock/sd-dock-container.ts +1 -1
- package/dist/data/getOrmDataEditToastErrorMessage.d.ts +0 -2
- package/dist/data/getOrmDataEditToastErrorMessage.d.ts.map +0 -1
- package/dist/data/getOrmDataEditToastErrorMessage.js +0 -8
- package/docs/bootstrap.md +0 -38
- package/docs/directives.md +0 -236
- package/docs/features.md +0 -169
- package/docs/provider-types.md +0 -283
- package/docs/providers.md +0 -379
- package/docs/styling.md +0 -222
- package/docs/ui-data.md +0 -333
- package/docs/ui-form.md +0 -502
- package/docs/ui-navigation.md +0 -273
- package/docs/ui-overlay.md +0 -157
- package/docs/ui-visual.md +0 -127
- package/docs/utils.md +0 -244
- package/src/data/getOrmDataEditToastErrorMessage.ts +0 -10
|
@@ -1,43 +1,44 @@
|
|
|
1
|
-
# Recipe: 모달 기반 선택 버튼
|
|
1
|
+
# Recipe: 모달 기반 선택 버튼
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
모달을 띄워 항목을 선택하고, 결과를 `value`(key) ↔ `selectedItems`(표시용 객체) 양방향 바인딩으로 수신하는 버튼 컴포넌트를 조립한다. 표준 `<sd-modal-select-button>`을 직접 사용하거나, 도메인별 데이터 로딩을 감싼 wrapper 컴포넌트로 컴포지션한다.
|
|
4
4
|
|
|
5
|
-
##
|
|
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
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
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
|
-
##
|
|
24
|
+
## 기본 레시피 (패턴 1: `<sd-modal-select-button>` 직접 사용)
|
|
31
25
|
|
|
32
|
-
###
|
|
26
|
+
### 1. 모달 컴포넌트
|
|
33
27
|
|
|
34
|
-
|
|
28
|
+
모달은 `SdSelectModal<T>`를 `implements`한다. `selectMode` / `selectedItemKeys` input과 `close` output을 모두 구현한다.
|
|
35
29
|
|
|
36
30
|
```typescript
|
|
37
|
-
import {
|
|
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
|
|
64
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
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
|
-
##
|
|
147
|
+
## 변형 (Variation)
|
|
123
148
|
|
|
124
|
-
|
|
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
|
|
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. `
|
|
174
|
-
- `<ng-template [itemOf]="items()" let-item
|
|
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
|
-
|
|
205
|
+
### 패턴 3: 사용자 정의 wrapper (도메인별 ORM 조회)
|
|
178
206
|
|
|
179
|
-
도메인별로 자주 쓰는 모달 선택 버튼은 `<sd-modal-select-button>`을
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
-
|
|
285
|
-
- `
|
|
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
|
-
|
|
314
|
+
**근거**: `effect` 콜백에 `async`를 선언하면 cleanup 시점이 반환 Promise 해소 시점과 어긋난다. 관련 규칙: [_common-rules.md — input 의존 로딩 규칙](./_common-rules.md#input-의존-데이터-로딩에-void-this_initasync를-사용하지-않는다).
|
|
290
315
|
|
|
291
|
-
|
|
316
|
+
### 시트 셀 안에 삽입
|
|
317
|
+
|
|
318
|
+
`[inset]="true" [size]="'sm'"` 규칙은 공통 규칙을 따른다 — [_common-rules.md — 시트 셀 내부 컨트롤 규칙](./_common-rules.md#시트-셀-내부-컨트롤에-insettrue-sizesm을-명시한다) 참조.
|
|
292
319
|
|
|
293
320
|
```html
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
409
|
+
## 관련 Entry
|
|
315
410
|
|
|
316
|
-
-
|
|
317
|
-
-
|
|
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 필드 초기값 제약 등 횡단 규칙
|