@simplysm/angular 14.0.51 → 14.0.52
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/dist/data/sheet/sd-sheet.d.ts +9 -12
- package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
- package/dist/data/sheet/sd-sheet.js +150 -168
- package/dist/data/sheet/types.d.ts +1 -0
- package/dist/data/sheet/types.d.ts.map +1 -1
- package/dist/data/sheet/useSheetCellStyling.d.ts +2 -2
- package/dist/data/sheet/useSheetCellStyling.d.ts.map +1 -1
- package/dist/data/sheet/useSheetCellStyling.js +20 -12
- package/dist/data/sheet/useSheetColumnFixing.d.ts +3 -8
- package/dist/data/sheet/useSheetColumnFixing.d.ts.map +1 -1
- package/dist/data/sheet/useSheetColumnFixing.js +19 -27
- package/dist/data/sheet/useSheetLayoutEngine.d.ts.map +1 -1
- package/dist/data/sheet/useSheetLayoutEngine.js +4 -1
- package/dist/layout/dock/sd-dock-container.d.ts.map +1 -1
- package/dist/layout/dock/sd-dock-container.js +3 -3
- package/package.json +7 -8
- package/src/data/sheet/sd-sheet.ts +39 -60
- package/src/data/sheet/types.ts +1 -0
- package/src/data/sheet/useSheetCellStyling.ts +19 -11
- package/src/data/sheet/useSheetColumnFixing.ts +21 -28
- package/src/data/sheet/useSheetLayoutEngine.ts +4 -1
- package/src/layout/dock/sd-dock-container.ts +1 -0
- package/README.md +0 -476
- package/docs/bootstrap/provide-sd-angular.md +0 -37
- package/docs/bootstrap/sd-angular-config-provider.md +0 -16
- package/docs/directives/sd-command-directive.md +0 -30
- package/docs/directives/sd-events.md +0 -25
- package/docs/directives/sd-intersection-directive.md +0 -36
- package/docs/directives/sd-invalid.md +0 -24
- package/docs/directives/sd-resize-directive.md +0 -42
- package/docs/directives/sd-ripple.md +0 -23
- package/docs/directives/sd-router-link.md +0 -38
- package/docs/directives/sd-show-effect.md +0 -18
- package/docs/directives/sd-typed-template.md +0 -69
- package/docs/features/sd-address-search-modal.md +0 -50
- package/docs/features/sd-permission-table.md +0 -20
- package/docs/features/sd-shared-data-components.md +0 -158
- package/docs/features/sd-tiptap-editor.md +0 -26
- package/docs/pipes/format-pipe.md +0 -41
- package/docs/plugins/sd-global-error-handler.md +0 -23
- package/docs/plugins/sd-option-event-plugin.md +0 -34
- package/docs/provider-types/sd-menu.md +0 -65
- package/docs/provider-types/sd-modal-content-def.md +0 -148
- package/docs/provider-types/sd-toast-content-def.md +0 -73
- package/docs/provider-types/shared-data-base.md +0 -59
- package/docs/providers/sd-activated-modal-provider.md +0 -34
- package/docs/providers/sd-app-structure-provider.md +0 -81
- package/docs/providers/sd-busy-provider.md +0 -18
- package/docs/providers/sd-file-dialog-provider.md +0 -40
- package/docs/providers/sd-local-storage-provider.md +0 -20
- package/docs/providers/sd-modal-provider.md +0 -67
- package/docs/providers/sd-navigate-window-provider.md +0 -18
- package/docs/providers/sd-print-provider.md +0 -25
- package/docs/providers/sd-service-client-factory-provider.md +0 -43
- package/docs/providers/sd-shared-data-provider.md +0 -64
- package/docs/providers/sd-system-config-provider.md +0 -46
- package/docs/providers/sd-system-log-provider.md +0 -18
- package/docs/providers/sd-theme-provider.md +0 -38
- package/docs/providers/sd-toast-provider.md +0 -65
- package/docs/recipes/_common-rules.md +0 -244
- package/docs/recipes/crud-detail/extension-a-edit-save.md +0 -230
- package/docs/recipes/crud-detail/extension-b-delete-restore.md +0 -142
- package/docs/recipes/crud-detail/extension-c-modal-view.md +0 -214
- package/docs/recipes/crud-detail/extension-d-control-view.md +0 -103
- package/docs/recipes/crud-detail/extension-e-auxiliary.md +0 -87
- package/docs/recipes/crud-detail/extension-f-complex-detail.md +0 -234
- package/docs/recipes/crud-detail.md +0 -353
- package/docs/recipes/crud-list/extension-a-inline-edit.md +0 -410
- package/docs/recipes/crud-list/extension-b-selection.md +0 -226
- package/docs/recipes/crud-list/extension-c-inline-delete.md +0 -87
- package/docs/recipes/crud-list/extension-d-select-modal.md +0 -207
- package/docs/recipes/crud-list/extension-e-readonly-modal.md +0 -165
- package/docs/recipes/crud-list/extension-f-modal-edit.md +0 -177
- package/docs/recipes/crud-list/extension-g-excel.md +0 -157
- package/docs/recipes/crud-list.md +0 -446
- package/docs/recipes/data-select-button.md +0 -412
- package/docs/recipes/page-modal-container.md +0 -260
- package/docs/styling/classes.md +0 -149
- package/docs/styling/mixins.md +0 -100
- package/docs/styling/themes.md +0 -35
- package/docs/styling/variables.md +0 -147
- package/docs/type-utilities/directive-input-signals.md +0 -232
- package/docs/ui-data/sd-list.md +0 -37
- package/docs/ui-data/sd-sheet.md +0 -227
- package/docs/ui-form/sd-additional-button.md +0 -26
- package/docs/ui-form/sd-anchor.md +0 -31
- package/docs/ui-form/sd-button.md +0 -105
- package/docs/ui-form/sd-checkbox-group.md +0 -39
- package/docs/ui-form/sd-checkbox.md +0 -81
- package/docs/ui-form/sd-date-range-picker.md +0 -27
- package/docs/ui-form/sd-form.md +0 -89
- package/docs/ui-form/sd-modal-select-button.md +0 -54
- package/docs/ui-form/sd-numpad.md +0 -26
- package/docs/ui-form/sd-range.md +0 -26
- package/docs/ui-form/sd-select.md +0 -68
- package/docs/ui-form/sd-shared-data-select.md +0 -52
- package/docs/ui-form/sd-state-preset.md +0 -37
- package/docs/ui-form/sd-switch.md +0 -27
- package/docs/ui-form/sd-textarea.md +0 -33
- package/docs/ui-form/sd-textfield.md +0 -145
- package/docs/ui-layout/sd-dock-container.md +0 -64
- package/docs/ui-layout/sd-dock.md +0 -37
- package/docs/ui-layout/sd-gap.md +0 -26
- package/docs/ui-layout/sd-kanban-board.md +0 -96
- package/docs/ui-layout/sd-kanban-lane.md +0 -34
- package/docs/ui-layout/sd-kanban.md +0 -29
- package/docs/ui-navigation/sd-collapse.md +0 -35
- package/docs/ui-navigation/sd-pagination.md +0 -26
- package/docs/ui-navigation/sd-sidebar-container.md +0 -49
- package/docs/ui-navigation/sd-sidebar-menu.md +0 -22
- package/docs/ui-navigation/sd-sidebar-user.md +0 -43
- package/docs/ui-navigation/sd-tab.md +0 -51
- package/docs/ui-navigation/sd-topbar-container.md +0 -97
- package/docs/ui-navigation/sd-topbar-menu.md +0 -23
- package/docs/ui-navigation/sd-topbar-user.md +0 -38
- package/docs/ui-navigation/sd-topbar.md +0 -30
- package/docs/ui-overlay/sd-busy-container.md +0 -69
- package/docs/ui-overlay/sd-confirm-modal.md +0 -30
- package/docs/ui-overlay/sd-dropdown.md +0 -40
- package/docs/ui-overlay/sd-modal.md +0 -34
- package/docs/ui-overlay/sd-prompt-modal.md +0 -30
- package/docs/ui-overlay/sd-toast.md +0 -35
- package/docs/ui-visual/sd-barcode.md +0 -36
- package/docs/ui-visual/sd-calendar.md +0 -34
- package/docs/ui-visual/sd-echarts.md +0 -32
- package/docs/ui-visual/sd-label.md +0 -24
- package/docs/ui-visual/sd-note.md +0 -23
- package/docs/ui-visual/sd-progress.md +0 -23
- package/docs/utils/inject-routing-signals.md +0 -161
- package/docs/utils/inject-sd-system-config-resource.md +0 -35
- package/docs/utils/mark.md +0 -43
- package/docs/utils/selection-managers.md +0 -96
- package/docs/utils/set-safe-style.md +0 -19
- package/docs/utils/setup-functions.md +0 -93
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
# Recipe: 모달 기반 선택 버튼
|
|
2
|
-
|
|
3
|
-
모달을 띄워 항목을 선택하고, 결과를 `value`(key) ↔ `selectedItems`(표시용 객체) 양방향 바인딩으로 수신하는 버튼 컴포넌트를 조립한다. 표준 `<sd-modal-select-button>`을 직접 사용하거나, 도메인별 데이터 로딩을 감싼 wrapper 컴포넌트로 컴포지션한다.
|
|
4
|
-
|
|
5
|
-
## When to use / When NOT to use
|
|
6
|
-
|
|
7
|
-
| 상황 | 적용 패턴 |
|
|
8
|
-
|------|-----------|
|
|
9
|
-
| 1회성 모달을 직접 띄워 선택 (도메인별 wrapper 불필요) | 패턴 1: `<sd-modal-select-button>` 직접 |
|
|
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`)
|
|
23
|
-
|
|
24
|
-
## 기본 레시피 (패턴 1: `<sd-modal-select-button>` 직접 사용)
|
|
25
|
-
|
|
26
|
-
### 1. 모달 컴포넌트
|
|
27
|
-
|
|
28
|
-
모달은 `SdSelectModal<T>`를 `implements`한다. `selectMode` / `selectedItemKeys` input과 `close` output을 모두 구현한다.
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
import {
|
|
32
|
-
Component,
|
|
33
|
-
input,
|
|
34
|
-
output,
|
|
35
|
-
signal,
|
|
36
|
-
ViewEncapsulation,
|
|
37
|
-
type InputSignal,
|
|
38
|
-
} from "@angular/core";
|
|
39
|
-
import {
|
|
40
|
-
SdSelectModal,
|
|
41
|
-
type SelectModalOutputResult,
|
|
42
|
-
} from "@simplysm/angular";
|
|
43
|
-
|
|
44
|
-
interface IItem {
|
|
45
|
-
id: number;
|
|
46
|
-
name: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
@Component({
|
|
50
|
-
selector: "app-item-select-modal",
|
|
51
|
-
standalone: true,
|
|
52
|
-
encapsulation: ViewEncapsulation.None,
|
|
53
|
-
template: `
|
|
54
|
-
<div class="p-default">
|
|
55
|
-
@for (item of _items(); track item.id) {
|
|
56
|
-
<div (click)="onItemClick(item)">{{ item.name }}</div>
|
|
57
|
-
}
|
|
58
|
-
<button (click)="onConfirm()">확인</button>
|
|
59
|
-
<button (click)="close.emit(undefined)">취소</button>
|
|
60
|
-
</div>
|
|
61
|
-
`,
|
|
62
|
-
})
|
|
63
|
-
export class ItemSelectModal implements SdSelectModal<IItem> {
|
|
64
|
-
initialized = signal(true);
|
|
65
|
-
close = output<SelectModalOutputResult<IItem> | undefined>();
|
|
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[]>([]);
|
|
73
|
-
|
|
74
|
-
onItemClick(item: IItem): void {
|
|
75
|
-
this._picked.update((prev) =>
|
|
76
|
-
this.selectMode() === "multi" ? [...prev, item] : [item],
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
onConfirm(): void {
|
|
81
|
-
const items = this._picked();
|
|
82
|
-
this.close.emit({
|
|
83
|
-
selectedItemKeys: items.map((it) => it.id),
|
|
84
|
-
selectedItems: items,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**핵심**:
|
|
91
|
-
- `close.emit(undefined)`는 취소(value 변경 없음), `close.emit({...})`는 확정 반환
|
|
92
|
-
- `initialized = signal(true)`는 `SdModalContentDef` 계약 필드로, 모달 초기화 완료 신호
|
|
93
|
-
|
|
94
|
-
### 2. 호출 측 컴포넌트
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
import { Component, signal } from "@angular/core";
|
|
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
|
-
}
|
|
108
|
-
|
|
109
|
-
@Component({
|
|
110
|
-
selector: "app-foo-view",
|
|
111
|
-
standalone: true,
|
|
112
|
-
imports: [SdModalSelectButton],
|
|
113
|
-
template: `
|
|
114
|
-
<sd-modal-select-button
|
|
115
|
-
[(value)]="value"
|
|
116
|
-
[(selectedItems)]="selectedItems"
|
|
117
|
-
[modal]="modalInfo"
|
|
118
|
-
[selectMode]="'single'"
|
|
119
|
-
[required]="true"
|
|
120
|
-
>
|
|
121
|
-
@if (selectedItems().length > 0) {
|
|
122
|
-
{{ selectedItems()[0].name }}
|
|
123
|
-
} @else {
|
|
124
|
-
선택하세요
|
|
125
|
-
}
|
|
126
|
-
</sd-modal-select-button>
|
|
127
|
-
`,
|
|
128
|
-
})
|
|
129
|
-
export class FooView {
|
|
130
|
-
value = signal<number | undefined>(undefined);
|
|
131
|
-
selectedItems = signal<IItem[]>([]);
|
|
132
|
-
|
|
133
|
-
modalInfo: SdSelectModalInfo<ItemSelectModal> = {
|
|
134
|
-
title: "항목 선택",
|
|
135
|
-
type: ItemSelectModal,
|
|
136
|
-
inputs: {},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
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`)
|
|
146
|
-
|
|
147
|
-
## 변형 (Variation)
|
|
148
|
-
|
|
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`).
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
import { Component, signal } from "@angular/core";
|
|
155
|
-
import {
|
|
156
|
-
SdSharedDataSelectButton,
|
|
157
|
-
SdItemOfTemplate,
|
|
158
|
-
type SdSelectModalInfo,
|
|
159
|
-
type SharedDataBase,
|
|
160
|
-
} from "@simplysm/angular";
|
|
161
|
-
import { ShopSelectModal } from "./shop-select.modal";
|
|
162
|
-
|
|
163
|
-
interface IShop extends SharedDataBase<number> {
|
|
164
|
-
__valueKey: number; // SharedDataBase 필수 식별자
|
|
165
|
-
__searchText: string; // 검색 매칭 대상
|
|
166
|
-
__isHidden: boolean; // 숨김 여부
|
|
167
|
-
name: string;
|
|
168
|
-
code: string;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@Component({
|
|
172
|
-
selector: "app-bar-view",
|
|
173
|
-
standalone: true,
|
|
174
|
-
imports: [SdSharedDataSelectButton, SdItemOfTemplate],
|
|
175
|
-
template: `
|
|
176
|
-
<sd-shared-data-select-button
|
|
177
|
-
[(value)]="shopId"
|
|
178
|
-
[items]="shops()"
|
|
179
|
-
[modal]="shopModalInfo"
|
|
180
|
-
[selectMode]="'single'"
|
|
181
|
-
>
|
|
182
|
-
<ng-template [itemOf]="shops()" let-item>
|
|
183
|
-
<span>{{ item.code }} - {{ item.name }}</span>
|
|
184
|
-
</ng-template>
|
|
185
|
-
</sd-shared-data-select-button>
|
|
186
|
-
`,
|
|
187
|
-
})
|
|
188
|
-
export class BarView {
|
|
189
|
-
shopId = signal<number | undefined>(undefined);
|
|
190
|
-
shops = signal<IShop[]>([]); // SdSharedDataProvider에서 로드
|
|
191
|
-
|
|
192
|
-
shopModalInfo: SdSelectModalInfo<ShopSelectModal> = {
|
|
193
|
-
title: "거래처 선택",
|
|
194
|
-
type: ShopSelectModal,
|
|
195
|
-
inputs: {},
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**핵심**:
|
|
201
|
-
- `items`가 source of truth. `selectedItems`는 외부로 노출되지 않고 내부 signal로 관리된다
|
|
202
|
-
- `<ng-template [itemOf]="items()" let-item>` 컨텍스트 디렉티브로 항목 템플릿을 정의한다. multi 모드에서는 선택된 항목들이 `, ` 구분자로 자동 나열된다
|
|
203
|
-
- `selectMode="multi"`이면 `value`는 `number[]`
|
|
204
|
-
|
|
205
|
-
### 패턴 3: 사용자 정의 wrapper (도메인별 ORM 조회)
|
|
206
|
-
|
|
207
|
-
도메인별로 자주 쓰는 모달 선택 버튼은 `<sd-modal-select-button>`을 컴포지션한 wrapper로 작성한다. `value`(key)만 외부에 노출하고, 표시용 데이터는 `effect`에서 비동기로 조회한다.
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
import {
|
|
211
|
-
booleanAttribute,
|
|
212
|
-
ChangeDetectionStrategy,
|
|
213
|
-
Component,
|
|
214
|
-
computed,
|
|
215
|
-
effect,
|
|
216
|
-
inject,
|
|
217
|
-
input,
|
|
218
|
-
model,
|
|
219
|
-
signal,
|
|
220
|
-
ViewEncapsulation,
|
|
221
|
-
} from "@angular/core";
|
|
222
|
-
import {
|
|
223
|
-
SdModalSelectButton,
|
|
224
|
-
type SdSelectModalInfo,
|
|
225
|
-
} from "@simplysm/angular";
|
|
226
|
-
import { expr } from "@simplysm/orm-common";
|
|
227
|
-
import { AppOrmProvider } from "../app-orm.provider";
|
|
228
|
-
import { LotSelectModal } from "./lot-select.modal";
|
|
229
|
-
|
|
230
|
-
interface ILot {
|
|
231
|
-
id: number;
|
|
232
|
-
code: string;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
@Component({
|
|
236
|
-
selector: "app-lot-select-button",
|
|
237
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
238
|
-
encapsulation: ViewEncapsulation.None,
|
|
239
|
-
standalone: true,
|
|
240
|
-
imports: [SdModalSelectButton],
|
|
241
|
-
template: `
|
|
242
|
-
<sd-modal-select-button
|
|
243
|
-
[(value)]="value"
|
|
244
|
-
[(selectedItems)]="_selectedItems"
|
|
245
|
-
[modal]="modalInfo()"
|
|
246
|
-
[disabled]="disabled()"
|
|
247
|
-
[required]="required()"
|
|
248
|
-
[inset]="inset()"
|
|
249
|
-
[size]="size()"
|
|
250
|
-
[selectMode]="'single'"
|
|
251
|
-
>
|
|
252
|
-
@for (item of _selectedItems(); track item.id; let index = $index) {
|
|
253
|
-
@if (index !== 0) { <span>, </span> }
|
|
254
|
-
<span>{{ item.code }}</span>
|
|
255
|
-
}
|
|
256
|
-
</sd-modal-select-button>
|
|
257
|
-
`,
|
|
258
|
-
})
|
|
259
|
-
export class LotSelectButton {
|
|
260
|
-
private readonly _appOrm = inject(AppOrmProvider);
|
|
261
|
-
|
|
262
|
-
value = model<number | undefined>();
|
|
263
|
-
modalInputs = input<SdSelectModalInfo<LotSelectModal>["inputs"]>({});
|
|
264
|
-
disabled = input(false, { transform: booleanAttribute });
|
|
265
|
-
required = input(false, { transform: booleanAttribute });
|
|
266
|
-
inset = input(false, { transform: booleanAttribute });
|
|
267
|
-
size = input<"sm" | "lg">();
|
|
268
|
-
|
|
269
|
-
protected readonly _selectedItems = signal<ILot[]>([]);
|
|
270
|
-
|
|
271
|
-
protected readonly modalInfo = computed<SdSelectModalInfo<LotSelectModal>>(() => ({
|
|
272
|
-
type: LotSelectModal,
|
|
273
|
-
title: "LOT 조회",
|
|
274
|
-
inputs: this.modalInputs(),
|
|
275
|
-
}));
|
|
276
|
-
|
|
277
|
-
constructor() {
|
|
278
|
-
effect(() => {
|
|
279
|
-
const v = this.value();
|
|
280
|
-
if (v == null) {
|
|
281
|
-
this._selectedItems.set([]);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
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
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
사용:
|
|
301
|
-
|
|
302
|
-
```html
|
|
303
|
-
<app-lot-select-button
|
|
304
|
-
[(value)]="data().lotId"
|
|
305
|
-
[required]="true"
|
|
306
|
-
[disabled]="!canEdit()"
|
|
307
|
-
/>
|
|
308
|
-
```
|
|
309
|
-
|
|
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`에 반영된다
|
|
313
|
-
|
|
314
|
-
**근거**: `effect` 콜백에 `async`를 선언하면 cleanup 시점이 반환 Promise 해소 시점과 어긋난다. 관련 규칙: [_common-rules.md — input 의존 로딩 규칙](./_common-rules.md#input-의존-데이터-로딩에-void-this_initasync를-사용하지-않는다).
|
|
315
|
-
|
|
316
|
-
### 시트 셀 안에 삽입
|
|
317
|
-
|
|
318
|
-
`[inset]="true" [size]="'sm'"` 규칙은 공통 규칙을 따른다 — [_common-rules.md — 시트 셀 내부 컨트롤 규칙](./_common-rules.md#시트-셀-내부-컨트롤에-insettrue-sizesm을-명시한다) 참조.
|
|
319
|
-
|
|
320
|
-
```html
|
|
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
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
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
|
-
```
|
|
406
|
-
|
|
407
|
-
**근거**: `SdSharedDataSelectButton._selectedItems`는 `protected readonly signal`로, `items` + `value`로부터 내부 effect가 자동 파생한다 (`packages/angular/src/data/shared-data/sd-shared-data-select-button.ts:79`). 외부에서 set하면 자동 파생 값과 즉시 덮어쓰기 경합이 발생한다.
|
|
408
|
-
|
|
409
|
-
## 관련 Entry
|
|
410
|
-
|
|
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 필드 초기값 제약 등 횡단 규칙
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
# 페이지/모달 컨테이너 직접 조립
|
|
2
|
-
|
|
3
|
-
소비 화면이 `<sd-busy-container>` · `<sd-topbar-container>` · `<sd-topbar>` 표준 컴포넌트를 직접 조립해 하나의 컴포넌트를 page · modal · control 3뷰로 재사용한다. 추상 컨테이너(`<sd-base-container>` 류)가 감추던 분기·타이틀·권한·초기화 숨김을 화면 내부에 인라인으로 풀어 쓴다.
|
|
4
|
-
|
|
5
|
-
## When to use / When NOT to use
|
|
6
|
-
|
|
7
|
-
- ✅ 한 컴포넌트를 page · modal · control 중 둘 이상으로 재사용한다.
|
|
8
|
-
- ✅ page 전용 / modal 전용 / control 전용 화면도 본 레시피에서 필요한 분기만 골라 쓴다 (불필요한 분기는 삭제).
|
|
9
|
-
- ✅ `injectViewTypeSignal()`로 자동 판정된 뷰에 따라 topbar·하단 액션 바·본문 레이아웃을 선택한다.
|
|
10
|
-
- ❌ 단일 화면 컨테이너 추상(`<sd-base-container>`, `useBaseContainer()` 등) 재도입 — 분기·타이틀·권한이 다시 감춰져 화면별 변경이 어려워진다.
|
|
11
|
-
- ❌ 리스트 본문 조립이 필요할 때 — [`crud-list.md`](./crud-list.md) (시트·필터·페이징).
|
|
12
|
-
- ❌ 상세 폼 본문 조립이 필요할 때 — [`crud-detail.md`](./crud-detail.md) (폼·저장 흐름).
|
|
13
|
-
- ❌ modal 뷰의 (a) 다른 화면에서 항목을 골라 돌려주는 **선택 모달** — [`crud-list/extension-d-select-modal.md`](./crud-list/extension-d-select-modal.md).
|
|
14
|
-
- ❌ modal 뷰의 (b) 부모 레코드 자식 목록·이력을 input으로 받아 보여주는 **조회 전용 modal** — [`crud-list/extension-e-readonly-modal.md`](./crud-list/extension-e-readonly-modal.md).
|
|
15
|
-
|
|
16
|
-
## 전제조건
|
|
17
|
-
|
|
18
|
-
- `provideSdAngular({ clientName })` 등록 (앱 부트스트랩) — `SdBusyProvider`·`SdAppStructureProvider` 등 의존 provider가 함께 등록된다.
|
|
19
|
-
- page 뷰 사용 시: 라우트 등록 + `SdAppStructureProvider`에 페이지 코드·타이틀이 정의되어 있어야 `injectViewTitleSignal()`이 페이지 타이틀을 반환한다 (`packages/angular/src/core/routing/injectViewTitleSignal.ts:20`).
|
|
20
|
-
- modal 뷰 사용 시: `SdModalProvider.showAsync(...)`로 진입한다. `SdActivatedModalProvider`가 모달 컨텍스트에 자동 주입된다 (`packages/angular/src/core/modal/sd-activated-modal.provider.ts:8`).
|
|
21
|
-
- 공통 규칙: `injectViewTypeSignal()` 호출 시점, page 컴포넌트의 `<sd-topbar>` 소유 → [`_common-rules.md`](./_common-rules.md).
|
|
22
|
-
|
|
23
|
-
## 기본 레시피
|
|
24
|
-
|
|
25
|
-
3뷰를 모두 갖춘 최대 구성이다. 화면 요구에 따라 일부 분기·요소를 생략한다 (생략 기준은 [§ 변형](#변형) 참조).
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
import { NgIcon } from "@ng-icons/core";
|
|
29
|
-
import { tablerAlertTriangle } from "@ng-icons/tabler-icons";
|
|
30
|
-
import {
|
|
31
|
-
booleanAttribute,
|
|
32
|
-
ChangeDetectionStrategy,
|
|
33
|
-
Component,
|
|
34
|
-
input,
|
|
35
|
-
ViewEncapsulation,
|
|
36
|
-
} from "@angular/core";
|
|
37
|
-
import {
|
|
38
|
-
injectViewTitleSignal,
|
|
39
|
-
injectViewTypeSignal,
|
|
40
|
-
SdBusyContainer,
|
|
41
|
-
SdTopbar,
|
|
42
|
-
SdTopbarContainer,
|
|
43
|
-
} from "@simplysm/angular";
|
|
44
|
-
|
|
45
|
-
@Component({
|
|
46
|
-
selector: "app-foo-view",
|
|
47
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
48
|
-
encapsulation: ViewEncapsulation.None,
|
|
49
|
-
standalone: true,
|
|
50
|
-
imports: [SdBusyContainer, SdTopbarContainer, SdTopbar, NgIcon],
|
|
51
|
-
template: `
|
|
52
|
-
<sd-busy-container [busy]="busy()" [message]="busyMessage()">
|
|
53
|
-
<!-- initialized() == null(미사용) 또는 true 일 때만 본문 렌더 — 초기 로딩 잔상 방지 -->
|
|
54
|
-
@if (initialized() == null || initialized()) {
|
|
55
|
-
@if (restricted()) {
|
|
56
|
-
<div class="fill tx-theme-gray-light p-xxl tx-center">
|
|
57
|
-
<br />
|
|
58
|
-
<ng-icon [svg]="tablerAlertTriangle" [size]="'5em'" />
|
|
59
|
-
<br />
|
|
60
|
-
<br />
|
|
61
|
-
'{{ viewTitle() }}'에 대한 사용권한이 없습니다. 시스템 관리자에게 문의하세요.
|
|
62
|
-
</div>
|
|
63
|
-
} @else if (viewType() === "page") {
|
|
64
|
-
<sd-topbar-container>
|
|
65
|
-
<sd-topbar>
|
|
66
|
-
<h4>{{ viewTitle() }}</h4>
|
|
67
|
-
<!-- 페이지 topbar 보조 영역(버튼·메뉴) 필요 시 여기 -->
|
|
68
|
-
</sd-topbar>
|
|
69
|
-
<div class="fill">
|
|
70
|
-
<!-- 본문: 페이지 레이아웃으로 채우기 -->
|
|
71
|
-
</div>
|
|
72
|
-
</sd-topbar-container>
|
|
73
|
-
} @else if (viewType() === "modal") {
|
|
74
|
-
<!--
|
|
75
|
-
modal 용도는 사전에 (a) 선택 모달 / (b) 조회 전용 modal 중 하나로 확정한다.
|
|
76
|
-
(a) → ./crud-list/extension-d-select-modal.md
|
|
77
|
-
(b) → ./crud-list/extension-e-readonly-modal.md
|
|
78
|
-
-->
|
|
79
|
-
<div class="flex-column fill">
|
|
80
|
-
<div class="flex-fill">
|
|
81
|
-
<!-- 본문: 모달 내부 컨텐츠 -->
|
|
82
|
-
</div>
|
|
83
|
-
<!-- 모달 하단 액션(확인/취소) 필요 시:
|
|
84
|
-
<div class="bdt bdt-theme-gray-lightest">...</div>
|
|
85
|
-
-->
|
|
86
|
-
</div>
|
|
87
|
-
} @else {
|
|
88
|
-
<!-- 본문: control 뷰(다른 화면의 영역으로 삽입) -->
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
</sd-busy-container>
|
|
92
|
-
`,
|
|
93
|
-
})
|
|
94
|
-
export class FooView {
|
|
95
|
-
// injectViewTypeSignal/injectViewTitleSignal은 inject() 내부 호출이므로
|
|
96
|
-
// 필드 이니셜라이저(=생성자 시점)에서만 호출한다 → ./_common-rules.md
|
|
97
|
-
protected readonly viewType = injectViewTypeSignal();
|
|
98
|
-
protected readonly viewTitle = injectViewTitleSignal();
|
|
99
|
-
|
|
100
|
-
initialized = input<boolean | undefined>(undefined);
|
|
101
|
-
restricted = input(false, { transform: booleanAttribute });
|
|
102
|
-
busy = input(false, { transform: booleanAttribute });
|
|
103
|
-
busyMessage = input<string>();
|
|
104
|
-
|
|
105
|
-
protected readonly tablerAlertTriangle = tablerAlertTriangle;
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
뷰 타입 자동 판정 규칙 (`packages/angular/src/core/routing/injectViewTypeSignal.ts:7`):
|
|
110
|
-
|
|
111
|
-
1. `SdActivatedModalProvider`가 주입 가능하면 `"modal"`
|
|
112
|
-
2. 활성 라우트의 컴포넌트 selector가 호스트 `tagName`과 일치하면 `"page"`
|
|
113
|
-
3. 그 외는 `"control"`
|
|
114
|
-
|
|
115
|
-
타이틀 자동 판정 규칙 (`packages/angular/src/core/routing/injectViewTitleSignal.ts:7`):
|
|
116
|
-
|
|
117
|
-
1. 모달 컨텍스트면 `SdActivatedModalProvider.modalComponent()?.title()`
|
|
118
|
-
2. 페이지 컨텍스트면 `SdAppStructureProvider.getTitleByFullCode(...)`
|
|
119
|
-
3. 예외 시 빈 문자열 (별도 try/catch 불필요)
|
|
120
|
-
|
|
121
|
-
## 변형
|
|
122
|
-
|
|
123
|
-
### 분기·요소 포함 조건 (생략 가능 기준)
|
|
124
|
-
|
|
125
|
-
기계적으로 모든 분기·요소를 포함하지 않는다. 화면 요구에 따라 아래 표대로 생략한다.
|
|
126
|
-
|
|
127
|
-
| 요소 | 포함 조건 | 생략하는 경우 예시 |
|
|
128
|
-
|------|----------|-------------------|
|
|
129
|
-
| `viewType() === "page"` 분기 + `<sd-topbar>` | 라우트로 진입하는 페이지 뷰가 필요할 때 | 모달/control 전용 컴포넌트 |
|
|
130
|
-
| `viewType() === "modal"` 분기 | 모달 뷰로도 재사용될 때 | page 전용 또는 control 전용 컴포넌트 |
|
|
131
|
-
| `@else` (control) 분기 | 다른 화면의 영역으로 삽입될 때 | page/modal 전용 컴포넌트 |
|
|
132
|
-
| `busy` / `busyMessage` input | 비동기 작업이 있어서 busy 표시가 필요할 때 | 동기적으로 렌더되는 화면 |
|
|
133
|
-
| `initialized` input | 초기 로딩 완료 전 본문을 숨겨야 할 때 | 초기 로딩 없이 즉시 렌더 가능한 화면 |
|
|
134
|
-
| `restricted` input + 권한 없음 메시지 | 권한 제어가 있는 화면 | 권한 제어가 없는 화면 |
|
|
135
|
-
| `injectViewTitleSignal()` | topbar에 타이틀을 표시할 때 | 타이틀 불필요한 화면 |
|
|
136
|
-
|
|
137
|
-
### page 전용 (modal·control 분기 생략)
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
template: `
|
|
141
|
-
<sd-busy-container [busy]="busy()">
|
|
142
|
-
@if (initialized() == null || initialized()) {
|
|
143
|
-
<sd-topbar-container>
|
|
144
|
-
<sd-topbar><h4>{{ viewTitle() }}</h4></sd-topbar>
|
|
145
|
-
<div class="fill"><!-- 본문 --></div>
|
|
146
|
-
</sd-topbar-container>
|
|
147
|
-
}
|
|
148
|
-
</sd-busy-container>
|
|
149
|
-
`,
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### modal 전용 (page·control 분기 생략)
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
template: `
|
|
156
|
-
<sd-busy-container [busy]="busy()">
|
|
157
|
-
<div class="flex-column fill">
|
|
158
|
-
<div class="flex-fill"><!-- 본문 --></div>
|
|
159
|
-
</div>
|
|
160
|
-
</sd-busy-container>
|
|
161
|
-
`,
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
이 분기 안에서 (a) 선택 모달 또는 (b) 조회 전용 modal을 결정한다 — 아래 "🚫 흔한 실수" 섹션의 "modal = 선택 모달 단정 금지" 참조.
|
|
165
|
-
|
|
166
|
-
### control 전용 (page·modal 분기 생략)
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
template: `
|
|
170
|
-
<sd-busy-container [busy]="busy()">
|
|
171
|
-
<!-- 본문: 다른 화면의 영역으로 삽입 -->
|
|
172
|
-
</sd-busy-container>
|
|
173
|
-
`,
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
control 뷰에는 `<sd-topbar-container>`·`<sd-topbar>`를 두지 않는다 (page 컴포넌트가 소유) — [`_common-rules.md` "page 컴포넌트가 <sd-topbar-container>와 <sd-topbar>를 소유한다"](./_common-rules.md#page-컴포넌트가-sd-topbar-container와-sd-topbar를-소유한다).
|
|
177
|
-
|
|
178
|
-
### viewType 수동 오버라이드 (권장하지 않음)
|
|
179
|
-
|
|
180
|
-
자동 판정으로 충분하지 않은 특수 상황(예: 특정 페이지 안에서 자기 자신을 모달처럼 보이게 함)에서만 사용한다. 추상화 복원을 부추기므로 기본은 자동 판정을 쓴다.
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
import { computed, input } from "@angular/core";
|
|
184
|
-
import { injectViewTypeSignal, type SdViewType } from "@simplysm/angular";
|
|
185
|
-
|
|
186
|
-
override = input<SdViewType>();
|
|
187
|
-
|
|
188
|
-
// injectViewTypeSignal()은 필드 이니셜라이저에서 한 번만 호출, 이후 computed에서 signal만 읽는다.
|
|
189
|
-
private readonly _autoViewType = injectViewTypeSignal();
|
|
190
|
-
protected readonly viewType = computed(() => this.override() ?? this._autoViewType());
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## 🚫 흔한 실수
|
|
194
|
-
|
|
195
|
-
### `<sd-base-container>` 재도입
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
// ❌ 단일 컨테이너 추상 컴포넌트로 분기를 다시 감춘다
|
|
199
|
-
template: `
|
|
200
|
-
<sd-base-container [busy]="busy()" [restricted]="restricted()">
|
|
201
|
-
<ng-content />
|
|
202
|
-
</sd-base-container>
|
|
203
|
-
`,
|
|
204
|
-
|
|
205
|
-
// ✅ 표준 조각을 화면이 직접 조립한다 (분기·타이틀·권한 가시화)
|
|
206
|
-
template: `
|
|
207
|
-
<sd-busy-container [busy]="busy()">
|
|
208
|
-
@if (restricted()) { ... }
|
|
209
|
-
@else if (viewType() === "page") { <sd-topbar-container>... </sd-topbar-container> }
|
|
210
|
-
@else if (viewType() === "modal") { ... }
|
|
211
|
-
@else { ... }
|
|
212
|
-
</sd-busy-container>
|
|
213
|
-
`,
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
**근거**: 추상 컨테이너는 page/modal/control 분기, 타이틀 계산, 권한 차단, 초기화 숨김을 한 번에 감추므로 화면별로 일부 동작만 바꾸기 어렵다. 표준 조각 직접 조립으로 분기를 인라인 노출한다.
|
|
217
|
-
|
|
218
|
-
### `useBaseContainer()` 같은 공통 헬퍼 추출
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
// ❌ 공통 헬퍼로 다시 추상화한다 — 결국 <sd-base-container>와 동일한 함정
|
|
222
|
-
const { template } = useBaseContainer({ busy, restricted, initialized });
|
|
223
|
-
|
|
224
|
-
// ✅ 화면 코드에 인라인으로 둔다
|
|
225
|
-
@Component({ template: ` <sd-busy-container [busy]="busy()"> ... </sd-busy-container> ` })
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
**근거**: 헬퍼 함수 형태로 분기·요소를 묶으면 본 레시피가 제거한 추상화가 다시 생긴다. "한 번만 쓰는 화면별 조립 코드"라는 형태가 의도적이다.
|
|
229
|
-
|
|
230
|
-
### `viewType() === "modal"`만으로 선택 모달이라고 단정한다
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
// ❌ modal 뷰면 무조건 SdSelectModal<T> 계약을 부착하고 close.emit으로 결과 반환을 기대한다
|
|
234
|
-
export class FooView implements SdSelectModal<FooItem> {
|
|
235
|
-
selectMode = input<"single" | "multi">();
|
|
236
|
-
selectedItemKeys = input<any[]>();
|
|
237
|
-
close = output<FooItem[] | undefined>();
|
|
238
|
-
// ...
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ✅ modal 용도를 사전에 (a)/(b) 중 하나로 확정하고 그 레시피를 따른다
|
|
242
|
-
// (a) 선택 모달: implements SdSelectModal<T> + 하단 액션 바 → ./crud-list/extension-d-select-modal.md
|
|
243
|
-
// (b) 조회 전용 modal: 계약 없음, SdModal 기본 "X"로 닫음 → ./crud-list/extension-e-readonly-modal.md
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**근거**: `viewType() === "modal"`은 "모달 컨텍스트에서 렌더 중"만 알려준다. 선택 모달 계약(`SdSelectModal<T>`)은 호출하는 쪽이 `selectMode`를 넘기고 결과를 받는 시나리오 한정이며, 조회 전용 modal과는 input·output·하단 액션 바 구성이 다르다.
|
|
247
|
-
|
|
248
|
-
### `injectViewTypeSignal()` 호출 시점 위반 (NG0203)
|
|
249
|
-
|
|
250
|
-
`computed`/`effect`/일반 메서드 콜백 안에서 `injectViewTypeSignal()`을 호출하면 injection context를 벗어나 `NG0203` 런타임 에러가 발생한다. 필드 이니셜라이저(=생성자 시점)에서 한 번만 호출하고 이후엔 반환된 signal만 읽는다 — 상세·코드 예시는 [`_common-rules.md` "injectViewTypeSignal()은 생성자 또는 필드 이니셜라이저에서만 호출한다"](./_common-rules.md#injectviewtypesignal은-생성자-또는-필드-이니셜라이저에서만-호출한다).
|
|
251
|
-
|
|
252
|
-
## 관련 Entry
|
|
253
|
-
|
|
254
|
-
- [`_common-rules.md`](./_common-rules.md) — 차이: 4계열 진입점·확장에 걸친 횡단 규칙 (본 레시피의 `injectViewTypeSignal` 호출 시점·`<sd-topbar>` 소유 규칙 정의 위치).
|
|
255
|
-
- [`crud-list.md`](./crud-list.md) — 차이: 리스트 본문(시트·필터·페이징) 조립.
|
|
256
|
-
- [`crud-detail.md`](./crud-detail.md) — 차이: 상세 폼 본문(폼·저장 흐름) 조립.
|
|
257
|
-
- [`crud-list/extension-d-select-modal.md`](./crud-list/extension-d-select-modal.md) — 차이: modal 뷰의 (a) 선택 모달 계약·하단 액션 바.
|
|
258
|
-
- [`crud-list/extension-e-readonly-modal.md`](./crud-list/extension-e-readonly-modal.md) — 차이: modal 뷰의 (b) 조회 전용 패턴.
|
|
259
|
-
- [`crud-detail/extension-c-modal-view.md`](./crud-detail/extension-c-modal-view.md) — 차이: 상세 폼의 modal 분기(canDeactivate·하단 액션 템플릿).
|
|
260
|
-
- [`crud-detail/extension-d-control-view.md`](./crud-detail/extension-d-control-view.md) — 차이: 상세 폼의 control 분기(마스터-디테일 디테일 영역).
|