@simplysm/angular 14.0.49 → 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/controls/select/sd-select.js +3 -3
- 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 +4 -4
- package/package.json +7 -8
- 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/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 +2 -1
- package/README.md +0 -467
- package/docs/bootstrap.md +0 -38
- package/docs/directives.md +0 -236
- package/docs/features.md +0 -154
- package/docs/pipes.md +0 -32
- package/docs/plugins.md +0 -37
- package/docs/provider-types.md +0 -283
- package/docs/providers.md +0 -379
- package/docs/recipes/crud-detail.md +0 -875
- package/docs/recipes/crud-list.md +0 -1037
- package/docs/recipes/data-select-button.md +0 -318
- package/docs/recipes/page-modal-container.md +0 -178
- package/docs/styling.md +0 -222
- package/docs/type-utilities.md +0 -250
- package/docs/ui-data.md +0 -333
- package/docs/ui-form.md +0 -502
- package/docs/ui-layout.md +0 -140
- package/docs/ui-navigation.md +0 -303
- package/docs/ui-overlay.md +0 -157
- package/docs/ui-visual.md +0 -127
- package/docs/utils.md +0 -244
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
# Recipe: 모달 기반 선택 버튼 직접 조립
|
|
2
|
-
|
|
3
|
-
소비 화면이 `<sd-modal-select-button>` 표준 컴포넌트를 **직접** 사용하거나, **컴포지션**(wrap)으로 도메인별 선택 버튼을 만든다. 과거 `SdDataSelectButton` / `SdDataSelectButtonBase` 추상화는 제거되었다.
|
|
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. 언제 사용하는가
|
|
20
|
-
|
|
21
|
-
| 상황 | 적용 패턴 |
|
|
22
|
-
|---|---|
|
|
23
|
-
| 외부 대형 테이블에서 모달로 선택, 결과를 key로 저장 | 패턴 3: 사용자 정의 select-button (load 비동기) |
|
|
24
|
-
| 메모리에 로드된 공유 데이터(`SharedDataBase`)에서 선택 | 패턴 2: `<sd-shared-data-select-button>` |
|
|
25
|
-
| 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` 참조) |
|
|
29
|
-
|
|
30
|
-
## 3. 패턴 1: `<sd-modal-select-button>` 직접 사용
|
|
31
|
-
|
|
32
|
-
### 3.1 모달 컴포넌트 구현
|
|
33
|
-
|
|
34
|
-
선택 모달은 `SdSelectModal<T>` 인터페이스를 구현한다.
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
import { Component, input, output, signal, ViewEncapsulation } from "@angular/core";
|
|
38
|
-
import {
|
|
39
|
-
SdSelectModal,
|
|
40
|
-
SelectModalOutputResult,
|
|
41
|
-
} from "@simplysm/angular";
|
|
42
|
-
|
|
43
|
-
interface IItem {
|
|
44
|
-
id: number;
|
|
45
|
-
name: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
@Component({
|
|
49
|
-
selector: "app-item-select-modal",
|
|
50
|
-
standalone: true,
|
|
51
|
-
encapsulation: ViewEncapsulation.None,
|
|
52
|
-
template: `
|
|
53
|
-
<div class="p-default">
|
|
54
|
-
<!-- 항목 리스트, 검색, 페이지네이션 등 -->
|
|
55
|
-
<button (click)="onConfirm()">확인</button>
|
|
56
|
-
<button (click)="close.emit(undefined)">취소</button>
|
|
57
|
-
</div>
|
|
58
|
-
`,
|
|
59
|
-
})
|
|
60
|
-
export class ItemSelectModal implements SdSelectModal<IItem> {
|
|
61
|
-
initialized = signal(true);
|
|
62
|
-
close = output<SelectModalOutputResult<IItem> | undefined>();
|
|
63
|
-
selectMode = input<"single" | "multi" | undefined>("single");
|
|
64
|
-
selectedItemKeys = input<any[]>([]);
|
|
65
|
-
|
|
66
|
-
// 내부에서 선택된 항목 관리 (예시)
|
|
67
|
-
private readonly _selectedItems = signal<IItem[]>([]);
|
|
68
|
-
|
|
69
|
-
onConfirm(): void {
|
|
70
|
-
const items = this._selectedItems();
|
|
71
|
-
this.close.emit({
|
|
72
|
-
selectedItemKeys: items.map((it) => it.id),
|
|
73
|
-
selectedItems: items,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 3.2 호출 측
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
import { Component, signal } from "@angular/core";
|
|
83
|
-
import { SdModalSelectButton, SdSelectModalInfo } from "@simplysm/angular";
|
|
84
|
-
|
|
85
|
-
@Component({
|
|
86
|
-
selector: "app-foo",
|
|
87
|
-
standalone: true,
|
|
88
|
-
imports: [SdModalSelectButton],
|
|
89
|
-
template: `
|
|
90
|
-
<sd-modal-select-button
|
|
91
|
-
[(value)]="value"
|
|
92
|
-
[(selectedItems)]="selectedItems"
|
|
93
|
-
[modal]="modalInfo"
|
|
94
|
-
[selectMode]="'single'"
|
|
95
|
-
>
|
|
96
|
-
@if (selectedItems().length > 0) {
|
|
97
|
-
{{ selectedItems()[0].name }}
|
|
98
|
-
} @else {
|
|
99
|
-
선택하세요
|
|
100
|
-
}
|
|
101
|
-
</sd-modal-select-button>
|
|
102
|
-
`,
|
|
103
|
-
})
|
|
104
|
-
export class FooPage {
|
|
105
|
-
value = signal<number | undefined>(undefined);
|
|
106
|
-
selectedItems = signal<IItem[]>([]);
|
|
107
|
-
|
|
108
|
-
modalInfo: SdSelectModalInfo<ItemSelectModal> = {
|
|
109
|
-
title: "항목 선택",
|
|
110
|
-
type: ItemSelectModal,
|
|
111
|
-
inputs: {},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
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 존재` 시 자동 표시
|
|
121
|
-
|
|
122
|
-
## 4. 패턴 2: `<sd-shared-data-select-button>` (공유 데이터)
|
|
123
|
-
|
|
124
|
-
메모리에 이미 로드된 `SharedDataBase` 기반 데이터에서 선택할 때 사용한다. `value` 변경 시 `items.filter(by __valueKey)`로 표시 데이터가 자동 채워진다 — 별도 `load()` 호출 불필요.
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
import { Component, signal } from "@angular/core";
|
|
128
|
-
import {
|
|
129
|
-
SdSharedDataSelectButton,
|
|
130
|
-
SdItemOfTemplate,
|
|
131
|
-
SdSelectModalInfo,
|
|
132
|
-
SharedDataBase,
|
|
133
|
-
} from "@simplysm/angular";
|
|
134
|
-
|
|
135
|
-
interface IShop extends SharedDataBase<number> {
|
|
136
|
-
__valueKey: number;
|
|
137
|
-
__searchText: string;
|
|
138
|
-
__isHidden: boolean;
|
|
139
|
-
name: string;
|
|
140
|
-
code: string;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
@Component({
|
|
144
|
-
selector: "app-bar",
|
|
145
|
-
standalone: true,
|
|
146
|
-
imports: [SdSharedDataSelectButton, SdItemOfTemplate],
|
|
147
|
-
template: `
|
|
148
|
-
<sd-shared-data-select-button
|
|
149
|
-
[(value)]="shopId"
|
|
150
|
-
[items]="shops()"
|
|
151
|
-
[modal]="shopModalInfo"
|
|
152
|
-
[selectMode]="'single'"
|
|
153
|
-
>
|
|
154
|
-
<ng-template [itemOf]="shops()" let-item>
|
|
155
|
-
<span>{{ item.code }} - {{ item.name }}</span>
|
|
156
|
-
</ng-template>
|
|
157
|
-
</sd-shared-data-select-button>
|
|
158
|
-
`,
|
|
159
|
-
})
|
|
160
|
-
export class BarPage {
|
|
161
|
-
shopId = signal<number | undefined>(undefined);
|
|
162
|
-
shops = signal<IShop[]>([]); // SdSharedDataProvider에서 로드
|
|
163
|
-
|
|
164
|
-
shopModalInfo: SdSelectModalInfo<ShopSelectModal> = {
|
|
165
|
-
title: "거래처 선택",
|
|
166
|
-
type: ShopSelectModal,
|
|
167
|
-
inputs: {},
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**핵심:**
|
|
173
|
-
- `items`가 source of truth. `value`(key) 또는 `items` 변경 시 표시되는 항목이 자동 재계산됨
|
|
174
|
-
- `<ng-template [itemOf]="items()" let-item>`은 항목 템플릿. multi 모드에서는 ", " 구분자로 자동 나열
|
|
175
|
-
- `selectMode="multi"`이면 `value`는 `number[]`
|
|
176
|
-
|
|
177
|
-
## 5. 패턴 3: 사용자 정의 select-button (LotSelectButton 패턴)
|
|
178
|
-
|
|
179
|
-
도메인별로 자주 쓰는 모달 선택 버튼은 `<sd-modal-select-button>`을 컴포지션하여 wrapper 컴포넌트로 만든다. value(key)만 저장하고 표시용 데이터는 비동기 `load(keys)`로 ORM에서 조회한다.
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
import {
|
|
183
|
-
ChangeDetectionStrategy,
|
|
184
|
-
Component,
|
|
185
|
-
computed,
|
|
186
|
-
effect,
|
|
187
|
-
inject,
|
|
188
|
-
input,
|
|
189
|
-
model,
|
|
190
|
-
signal,
|
|
191
|
-
ViewEncapsulation,
|
|
192
|
-
booleanAttribute,
|
|
193
|
-
} from "@angular/core";
|
|
194
|
-
import {
|
|
195
|
-
SdModalSelectButton,
|
|
196
|
-
SdSelectModalInfo,
|
|
197
|
-
} from "@simplysm/angular";
|
|
198
|
-
import { AppOrmProvider } from "../app-orm.provider";
|
|
199
|
-
import { expr } from "@simplysm/orm-common";
|
|
200
|
-
import { LotSelectModal } from "./lot-select.modal";
|
|
201
|
-
|
|
202
|
-
interface ILot {
|
|
203
|
-
id: number;
|
|
204
|
-
code: string;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
@Component({
|
|
208
|
-
selector: "app-lot-select-button",
|
|
209
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
210
|
-
encapsulation: ViewEncapsulation.None,
|
|
211
|
-
standalone: true,
|
|
212
|
-
imports: [SdModalSelectButton],
|
|
213
|
-
template: `
|
|
214
|
-
<sd-modal-select-button
|
|
215
|
-
[(value)]="value"
|
|
216
|
-
[(selectedItems)]="_selectedItems"
|
|
217
|
-
[modal]="modalInfo()"
|
|
218
|
-
[disabled]="disabled()"
|
|
219
|
-
[required]="required()"
|
|
220
|
-
[inset]="inset()"
|
|
221
|
-
[size]="size()"
|
|
222
|
-
[selectMode]="'single'"
|
|
223
|
-
>
|
|
224
|
-
@for (item of _selectedItems(); track item.id; let index = $index) {
|
|
225
|
-
@if (index !== 0) { <span>, </span> }
|
|
226
|
-
<span>{{ item.code }}</span>
|
|
227
|
-
}
|
|
228
|
-
</sd-modal-select-button>
|
|
229
|
-
`,
|
|
230
|
-
})
|
|
231
|
-
export class LotSelectButton {
|
|
232
|
-
private readonly _appOrm = inject(AppOrmProvider);
|
|
233
|
-
|
|
234
|
-
value = model<number | undefined>();
|
|
235
|
-
modalInputs = input<SdSelectModalInfo<LotSelectModal>["inputs"]>({});
|
|
236
|
-
disabled = input(false, { transform: booleanAttribute });
|
|
237
|
-
required = input(false, { transform: booleanAttribute });
|
|
238
|
-
inset = input(false, { transform: booleanAttribute });
|
|
239
|
-
size = input<"sm" | "lg">();
|
|
240
|
-
|
|
241
|
-
protected readonly _selectedItems = signal<ILot[]>([]);
|
|
242
|
-
|
|
243
|
-
protected readonly modalInfo = computed<SdSelectModalInfo<LotSelectModal>>(() => ({
|
|
244
|
-
type: LotSelectModal,
|
|
245
|
-
title: "LOT조회",
|
|
246
|
-
inputs: this.modalInputs(),
|
|
247
|
-
}));
|
|
248
|
-
|
|
249
|
-
constructor() {
|
|
250
|
-
// value 변경 시 비동기 load → _selectedItems 갱신
|
|
251
|
-
effect(() => {
|
|
252
|
-
const v = this.value();
|
|
253
|
-
if (v == null) {
|
|
254
|
-
this._selectedItems.set([]);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
void (async () => {
|
|
259
|
-
const items = await this._appOrm.connectAsync(async (db) =>
|
|
260
|
-
db.lot()
|
|
261
|
-
.where((it) => [expr.in(it.id, [v])])
|
|
262
|
-
.select((it) => ({ id: it.id, code: it.code }))
|
|
263
|
-
.execute(),
|
|
264
|
-
);
|
|
265
|
-
this._selectedItems.set(items);
|
|
266
|
-
})();
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
사용:
|
|
273
|
-
|
|
274
|
-
```html
|
|
275
|
-
<app-lot-select-button
|
|
276
|
-
[(value)]="data().lotId"
|
|
277
|
-
[required]="true"
|
|
278
|
-
[disabled]="!canEdit()"
|
|
279
|
-
/>
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
**핵심:**
|
|
283
|
-
- `<sd-modal-select-button>`이 모달 호출/erase/invalid 로직 담당 → wrapper는 비동기 load만 추가
|
|
284
|
-
- `_selectedItems`는 내부 signal. 외부에서는 `value`만 set
|
|
285
|
-
- effect 콜백은 동기여야 하므로 비동기 작업은 `void (async () => { ... })()` IIFE로 감싼다. 로드 함수가 한 곳에서만 호출되므로 별도 private 메서드로 분리하지 않고 effect 내부에 직접 인라인한다
|
|
286
|
-
- multi 모드를 지원하려면 `value = model<number[] | undefined>()`로 변경 + `selectMode = input<"single"|"multi">("single")` 추가 + effect의 `expr.in(it.id, [v])`를 배열 전체로 바꾸고 배열 길이 분기 추가
|
|
287
|
-
|
|
288
|
-
### 5.1 시트 셀 안에 삽입
|
|
289
|
-
|
|
290
|
-
`[inset]="true"` + `[size]="'sm'"`로 시트 셀에 자연스럽게 녹아든다 (관용 규칙):
|
|
291
|
-
|
|
292
|
-
```html
|
|
293
|
-
<sd-sheet-column [key]="'lotId'" [header]="'LOT'">
|
|
294
|
-
<ng-template [cell]="items()" let-item="item">
|
|
295
|
-
<app-lot-select-button
|
|
296
|
-
[inset]="true"
|
|
297
|
-
[size]="'sm'"
|
|
298
|
-
[(value)]="item.lotId"
|
|
299
|
-
(valueChange)="mark(items)"
|
|
300
|
-
/>
|
|
301
|
-
</ng-template>
|
|
302
|
-
</sd-sheet-column>
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
## 6. 주의사항
|
|
306
|
-
|
|
307
|
-
- **`SdDataSelectButton` / `SdDataSelectButtonBase`는 삭제됨.** 기존 `extends SdDataSelectButtonBase` 코드는 패턴 3(사용자 정의 select-button) 형태로 마이그레이션한다.
|
|
308
|
-
- **신규 추상화 클래스를 만들지 말 것.** `SelectButtonBase` 같은 공통 부모 클래스를 다시 만들면 본 WBS가 제거한 패턴이 되살아난다. 도메인별 wrapper 컴포넌트를 각자 직접 작성한다.
|
|
309
|
-
- **`SdSelectModal<T>` 인터페이스는 모달 컴포넌트가 직접 `implements`한다.** `selectMode`/`selectedItemKeys` `InputSignal`과 `close` `output<SelectModalOutputResult<T>>`를 모두 구현해야 한다.
|
|
310
|
-
- **`<sd-shared-data-select-button>`의 `selectedItems`는 외부 노출되지 않는다.** 내부 signal로 자동 관리되므로 외부에서는 `value` + `items`만 set한다. 직접 set이 필요하면 패턴 3로 wrapper를 작성한다.
|
|
311
|
-
- **`effect()` 내부의 비동기 호출은 `void` 키워드 또는 별도 메서드 호출**로 처리한다. effect 콜백을 `async`로 만들면 cleanup 시점이 어긋난다.
|
|
312
|
-
|
|
313
|
-
## 7. Cross-reference
|
|
314
|
-
|
|
315
|
-
- 선택 모달이 CRUD 리스트와 동일한 컴포넌트일 때 — [recipes/crud-list.md](./crud-list.md)의 "변형 2: 선택 모달 뷰" 섹션 참조
|
|
316
|
-
- 공유 데이터 드롭다운(`SdSharedDataSelect`) / 목록형 선택(`SdSharedDataSelectList`) — [features.md](../features.md) 참조
|
|
317
|
-
- `SdModalSelectButton` 자체 API — `packages/angular/src/controls/button/sd-modal-select-button.ts:148`
|
|
318
|
-
- `SharedDataBase` / `SdSharedDataProvider` — [features.md](../features.md), `packages/angular/src/core/shared-data/sd-shared-data.provider.ts`
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
# Recipe: 페이지/모달 컨테이너 직접 조립
|
|
2
|
-
|
|
3
|
-
소비 화면이 `<sd-busy-container>` · `<sd-topbar-container>` · `<sd-topbar>` 표준 컴포넌트를 **직접 조립**하여 "하나의 컴포넌트를 페이지·모달·제어(control) 뷰에서 모두 재사용"하는 구조를 만든다. 과거 `<sd-base-container>`가 감추고 있던 분기·타이틀 계산·권한 제한·초기화 숨김을 화면 내부에 인라인으로 풀어쓴다.
|
|
4
|
-
|
|
5
|
-
## 1. Overview
|
|
6
|
-
|
|
7
|
-
- 제거된 추상화: `SdBaseContainer`(`<sd-base-container>`)
|
|
8
|
-
- 대체: 소비 컴포넌트가 표준 조각을 `@if` 분기로 직접 조립
|
|
9
|
-
- 조립 요소:
|
|
10
|
-
- `<sd-busy-container [busy] [message]>` — 전체 busy 오버레이 (`@simplysm/angular` → `SdBusyContainer`, `busy`·`message`·`type` input)
|
|
11
|
-
- `<sd-topbar-container>` + `<sd-topbar>` — 페이지 뷰에서 상단 헤더
|
|
12
|
-
- `@if/@else if` — 뷰 타입 분기
|
|
13
|
-
- `injectViewTypeSignal()` — 라우트·모달 컨텍스트에 따라 `"page" | "modal" | "control"` 판정
|
|
14
|
-
- `computed()` — `header ?? 모달 타이틀 ?? 앱 구조 타이틀` 우선순위 타이틀 계산
|
|
15
|
-
- 유지되는 조력자: `SdActivatedModalProvider`, `SdAppStructureProvider`, `SdSystemLogProvider`, `injectCurrentPageCodeSignal`, `injectFullPageCodeSignal`
|
|
16
|
-
|
|
17
|
-
## 2. 언제 사용하는가
|
|
18
|
-
|
|
19
|
-
| 상황 | 적용 여부 |
|
|
20
|
-
|---|---|
|
|
21
|
-
| 하나의 화면 컴포넌트를 페이지와 모달 양쪽에서 재사용 | 레시피 전체 적용 |
|
|
22
|
-
| 페이지 뷰만 필요 (topbar 있는 라우트 화면) | 페이지 블록만 사용, 모달·control 분기 생략 가능 |
|
|
23
|
-
| 모달 뷰만 필요 (프로그래밍 방식 `SdModalProvider.showAsync()` 전용) | 모달 블록만 사용 |
|
|
24
|
-
| 다른 화면의 영역 일부로 삽입되는 컨트롤 | control 분기(`@else` 블록)만 사용. topbar·모달 분기 생략 |
|
|
25
|
-
| 커스텀 단축키·이탈 확인이 필요 | `SdCommandDirective` + `setupCanDeactivate`를 본문에 직접 부착 (본 레시피 범위 외, `features-data-detail.md`류 레시피 참조) |
|
|
26
|
-
|
|
27
|
-
## 3. 완성 예제
|
|
28
|
-
|
|
29
|
-
아래는 하나의 컴포넌트가 **페이지·모달·control 뷰 모두**를 커버하는 완성 형태다. 필요 없는 분기는 삭제하여 단순화할 수 있다.
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
import { NgIcon } from "@ng-icons/core";
|
|
33
|
-
import { tablerAlertTriangle } from "@ng-icons/tabler-icons";
|
|
34
|
-
import {
|
|
35
|
-
booleanAttribute,
|
|
36
|
-
ChangeDetectionStrategy,
|
|
37
|
-
Component,
|
|
38
|
-
computed,
|
|
39
|
-
inject,
|
|
40
|
-
input,
|
|
41
|
-
ViewEncapsulation,
|
|
42
|
-
} from "@angular/core";
|
|
43
|
-
import {
|
|
44
|
-
injectCurrentPageCodeSignal,
|
|
45
|
-
injectFullPageCodeSignal,
|
|
46
|
-
injectViewTypeSignal,
|
|
47
|
-
SdActivatedModalProvider,
|
|
48
|
-
SdAppStructureProvider,
|
|
49
|
-
SdBusyContainer,
|
|
50
|
-
SdSystemLogProvider,
|
|
51
|
-
SdTopbar,
|
|
52
|
-
SdTopbarContainer,
|
|
53
|
-
} from "@simplysm/angular";
|
|
54
|
-
|
|
55
|
-
@Component({
|
|
56
|
-
selector: "app-foo",
|
|
57
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
58
|
-
encapsulation: ViewEncapsulation.None,
|
|
59
|
-
standalone: true,
|
|
60
|
-
imports: [SdBusyContainer, SdTopbarContainer, SdTopbar, NgIcon],
|
|
61
|
-
template: `
|
|
62
|
-
<sd-busy-container [busy]="busy()" [message]="busyMessage()">
|
|
63
|
-
@if (initialized() == null || initialized()) {
|
|
64
|
-
@if (restricted()) {
|
|
65
|
-
<div class="fill tx-theme-gray-light p-xxl tx-center">
|
|
66
|
-
<br />
|
|
67
|
-
<ng-icon [svg]="tablerAlertTriangle" [size]="'5em'" />
|
|
68
|
-
<br />
|
|
69
|
-
<br />
|
|
70
|
-
'{{ modalOrPageTitle() }}'에 대한 사용권한이 없습니다. 시스템 관리자에게 문의하세요.
|
|
71
|
-
</div>
|
|
72
|
-
} @else if (viewType() === "page") {
|
|
73
|
-
<sd-topbar-container>
|
|
74
|
-
<sd-topbar>
|
|
75
|
-
<h4>{{ modalOrPageTitle() }}</h4>
|
|
76
|
-
<!-- 페이지 topbar 보조 영역(버튼·메뉴 등) 필요 시 여기 -->
|
|
77
|
-
</sd-topbar>
|
|
78
|
-
<div class="fill">
|
|
79
|
-
<!-- 본문: 페이지 레이아웃으로 채우기 -->
|
|
80
|
-
</div>
|
|
81
|
-
</sd-topbar-container>
|
|
82
|
-
} @else if (viewType() === "modal") {
|
|
83
|
-
<div class="flex-column fill">
|
|
84
|
-
<div class="flex-fill">
|
|
85
|
-
<!-- 본문: 모달 내부 컨텐츠 -->
|
|
86
|
-
</div>
|
|
87
|
-
<!-- 모달 하단 액션(확인/취소 버튼 등) 필요 시:
|
|
88
|
-
<div class="bdt bdt-theme-gray-lightest">
|
|
89
|
-
...
|
|
90
|
-
</div>
|
|
91
|
-
-->
|
|
92
|
-
</div>
|
|
93
|
-
} @else {
|
|
94
|
-
<!-- 본문: control 뷰(다른 화면의 영역으로 삽입) -->
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
</sd-busy-container>
|
|
98
|
-
`,
|
|
99
|
-
})
|
|
100
|
-
export class FooPage {
|
|
101
|
-
private readonly _sdActivatedModal = inject(SdActivatedModalProvider, { optional: true });
|
|
102
|
-
private readonly _sdAppStructure = inject(SdAppStructureProvider);
|
|
103
|
-
private readonly _sdSystemLog = inject(SdSystemLogProvider);
|
|
104
|
-
|
|
105
|
-
private readonly _fullPageCode = injectFullPageCodeSignal();
|
|
106
|
-
private readonly _currPageCode = injectCurrentPageCodeSignal();
|
|
107
|
-
|
|
108
|
-
protected readonly viewType = injectViewTypeSignal();
|
|
109
|
-
|
|
110
|
-
header = input<string>();
|
|
111
|
-
initialized = input<boolean | undefined>(undefined);
|
|
112
|
-
restricted = input(false, { transform: booleanAttribute });
|
|
113
|
-
busy = input(false, { transform: booleanAttribute });
|
|
114
|
-
busyMessage = input<string>();
|
|
115
|
-
|
|
116
|
-
protected readonly modalOrPageTitle = computed(() => {
|
|
117
|
-
try {
|
|
118
|
-
return (
|
|
119
|
-
this.header() ??
|
|
120
|
-
this._sdActivatedModal?.modalComponent()?.title() ??
|
|
121
|
-
this._sdAppStructure.getTitleByFullCode(this._currPageCode?.() ?? this._fullPageCode())
|
|
122
|
-
);
|
|
123
|
-
} catch (err) {
|
|
124
|
-
void this._sdSystemLog.writeAsync("warn", `modalOrPageTitle 계산 중 에러: ${String(err)}`);
|
|
125
|
-
return "";
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
protected readonly tablerAlertTriangle = tablerAlertTriangle;
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## 4. 분해 설명
|
|
134
|
-
|
|
135
|
-
각 블록의 역할과 원본 `SdBaseContainer` 코드 대응 지점:
|
|
136
|
-
|
|
137
|
-
| 블록 | 역할 | 원본 대응 |
|
|
138
|
-
|---|---|---|
|
|
139
|
-
| `<sd-busy-container [busy] [message]>` | 화면 전체에 busy 오버레이를 씌운다. 자식 전체를 감싼다 | `sd-base-container.ts:42` |
|
|
140
|
-
| `@if (initialized() == null || initialized())` | `undefined` 또는 `true`일 때만 자식 렌더. `false`면 콘텐츠 전부 숨김(초기화 전 잔상 방지) | `sd-base-container.ts:43` |
|
|
141
|
-
| `@if (restricted())` | 권한 없음 시 경고 메시지를 표시하고 콘텐츠는 렌더링하지 않음 | `sd-base-container.ts:44-51` |
|
|
142
|
-
| `@else if (viewType() === "page")` | 페이지 뷰: `<sd-topbar-container>` + `<sd-topbar>` 헤더에 계산된 제목 표시 | `sd-base-container.ts:52-63` |
|
|
143
|
-
| `@else if (viewType() === "modal")` | 모달 뷰: flex-column 레이아웃, 하단 액션은 선택 | `sd-base-container.ts:64-74` |
|
|
144
|
-
| `@else` | control 뷰: 본문만 raw 렌더링 | `sd-base-container.ts:75-77` |
|
|
145
|
-
| `modalOrPageTitle` computed | 제목 우선순위 계산 + 예외 시 빈 문자열 + `writeAsync("warn", ...)` | `sd-base-container.ts:102-113` |
|
|
146
|
-
|
|
147
|
-
## 5. 뷰 타입 결정
|
|
148
|
-
|
|
149
|
-
`injectViewTypeSignal()`은 인자 없이 호출한다. 내부 판정 규칙:
|
|
150
|
-
|
|
151
|
-
1. `SdActivatedModalProvider`가 주입 가능하면 **`"modal"`**
|
|
152
|
-
2. 현재 활성 라우트의 컴포넌트 `selector`가 이 화면의 `<host>.tagName`과 일치하면 **`"page"`**
|
|
153
|
-
3. 그 외는 **`"control"`** (다른 화면의 내부에 삽입된 영역)
|
|
154
|
-
|
|
155
|
-
일반적으로 이 자동 판정으로 충분하다. 수동 오버라이드가 필요한 특수 상황(예: 특정 페이지 안에 자기 자신을 모달처럼 보이게 하고 싶은 경우)에는 아래와 같이 **`injectViewTypeSignal()`은 필드 초기화 시점에 한 번만 호출**하고 `computed`에서는 signal만 재사용한다.
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
override = input<SdViewType>();
|
|
159
|
-
private readonly _autoViewType = injectViewTypeSignal();
|
|
160
|
-
protected readonly viewType = computed(() => this.override() ?? this._autoViewType());
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
`injectViewTypeSignal()` 내부는 `inject(SdActivatedModalProvider, { optional: true })` · `inject(ActivatedRoute, { optional: true })`를 호출한다. Angular `inject()`는 injection context(생성자 실행 중 또는 필드 초기화 시점) 안에서만 유효하므로, `computed` 콜백이나 effect 안에서 호출하면 `NG0203` 런타임 에러가 발생한다. 이 오버라이드는 추상화 복원을 부추기므로 **기본은 자동 판정으로 쓰기**를 권장한다.
|
|
164
|
-
|
|
165
|
-
## 6. 타이틀 우선순위
|
|
166
|
-
|
|
167
|
-
타이틀은 **화면 내부의 `computed`로 직접 계산**한다. 우선순위:
|
|
168
|
-
|
|
169
|
-
1. `header()` input이 지정되어 있으면 그 값
|
|
170
|
-
2. 모달 컨텍스트면 `SdActivatedModalProvider.modalComponent()?.title()`
|
|
171
|
-
3. 페이지 컨텍스트면 `SdAppStructureProvider.getTitleByFullCode(this._currPageCode?.() ?? this._fullPageCode())`
|
|
172
|
-
|
|
173
|
-
`getTitleByFullCode`는 앱 구조(`items`)에 해당 fullCode 항목이 없으면 `Error`를 던진다. 따라서 `try/catch`로 래핑하고 실패 시 빈 문자열을 반환하면서 `SdSystemLogProvider.writeAsync("warn", ...)`으로 경고를 남긴다. 이는 화면 생성 시점에 앱 구조 로딩이 지연되는 상황에서 화면 전체가 깨지지 않도록 하기 위함이다.
|
|
174
|
-
|
|
175
|
-
## 7. 주의사항
|
|
176
|
-
|
|
177
|
-
- **신규 유틸 함수를 추출하지 말 것.** `useBaseContainer()`, `computeModalOrPageTitle()` 같은 공통 헬퍼를 도입하면 이 레시피가 제거한 추상화가 다시 생긴다. 세 줄짜리 `computed`를 화면마다 반복하는 편이 낫다.
|
|
178
|
-
- **본문 채우기는 화면의 책임이다.** 위 예제의 `<!-- 본문: ... -->` 주석 자리에 `<sd-sheet>`(리스트), `<sd-form>`(상세), 임의 HTML 등 화면별 콘텐츠를 삽입한다. 리스트·상세 화면 조립은 `crud-list.md`·`crud-detail.md` 레시피 참조.
|