@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,353 +0,0 @@
|
|
|
1
|
-
# Recipe: CRUD 상세폼 화면 직접 조립
|
|
2
|
-
|
|
3
|
-
단일 레코드 CRUD 상세폼을, `<sd-busy-container>` · `<sd-topbar-container>` · `<sd-topbar>` · `<sd-form>` 표준 컴포넌트를 소비 화면이 **직접 조립**하여 구성한다. 과거 `SdDataDetail` / `SdDataDetailBase`가 감추고 있던 load·save·delete 라이프사이클, snapshot 기반 변경 감지, 이탈 방지, busy 카운트, Ctrl+S/Ctrl+Alt+L 단축키를 화면 내부에 인라인으로 풀어쓴다.
|
|
4
|
-
|
|
5
|
-
## When to use / When NOT to use
|
|
6
|
-
|
|
7
|
-
- ✅ 단일 레코드(고객, 주문 헤더 등)의 CRUD 상세폼을 만들 때
|
|
8
|
-
- ✅ page / modal / control 뷰 중 **필요한 뷰만 선택적으로** 지원해야 할 때
|
|
9
|
-
- ✅ 편집·저장·삭제·복구·보조 기능·복합 상세(내부 시트)를 상황에 따라 누적해야 할 때
|
|
10
|
-
- ❌ CRUD 리스트(시트) 화면 → [`crud-list.md`](./crud-list.md)
|
|
11
|
-
- ❌ 페이지/모달 뷰 분기만 필요한 단순 컨테이너(라이프사이클 없음) → [`page-modal-container.md`](./page-modal-container.md)
|
|
12
|
-
- ❌ 다른 화면에서 데이터를 고르는 선택 버튼 → [`data-select-button.md`](./data-select-button.md)
|
|
13
|
-
|
|
14
|
-
## 전제조건
|
|
15
|
-
|
|
16
|
-
- `provideSdAngular({ clientName })` 부트스트랩 완료
|
|
17
|
-
- 앱별 ORM provider (예: `AppOrmProvider`) — `@simplysm/angular`가 아니라 각 앱이 소유
|
|
18
|
-
- 권한 제어가 있는 경우 `SdAppStructureProvider` 등록 (`injectPermsSignal` 전제)
|
|
19
|
-
|
|
20
|
-
## 기본 레시피 (page 뷰, 읽기 전용)
|
|
21
|
-
|
|
22
|
-
라우트 진입 시 `itemId`를 받아 단일 레코드를 로드하고 읽기 전용 필드로 표시하는 최소 완성 컴포넌트다. 편집·삭제·modal·control·보조·복합이 필요하면 [변형](#변형-확장-a-f-인덱스)의 확장을 선택적으로 얹는다.
|
|
23
|
-
|
|
24
|
-
```typescript
|
|
25
|
-
import { NgIcon } from "@ng-icons/core";
|
|
26
|
-
import { tablerAlertTriangle, tablerRefresh } from "@ng-icons/tabler-icons";
|
|
27
|
-
import {
|
|
28
|
-
ChangeDetectionStrategy,
|
|
29
|
-
Component,
|
|
30
|
-
effect,
|
|
31
|
-
inject,
|
|
32
|
-
input,
|
|
33
|
-
signal,
|
|
34
|
-
untracked,
|
|
35
|
-
ViewEncapsulation,
|
|
36
|
-
} from "@angular/core";
|
|
37
|
-
import type { DateTime } from "@simplysm/core-common";
|
|
38
|
-
import {
|
|
39
|
-
FormatPipe,
|
|
40
|
-
injectPermsSignal,
|
|
41
|
-
injectViewTitleSignal,
|
|
42
|
-
SdBusyContainer,
|
|
43
|
-
SdButton,
|
|
44
|
-
SdCommandDirective,
|
|
45
|
-
SdForm,
|
|
46
|
-
SdTextfield,
|
|
47
|
-
SdToastProvider,
|
|
48
|
-
SdTopbar,
|
|
49
|
-
SdTopbarContainer,
|
|
50
|
-
} from "@simplysm/angular";
|
|
51
|
-
// 앱별 대체: ORM provider + DbContext. @simplysm/angular가 아니라 각 앱이 소유한다.
|
|
52
|
-
import { AppOrmProvider } from "@adtek/client-common";
|
|
53
|
-
|
|
54
|
-
interface ICustomer {
|
|
55
|
-
id: number | undefined; // undefined면 신규 — 확장 A 편집/저장 시 활용
|
|
56
|
-
name: string;
|
|
57
|
-
phone: string;
|
|
58
|
-
lastModifiedAt: DateTime | undefined;
|
|
59
|
-
lastModifiedBy: string | undefined;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@Component({
|
|
63
|
-
selector: "app-customer-detail",
|
|
64
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
65
|
-
encapsulation: ViewEncapsulation.None,
|
|
66
|
-
standalone: true,
|
|
67
|
-
imports: [
|
|
68
|
-
SdBusyContainer, SdTopbarContainer, SdTopbar,
|
|
69
|
-
SdForm, SdButton, SdTextfield,
|
|
70
|
-
FormatPipe, NgIcon,
|
|
71
|
-
],
|
|
72
|
-
hostDirectives: [
|
|
73
|
-
{ directive: SdCommandDirective, outputs: ["sdRefreshCommand"] },
|
|
74
|
-
],
|
|
75
|
-
host: {
|
|
76
|
-
"(sdRefreshCommand)": "onRefreshButtonClick()",
|
|
77
|
-
},
|
|
78
|
-
template: `
|
|
79
|
-
<sd-busy-container [busy]="busyCount() > 0">
|
|
80
|
-
@if (initialized()) {
|
|
81
|
-
@if (!perms().includes("use")) {
|
|
82
|
-
<div class="fill tx-theme-gray-light p-xxl tx-center">
|
|
83
|
-
<br />
|
|
84
|
-
<ng-icon [svg]="tablerAlertTriangle" [size]="'5em'" />
|
|
85
|
-
<br />
|
|
86
|
-
<br />
|
|
87
|
-
'{{ viewTitle() }}'에 대한 사용권한이 없습니다. 시스템 관리자에게 문의하세요.
|
|
88
|
-
</div>
|
|
89
|
-
} @else {
|
|
90
|
-
<sd-topbar-container>
|
|
91
|
-
<sd-topbar>
|
|
92
|
-
<h4>{{ viewTitle() }}</h4>
|
|
93
|
-
|
|
94
|
-
<sd-button [theme]="'link-info'" (click)="onRefreshButtonClick()">
|
|
95
|
-
<ng-icon [svg]="tablerRefresh" />
|
|
96
|
-
새로고침
|
|
97
|
-
<small>(CTRL+ALT+L)</small>
|
|
98
|
-
</sd-button>
|
|
99
|
-
</sd-topbar>
|
|
100
|
-
|
|
101
|
-
<div class="flex-column fill">
|
|
102
|
-
<sd-form class="flex-fill">
|
|
103
|
-
<div class="p-default">
|
|
104
|
-
<table class="form-table">
|
|
105
|
-
<tbody>
|
|
106
|
-
<tr>
|
|
107
|
-
<th>명칭</th>
|
|
108
|
-
<td>
|
|
109
|
-
<sd-textfield
|
|
110
|
-
[type]="'text'"
|
|
111
|
-
[readonly]="true"
|
|
112
|
-
[(value)]="data().name"
|
|
113
|
-
/>
|
|
114
|
-
</td>
|
|
115
|
-
</tr>
|
|
116
|
-
<tr>
|
|
117
|
-
<th>전화번호</th>
|
|
118
|
-
<td>
|
|
119
|
-
<sd-textfield
|
|
120
|
-
[type]="'text'"
|
|
121
|
-
[readonly]="true"
|
|
122
|
-
[(value)]="data().phone"
|
|
123
|
-
/>
|
|
124
|
-
</td>
|
|
125
|
-
</tr>
|
|
126
|
-
</tbody>
|
|
127
|
-
</table>
|
|
128
|
-
</div>
|
|
129
|
-
</sd-form>
|
|
130
|
-
|
|
131
|
-
@if (data().lastModifiedAt || data().lastModifiedBy) {
|
|
132
|
-
<div class="p-sm-default">
|
|
133
|
-
최종수정:
|
|
134
|
-
@if (data().lastModifiedAt) {
|
|
135
|
-
{{ data().lastModifiedAt | format: "yyyy-MM-dd HH:mm" }}
|
|
136
|
-
}
|
|
137
|
-
@if (data().lastModifiedBy) {
|
|
138
|
-
({{ data().lastModifiedBy }})
|
|
139
|
-
}
|
|
140
|
-
</div>
|
|
141
|
-
}
|
|
142
|
-
</div>
|
|
143
|
-
</sd-topbar-container>
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
</sd-busy-container>
|
|
147
|
-
`,
|
|
148
|
-
})
|
|
149
|
-
export class CustomerDetail {
|
|
150
|
-
//== DI ==
|
|
151
|
-
private readonly _appOrm = inject(AppOrmProvider);
|
|
152
|
-
private readonly _sdToast = inject(SdToastProvider);
|
|
153
|
-
|
|
154
|
-
//== input ==
|
|
155
|
-
itemId = input<number>();
|
|
156
|
-
|
|
157
|
-
//== 타이틀 / 권한 ==
|
|
158
|
-
protected readonly viewTitle = injectViewTitleSignal();
|
|
159
|
-
perms = injectPermsSignal(["sales.customer"], ["use"]);
|
|
160
|
-
|
|
161
|
-
//== 상태 ==
|
|
162
|
-
protected readonly busyCount = signal(0);
|
|
163
|
-
protected readonly initialized = signal(false);
|
|
164
|
-
protected readonly data = signal<ICustomer>({
|
|
165
|
-
id: undefined,
|
|
166
|
-
name: "",
|
|
167
|
-
phone: "",
|
|
168
|
-
lastModifiedAt: undefined,
|
|
169
|
-
lastModifiedBy: undefined,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
//== 라이프사이클 ==
|
|
173
|
-
constructor() {
|
|
174
|
-
// 최초 진입 + itemId 변경 시 재조회.
|
|
175
|
-
effect(() => {
|
|
176
|
-
this.itemId(); // 의존성 등록
|
|
177
|
-
if (!this.perms().includes("use")) {
|
|
178
|
-
this.initialized.set(true);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
void untracked(async () => {
|
|
183
|
-
this.busyCount.update((v) => v + 1);
|
|
184
|
-
await this._sdToast.try(async () => {
|
|
185
|
-
await this._refresh();
|
|
186
|
-
});
|
|
187
|
-
this.busyCount.update((v) => v - 1);
|
|
188
|
-
this.initialized.set(true);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
//== 이벤트 핸들러 ==
|
|
194
|
-
protected async onRefreshButtonClick(): Promise<void> {
|
|
195
|
-
if (this.busyCount() > 0) return;
|
|
196
|
-
if (!this.perms().includes("use")) return;
|
|
197
|
-
|
|
198
|
-
this.busyCount.update((v) => v + 1);
|
|
199
|
-
await this._sdToast.try(async () => {
|
|
200
|
-
await this._refresh();
|
|
201
|
-
});
|
|
202
|
-
this.busyCount.update((v) => v - 1);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
//== 내부 메서드 ==
|
|
206
|
-
private async _refresh(): Promise<void> {
|
|
207
|
-
let data: ICustomer;
|
|
208
|
-
if (this.itemId() == null) {
|
|
209
|
-
data = {
|
|
210
|
-
id: undefined, name: "", phone: "",
|
|
211
|
-
lastModifiedAt: undefined, lastModifiedBy: undefined,
|
|
212
|
-
};
|
|
213
|
-
} else {
|
|
214
|
-
// 앱별 ORM 조회 — 예:
|
|
215
|
-
// data = await this._appOrm.connectAsync(async (db) =>
|
|
216
|
-
// (await db.customer.where((it) => [expr.eq(it.id, this.itemId())]).single())!
|
|
217
|
-
// );
|
|
218
|
-
throw new Error("구현 필요");
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.data.set(data);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
//== for Template ==
|
|
225
|
-
protected readonly tablerAlertTriangle = tablerAlertTriangle;
|
|
226
|
-
protected readonly tablerRefresh = tablerRefresh;
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### 조건부 요소 포함 기준
|
|
231
|
-
|
|
232
|
-
인프라·라이프사이클 요소는 화면 요건에 따라 포함·생략한다. 필요 없는 요소를 기계적으로 포함하지 않는다.
|
|
233
|
-
|
|
234
|
-
| 요소 | 포함 조건 | 생략 예시 |
|
|
235
|
-
|------|----------|----------|
|
|
236
|
-
| `<sd-topbar-container>` + `<sd-topbar>` | routes로 연결된 page에서 헤더가 필요할 때. 기본 레시피는 page 전용이라 조건 없이 렌더 | route 미연결(control·래퍼) |
|
|
237
|
-
| `injectViewTitleSignal()` | topbar에 타이틀을 표시할 때 | topbar 없음·타이틀 불필요 |
|
|
238
|
-
| `injectViewTypeSignal()` + 분기 | page 외에 modal/control로도 겸용될 때. 기본 레시피는 page 전용이므로 **미포함** — [확장 C](./crud-detail/extension-c-modal-view.md) / [확장 D](./crud-detail/extension-d-control-view.md)에서 도입 | page 전용 |
|
|
239
|
-
| `injectPermsSignal()` + 권한 없음 블록 | 권한 제어가 있는 화면 | 권한 제어 없음 |
|
|
240
|
-
| `<sd-busy-container>` + `busyCount` | 비동기 작업(DB 조회 등)이 있어 busy 표시가 필요할 때 | 동기 래퍼·레이아웃 |
|
|
241
|
-
| `initialized` + `@if (initialized())` | 초기 로딩 완료 전 깜박임 방지가 필요할 때 | 빈 상태 렌더가 무방 |
|
|
242
|
-
|
|
243
|
-
## 변형 (확장 A~F 인덱스)
|
|
244
|
-
|
|
245
|
-
| 상황 | 시작 지점 + 필요한 확장 |
|
|
246
|
-
|---|---|
|
|
247
|
-
| 단일 레코드 **읽기 전용 상세 폼** (page 뷰, 감사 필드 표시) | 기본 레시피만 |
|
|
248
|
-
| 편집·저장 가능한 단일 레코드 상세 폼 | 기본 + [확장 A](./crud-detail/extension-a-edit-save.md) |
|
|
249
|
-
| 삭제·복구 토글 포함 | 기본 + A + [확장 B](./crud-detail/extension-b-delete-restore.md) |
|
|
250
|
-
| page + **modal 뷰** 재사용 (확인·취소·삭제·복구 하단 바) | 기본 + A + B + [확장 C](./crud-detail/extension-c-modal-view.md) |
|
|
251
|
-
| 마스터-디테일의 "디테일" 영역 (**control 뷰**) | 기본 + A + B + [확장 D](./crud-detail/extension-d-control-view.md) |
|
|
252
|
-
| page + modal + control **3뷰** 모두 지원 | 기본 + A + B + C + D |
|
|
253
|
-
| 메인 폼과 별개의 **보조 기능** (가져오기/출력 등) | 기본 + A + [확장 E](./crud-detail/extension-e-auxiliary.md) |
|
|
254
|
-
| 상세 폼 내부에 **하위 컬렉션** 편집 (박스 목록 등) | 기본 + A + [확장 F](./crud-detail/extension-f-complex-detail.md) |
|
|
255
|
-
|
|
256
|
-
### 각 확장 요약
|
|
257
|
-
|
|
258
|
-
- **A. 편집/저장** (선행: 없음) — 읽기 전용을 편집 가능으로 전환. snapshot 기반 변경 감지, `setupCanDeactivate` 이탈 방지, Ctrl+S 일괄 저장. → [상세](./crud-detail/extension-a-edit-save.md)
|
|
259
|
-
- **B. 삭제/복구 토글** (선행: A) — soft-delete 전제. `isDeleted` 필드 + 삭제·복구 버튼. → [상세](./crud-detail/extension-b-delete-restore.md)
|
|
260
|
-
- **C. modal 뷰** (선행: A + B) — `SdModalContentDef` 계약, 하단 액션 바, 모달 우측 상단 액션 슬롯. → [상세](./crud-detail/extension-c-modal-view.md)
|
|
261
|
-
- **D. control 뷰** (선행: A + B) — 마스터-디테일 디테일 영역. 상단 `<sd-dock>` 도구 바. 확장 C와 병행 가능. → [상세](./crud-detail/extension-d-control-view.md)
|
|
262
|
-
- **E. 보조 기능 영역** (선행: A) — 메인 submit과 분리된 보조 `<sd-form>`. 권한 복사·출력 등. → [상세](./crud-detail/extension-e-auxiliary.md)
|
|
263
|
-
- **F. 복합 상세** (선행: A) — 내부 `<sd-sheet>` 중첩, `oneWayDiffs` 기반 일괄 저장. → [상세](./crud-detail/extension-f-complex-detail.md)
|
|
264
|
-
|
|
265
|
-
## 🚫 흔한 실수 (Anti-patterns)
|
|
266
|
-
|
|
267
|
-
> 공통 규칙(`mark` 오용, `injectViewTypeSignal()` 호출 위치, `_sdSharedData.wait()` 조건, 시트 셀 `[inset]`/`[size]`, soft-delete 선택 기준 등)은 [레시피 공통 규칙](./_common-rules.md)을 참조한다. 이 섹션은 **CRUD 상세폼 진입점 고유 실수**만 다룬다.
|
|
268
|
-
|
|
269
|
-
### 지원할 뷰를 확인하지 않고 3뷰 분기를 미리 박는다
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
// ❌ 실제로는 page 뷰만 쓰는데 modal·control 분기를 선제 조립
|
|
273
|
-
// close output, viewType, <sd-dock [position]="'bottom'">, <ng-template #modalActionTpl>가
|
|
274
|
-
// 모두 죽은 코드로 남고, implements SdModalContentDef 경로는 호출되지 않는다.
|
|
275
|
-
@Component({ /* ... */ })
|
|
276
|
-
export class CustomerDetail implements SdModalContentDef<boolean | undefined> {
|
|
277
|
-
close = output<boolean | undefined>();
|
|
278
|
-
protected readonly viewType = injectViewTypeSignal();
|
|
279
|
-
// ...
|
|
280
|
-
template: `
|
|
281
|
-
<sd-topbar-container>
|
|
282
|
-
@if (viewType() === "page") { <sd-topbar>...</sd-topbar> }
|
|
283
|
-
<sd-dock-container>
|
|
284
|
-
@if (viewType() === "control" && canEdit()) { <sd-dock>...</sd-dock> }
|
|
285
|
-
@if (viewType() === "modal" && canEdit()) {
|
|
286
|
-
<sd-dock [position]="'bottom'">...</sd-dock>
|
|
287
|
-
}
|
|
288
|
-
</sd-dock-container>
|
|
289
|
-
</sd-topbar-container>
|
|
290
|
-
<ng-template #modalActionTpl>...</ng-template>
|
|
291
|
-
`
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ✅ 기본 레시피는 page 전용으로 시작. 필요한 뷰가 생기면 확장을 얹는다
|
|
295
|
-
// modal 뷰가 필요해지는 시점에 확장 C를, control 뷰는 확장 D를 추가.
|
|
296
|
-
@Component({ /* ... */ })
|
|
297
|
-
export class CustomerDetail {
|
|
298
|
-
itemId = input<number>();
|
|
299
|
-
// page 전용 — viewType 분기·close output 없음
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
**근거**: 실제 호출되지 않는 뷰의 계약·분기·슬롯은 죽은 코드로 남아 LLM이 모달 사용처 탐색 시 오판 원인이 된다. 지원할 뷰를 먼저 확정하고 필요한 확장만 얹는다.
|
|
304
|
-
|
|
305
|
-
### `effect` 내부 비동기 호출을 `untracked`로 감싸지 않는다
|
|
306
|
-
|
|
307
|
-
```typescript
|
|
308
|
-
// ❌ _refresh() 내부에서 읽는 signal이 effect 의존성으로 등록 → 무한 루프
|
|
309
|
-
constructor() {
|
|
310
|
-
effect(() => {
|
|
311
|
-
this.itemId();
|
|
312
|
-
void this._refresh(); // 내부의 this.data()·this.perms() 읽기가 그대로 의존성화
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ✅ 비동기 작업은 untracked로 감싸 의존성 등록을 차단한다
|
|
317
|
-
constructor() {
|
|
318
|
-
effect(() => {
|
|
319
|
-
this.itemId(); // 여기서만 의존성 등록
|
|
320
|
-
void untracked(async () => {
|
|
321
|
-
await this._sdToast.try(() => this._refresh());
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
**근거**: Angular `effect`는 콜백 실행 중 읽힌 모든 signal을 의존성으로 등록한다. 비동기 함수 내부에서 signal을 읽으면 effect가 재실행되어 무한 루프가 발생한다.
|
|
328
|
-
|
|
329
|
-
### `setupCanDeactivate`를 `computed`·`effect`·일반 메서드에서 호출한다
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
// ❌ NG0203 — inject()는 injection context에서만 호출 가능
|
|
333
|
-
protected readonly canEdit = computed(() => {
|
|
334
|
-
setupCanDeactivate(() => !this._dirty()); // 런타임 에러
|
|
335
|
-
return true;
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// ✅ 생성자(또는 필드 이니셜라이저)에서만 호출
|
|
339
|
-
constructor() {
|
|
340
|
-
setupCanDeactivate(() => !this._dirty());
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
**근거**: `setupCanDeactivate`는 내부에서 `inject(Router)` 등을 사용하므로 injection context가 필수다. computed/effect 콜백이나 일반 메서드 호출 시점엔 context가 없다.
|
|
345
|
-
|
|
346
|
-
## 관련 Entry
|
|
347
|
-
|
|
348
|
-
- [`crud-list.md`](./crud-list.md) — 시트 기반 CRUD 리스트. 차이: 단일 레코드 vs 컬렉션
|
|
349
|
-
- [`page-modal-container.md`](./page-modal-container.md) — page/modal 뷰 분기만 필요한 단순 컨테이너. 차이: CRUD 라이프사이클 없음
|
|
350
|
-
- [`data-select-button.md`](./data-select-button.md) — 데이터 선택 버튼. 차이: 모달 내부 선택 UI
|
|
351
|
-
- [`_common-rules.md`](./_common-rules.md) — 레시피 공통 규칙 (✅ Always / ⚠️ Ask first / 🚫 Never)
|
|
352
|
-
- [`SdModalContentDef`](../provider-types/sd-modal-content-def.md) — 확장 C(modal 뷰)에서 구현하는 계약
|
|
353
|
-
- [`SdModalProvider.showAsync`](../providers/sd-modal-provider.md) — 프로그래밍 방식 모달 호출
|