@simplysm/sd-claude 14.0.94 → 14.0.95
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/claude/references/sd-simplysm14/README.md +6 -2
- package/claude/references/sd-simplysm14/apis/angular/README.md +180 -43
- package/claude/references/sd-simplysm14/apis/angular/controls.md +275 -125
- package/claude/references/sd-simplysm14/apis/angular/crud.md +54 -59
- package/claude/references/sd-simplysm14/apis/angular/directives.md +139 -48
- package/claude/references/sd-simplysm14/apis/angular/features.md +102 -88
- package/claude/references/sd-simplysm14/apis/angular/kanban.md +54 -0
- package/claude/references/sd-simplysm14/apis/angular/layout.md +60 -36
- package/claude/references/sd-simplysm14/apis/angular/overlay.md +127 -75
- package/claude/references/sd-simplysm14/apis/angular/routing-appstructure.md +97 -51
- package/claude/references/sd-simplysm14/apis/angular/shared-data.md +74 -58
- package/claude/references/sd-simplysm14/apis/angular/sheet.md +81 -60
- package/claude/references/sd-simplysm14/apis/excel/README.md +5 -5
- package/claude/references/sd-simplysm14/apis/excel/cell.md +3 -3
- package/claude/references/sd-simplysm14/apis/excel/style.md +2 -2
- package/claude/references/sd-simplysm14/apis/excel/workbook-worksheet.md +5 -4
- package/claude/references/sd-simplysm14/apis/excel/wrapper.md +2 -2
- package/claude/references/sd-simplysm14/manuals/client-app-structure.md +4 -2
- package/claude/references/sd-simplysm14/manuals/client-component.md +23 -23
- package/claude/references/sd-simplysm14/manuals/client-crud.md +151 -4
- package/claude/references/sd-simplysm14/manuals/client-demo.md +5 -18
- package/claude/references/sd-simplysm14/manuals/client-shared-data.md +5 -2
- package/claude/references/sd-simplysm14/manuals/data-log.md +1 -1
- package/claude/sd-system-prompt.md +7 -0
- package/claude/skills/sd-debug/SKILL.md +142 -27
- package/claude/skills/sd-review/SKILL.md +158 -20
- package/claude/skills/sd-spec/SKILL.md +1 -0
- package/package.json +1 -1
- package/claude/references/sd-simplysm14/apis/angular/infra.md +0 -82
- package/claude/skills/sd-debug/workflow.js +0 -390
- package/claude/skills/sd-review/workflow.js +0 -324
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
|
|
51
51
|
`<sd-sheet-column>` 은 `<sd-crud-list>` 의 직속 자식으로 두면 내부 시트로 자동 투영됨.
|
|
52
52
|
|
|
53
|
+
### 와이어프레임이 표준 버튼 위치와 충돌하면
|
|
54
|
+
|
|
55
|
+
`(create)/(delete)/(restore)` 표준 출력이 와이어프레임에 명시된 버튼 위치를 가린다면, 표준 출력 사용을 포기하고 `#toolTpl` 등 슬롯 안에 `sd-button` 으로 직접 배치. 시각 요소 배치는 와이어프레임이 1순위 ([client-component.md "시각 요소 배치 기준"](./client-component.md)).
|
|
56
|
+
|
|
53
57
|
### viewType 별 동작
|
|
54
58
|
|
|
55
59
|
- **`'page'`** — 라우팅 진입 단위. 상단에 저장 버튼.
|
|
@@ -110,7 +114,7 @@ async onEdit(item: IItem, event: Event): Promise<void> {
|
|
|
110
114
|
```html
|
|
111
115
|
<ng-template #toolTpl>
|
|
112
116
|
<sd-button [size]="'sm'" [theme]="'link-success'" (click)="onDownloadExcelButtonClick()">
|
|
113
|
-
<ng-icon [svg]="
|
|
117
|
+
<ng-icon [svg]="tablerUpload" />
|
|
114
118
|
엑셀 다운로드
|
|
115
119
|
</sd-button>
|
|
116
120
|
</ng-template>
|
|
@@ -149,9 +153,106 @@ async onDownloadExcelButtonClick(): Promise<void> {
|
|
|
149
153
|
- 조회는 목록과 같은 `_search` 를 페이징 인자만 꺼서 재사용 — 보이는 페이지가 아니라 결과 전체를 받음.
|
|
150
154
|
- 파일명은 `<화면제목>_<yyMMdd>.xlsx`. 화면 제목은 `injectViewTitleSignal()`.
|
|
151
155
|
- 양식 컬럼 = 화면 표시 컬럼 + `삭제`(참/거짓) + `수정일시`·`수정자`([data-log.md](./data-log.md) 의 표시 규약). 참조 마스터는 명칭으로 출력.
|
|
152
|
-
-
|
|
156
|
+
- 같은 `_excelWrapper`(zod 스키마) 를 아래 업로드 레시피와 **공유**함 — `write` 가 다운로드, `read` 가 업로드.
|
|
153
157
|
- `ExcelWrapper`/`downloadBlob` 자체 사용법은 [apis/excel/README.md](../apis/excel/README.md) · [apis/core-browser/README.md](../apis/core-browser/README.md).
|
|
154
158
|
|
|
159
|
+
### 엑셀 업로드로 일괄 등록·수정하려면
|
|
160
|
+
|
|
161
|
+
다운로드와 **같은 `_excelWrapper`(zod 스키마) 를 공유**해 역방향으로 읽음. `#toolTpl` 의 다운로드 버튼 옆에 업로드 버튼을 `edit` 권한으로 게이팅해 두고, `openFileDialog` → `_excelWrapper.read` → 정합성 검증 → `save` 일괄 저장. 아래 예시는 참조 마스터(예: 역할) 컬럼을 포함한 목록 기준.
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<ng-template #toolTpl>
|
|
165
|
+
@if (perms().includes("edit")) {
|
|
166
|
+
<sd-button [size]="'sm'" [theme]="'link-success'" (click)="onUploadExcelButtonClick()">
|
|
167
|
+
<ng-icon [svg]="tablerUpload" />
|
|
168
|
+
엑셀 업로드
|
|
169
|
+
</sd-button>
|
|
170
|
+
}
|
|
171
|
+
<!-- 위 '엑셀 다운로드' 버튼과 같은 #toolTpl 안에 둠 -->
|
|
172
|
+
</ng-template>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
async onUploadExcelButtonClick(): Promise<void> {
|
|
177
|
+
if (this.busyCount() > 0) return;
|
|
178
|
+
if (!this.perms().includes("edit")) return;
|
|
179
|
+
|
|
180
|
+
const files = await openFileDialog({ accept: ".xlsx" });
|
|
181
|
+
if (files == null) return; // 취소
|
|
182
|
+
|
|
183
|
+
this.busyCount.update((v) => v + 1);
|
|
184
|
+
await this._sdToast.try(async () => {
|
|
185
|
+
// 파생 컬럼(수정일시·수정자)은 업로드에서 제외하고 파싱.
|
|
186
|
+
const records = await this._excelWrapper.read(files[0], 0, {
|
|
187
|
+
excludes: ["lastModifiedAt", "lastModifiedBy"],
|
|
188
|
+
});
|
|
189
|
+
if (records.length === 0) throw new Error("업로드할 데이터가 없습니다.");
|
|
190
|
+
|
|
191
|
+
// 다건일 때만 에러 메시지에 항목명을 붙임(단건이면 평문).
|
|
192
|
+
const label = (itemName: string): string =>
|
|
193
|
+
records.length > 1 ? `직원 '${itemName}': ` : "";
|
|
194
|
+
|
|
195
|
+
// 참조 마스터: 명칭 → ID 역변환(활성 기준). 매칭 안 되는 명칭은 throw.
|
|
196
|
+
const roleIdByName = new Map(this.sharedRoles.items().map((r) => [r.name, r.id] as const));
|
|
197
|
+
const inputs = records.map((rec) => {
|
|
198
|
+
let roleId: number | undefined;
|
|
199
|
+
const roleName = rec.roleName?.trim();
|
|
200
|
+
if (roleName != null && roleName !== "") {
|
|
201
|
+
roleId = roleIdByName.get(roleName);
|
|
202
|
+
if (roleId == null) throw new Error(`${label(rec.name)}존재하지 않는 역할입니다. (${roleName})`);
|
|
203
|
+
}
|
|
204
|
+
return { id: rec.id, name: rec.name, roleId, isDeleted: rec.isDeleted };
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ID↔이름 정합성 — id 있는 행은 DB의 (id, name) 과 대조. id 없으면 신규.
|
|
208
|
+
const ids = inputs.flatMap((x) => (x.id != null ? [x.id] : []));
|
|
209
|
+
const dbRows =
|
|
210
|
+
ids.length === 0
|
|
211
|
+
? []
|
|
212
|
+
: await this._appOrm.connectWithoutTransAsync((db) =>
|
|
213
|
+
db
|
|
214
|
+
.employee()
|
|
215
|
+
.where((c) => [expr.in(c.id, ids)])
|
|
216
|
+
.select((c) => ({ id: c.id, name: c.name }))
|
|
217
|
+
.execute(),
|
|
218
|
+
);
|
|
219
|
+
const dbNameById = new Map(dbRows.map((r) => [r.id, r.name] as const));
|
|
220
|
+
|
|
221
|
+
// 기존 이름(비즈니스키)이 바뀌면 엑셀 행 어긋남(정렬 사고) 의심 → 변경 건수를 입력받아 확인.
|
|
222
|
+
let nameChangedCount = 0;
|
|
223
|
+
for (const x of inputs) {
|
|
224
|
+
if (x.id == null) continue;
|
|
225
|
+
const dbName = dbNameById.get(x.id);
|
|
226
|
+
if (dbName == null) throw new Error(`${label(x.name)}ID ${x.id} 에 해당하는 직원이 없습니다.`);
|
|
227
|
+
if (dbName !== x.name.trim()) nameChangedCount++;
|
|
228
|
+
}
|
|
229
|
+
if (nameChangedCount >= 1) {
|
|
230
|
+
const answer = prompt(
|
|
231
|
+
`기존 항목 ${nameChangedCount}건의 이름이 변경됩니다.\n계속하려면 ${nameChangedCount} 을(를) 입력하세요.`,
|
|
232
|
+
);
|
|
233
|
+
if (answer == null || answer.trim() !== String(nameChangedCount)) return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const results = await this._appService.employee.save(inputs); // 일괄 저장(트랜잭션)
|
|
237
|
+
await this._appSharedData.emitAsync(
|
|
238
|
+
"직원",
|
|
239
|
+
results.map((r) => r.id),
|
|
240
|
+
);
|
|
241
|
+
this._sdToast.success(`${results.length}건이 반영되었습니다.`);
|
|
242
|
+
await this._refresh();
|
|
243
|
+
});
|
|
244
|
+
this.busyCount.update((v) => v - 1);
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- 다운로드와 같은 `_excelWrapper` 를 그대로 재사용 — `read(file, sheetIndex, { excludes })` 가 역방향. `excludes` 로 파생 컬럼(수정일시·수정자)을 파싱에서 뺌.
|
|
249
|
+
- 빈 파일(0건)은 throw — 정상 처리하지 않음.
|
|
250
|
+
- 참조 마스터(역할 등)는 **명칭 → ID 역변환**. 매칭 안 되는 명칭은 throw — 일부만 건너뛰지 않음(다중 작업 원자성).
|
|
251
|
+
- `id` 유무로 신규/수정 분기. `id` 있는 행은 DB의 `(id, name)` 과 대조해 존재·정합성 확인(없으면 throw).
|
|
252
|
+
- 기존 이름(비즈니스키) 변경은 엑셀 행이 밀린 사고일 수 있어, 변경 건수를 직접 입력받아 확인 후 진행.
|
|
253
|
+
- 저장은 `save(inputs)` 한 번으로 일괄 — 한 건이라도 실패하면 전체 롤백(트랜잭션 원자성).
|
|
254
|
+
- 업로드 버튼은 `edit` 권한일 때만 노출. import 추가: `openFileDialog`(`@simplysm/core-browser`) · `tablerUpload`(`@ng-icons/tabler-icons`).
|
|
255
|
+
|
|
155
256
|
### 특정 행의 선택·삭제를 막으려면
|
|
156
257
|
|
|
157
258
|
`[getItemSelectableFn]` 로 행별 선택 가능 여부를 반환. 문자열을 반환하면 그 사유가 안내되고 해당 행은 선택(→삭제) 불가. 개별 선택·전체 선택 모든 경로에 적용됨.
|
|
@@ -167,6 +268,26 @@ getItemSelectableFn = (item: IItem): boolean | string =>
|
|
|
167
268
|
- `true` = 선택 가능, `string` = 선택 불가 + 사유. 선택 자체가 막히므로 핸들러에 같은 가드를 또 두지 않아도 됨.
|
|
168
269
|
- 단건 상세에는 선택 개념이 없으므로, 같은 제약을 삭제 버튼을 조건부로 숨겨 적용(`@if (!isSelf() && perms().includes("edit")) { ...삭제 버튼... }`).
|
|
169
270
|
|
|
271
|
+
### 시트 정렬을 서버 정렬로 반영하려면
|
|
272
|
+
|
|
273
|
+
`[(sorts)]="sortingDefs"` 로 받은 정렬 조건을 `_search` 쿼리에 반영. 시트 컬럼 `key` 가 select 별칭과 일치하므로, 컬럼별 분기 없이 `obj.getChainValue` 로 `key` 를 컬럼으로 풀어 `orderBy` 에 전달.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// 화면 사용자가 지정한 정렬을 우선순위대로 적용
|
|
277
|
+
for (const sort of this.sortingDefs()) {
|
|
278
|
+
qr2 = qr2.orderBy((c) => obj.getChainValue(c, sort.key) as any, sort.desc ? "DESC" : "ASC");
|
|
279
|
+
}
|
|
280
|
+
// 이 화면의 기본 정렬 — 여기를 고쳐 화면별 기본값을 바꿈
|
|
281
|
+
if (!this.sortingDefs().some((s) => s.key === "id")) {
|
|
282
|
+
qr2 = qr2.orderBy((c) => c.id, "DESC");
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- 아래 `if` 블록이 그 화면의 기본 정렬을 정하는 자리. 예시는 `id DESC` — 다른 기본 정렬이 필요하면 `orderBy` 의 컬럼·방향과 `s.key` 를 같은 키로 함께 바꿈.
|
|
287
|
+
- 화면 사용자가 시트 헤더로 정렬하면 그 정렬이 1순위로 적용되고, 기본 정렬은 맨 뒤에 깔림.
|
|
288
|
+
- 컬럼마다 `if (sort.key === "X") orderBy((c) => c.X, ...)` 식 분기 금지 — `sort.key` 가 select 별칭과 일치하므로 한 줄로 처리.
|
|
289
|
+
- `obj` 는 `@simplysm/core-common`, `SortingDef` 는 `@simplysm/angular`.
|
|
290
|
+
|
|
170
291
|
## `sd-crud-detail`
|
|
171
292
|
|
|
172
293
|
단일 레코드 편집 화면의 표준 골격. 다음 기능을 일괄 제공: 폼 래핑, CTRL+S 단축키 저장, 저장 버튼, 모달의 "확인" 버튼 자동 처리.
|
|
@@ -209,7 +330,9 @@ getItemSelectableFn = (item: IItem): boolean | string =>
|
|
|
209
330
|
|
|
210
331
|
### 삭제 (onDelete)
|
|
211
332
|
|
|
212
|
-
`confirm` → soft delete(`isDeleted=true`) → 이력 적재 → 공유 데이터 통지 →
|
|
333
|
+
`confirm` → soft delete(`isDeleted=true`) → 이력 적재 → 공유 데이터 통지 → 목록(list)은 `_refresh()` / 단건(detail)은 `submitted.emit(true)`.
|
|
334
|
+
|
|
335
|
+
**벌크 삭제 (list)**:
|
|
213
336
|
|
|
214
337
|
```ts
|
|
215
338
|
async onDelete(targets: IItem[]): Promise<void> {
|
|
@@ -236,6 +359,30 @@ async onDelete(targets: IItem[]): Promise<void> {
|
|
|
236
359
|
}
|
|
237
360
|
```
|
|
238
361
|
|
|
362
|
+
**단건 삭제 (detail)**: `sd-crud-detail` 표준 호출에는 `(delete)` output 이 없으므로, 삭제 버튼을 `#commandTpl` 슬롯에 두고 `(click)="onDelete()"` 로 배선. 목록의 `_refresh()` 대신 `submitted.emit(true)` 로 부모(list) 에 통지.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
async onDelete(): Promise<void> {
|
|
366
|
+
if (this.busyCount() > 0) return;
|
|
367
|
+
if (!this.canEdit()) return;
|
|
368
|
+
if (!confirm("삭제하시겠습니까?")) return;
|
|
369
|
+
|
|
370
|
+
const id = this.dataId();
|
|
371
|
+
const employeeId = this._appAuth.authInfo()?.employeeId;
|
|
372
|
+
this.busyCount.update((v) => v + 1);
|
|
373
|
+
await this._sdToast.try(async () => {
|
|
374
|
+
await this._appOrm.connectAsync(async (db) => {
|
|
375
|
+
await db.role().where((c) => [expr.eq(c.id, id)]).update(() => ({ isDeleted: true }));
|
|
376
|
+
await db.role().insertDataLog({ action: "삭제", itemId: id, employeeId });
|
|
377
|
+
});
|
|
378
|
+
await this._appSharedData.emitAsync("역할", [id]);
|
|
379
|
+
this._sdToast.success("삭제되었습니다.");
|
|
380
|
+
this.submitted.emit(true);
|
|
381
|
+
});
|
|
382
|
+
this.busyCount.update((v) => v - 1);
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
239
386
|
### 복구 (onRestore)
|
|
240
387
|
|
|
241
388
|
복구는 `confirm` 없이 진행하되, **활성 유니크 컬럼(명칭·코드)이 있으면 복구로 활성 중복이 생기지 않는지 재검증**해야 함(삭제된 동안 같은 값의 활성 레코드가 생겼을 수 있음). 검증 위치가 단건/벌크에서 다름. 활성 유니크 정책 자체는 [orm.md](./orm.md) 의 유니크 전략.
|
|
@@ -288,4 +435,4 @@ for (const id of ids) {
|
|
|
288
435
|
- 삭제·복구·이력 적재는 한 `connectAsync` 트랜잭션 안에서 수행 — 데이터만 바뀌고 이력이 빠지거나 그 반대가 되지 않게 함.
|
|
289
436
|
- 벌크 복구는 하나라도 충돌하면 전체 롤백(원자성). 충돌분만 빼고 나머지를 복구하지 않음.
|
|
290
437
|
- 활성 유니크 검증은 복구 경로에서 빠뜨리지 않음 — 단건은 선검증, 벌크는 후검증. 활성 유니크가 없는 모델이면 생략 가능.
|
|
291
|
-
-
|
|
438
|
+
- 단건(detail)은 삭제 후 [client-component.md "detail 데이터 흐름"](./client-component.md) 의 계약대로 `submitted.emit(true)` 로 부모에 통지(modal 컨텍스트에선 모달 호스트가 그 위에 `close` 로 닫음 — `emit` 의 대체 아님). 복구 후엔 닫지 않고 refresh — 복구 직후 상세를 계속 보도록.
|
|
@@ -4,24 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## 화면 정의 섹션(4번 섹션) 의 화면 유형별 파일 역할
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
9
|
-
| 마스터(체크박스·`[E N]`·5버튼바) / 시트 단일 | `<domain>.list.ts` |
|
|
10
|
-
| 단건 입력 폼 | `<domain>.detail.ts` |
|
|
11
|
-
| 좌 목록 + 우 단건 | `<domain>.view.ts` + `.list.ts` + `.detail.ts` |
|
|
12
|
-
| 좌 헤더 목록 + 우(헤더 정보 + 라인 시트) 마스터-라인 | `<domain>.view.ts` + `.list.ts` + `.detail.ts` — 우 라인 영역은 `.detail.ts` (헤더 단건 + 라인) |
|
|
13
|
-
| 모달 전용 비-CRUD 화면 (도구·검색·설정 등) | `<domain>.modal.ts` |
|
|
14
|
-
| 프린트 양식 | `<domain>.print-template.ts` |
|
|
15
|
-
|
|
16
|
-
`<domain>` 은 화면명을 dash-case 영문으로 음역한 슬러그. 같은 도메인 폴더에 같은 역할의 파일이 2개 이상이면 `<domain>-<갈래>.<역할>.ts` 형식 사용.
|
|
17
|
-
|
|
18
|
-
동작 섹션의 `→ [화면.X] 을 모달로 띄움` 표기는 표시 방식일 뿐 파일 역할이 아님. 화면.X 가 단건 편집이면 `.detail.ts` 를 `showAsync` 로 띄우고(= 위 "단건 입력 폼" 행), 모달 전용 비-CRUD UI 일 때만 `.modal.ts`. 판별 기준은 [client-component.md "detail 과 modal 구분"](./client-component.md) 참조.
|
|
7
|
+
화면 유형 → 파일 구성 매핑(`<domain>` 슬러그·모달 표기 해석 포함) 은 [client-component.md "파일명·역할·위치"](./client-component.md) 의 "화면 정의 → 파일 구성" 을 따름. 데모도 같은 매핑으로 파일을 구성.
|
|
19
8
|
|
|
20
9
|
## 와이어프레임 기준
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`sd-crud-list` 의 표준 출력(`(create)/(delete)/(restore)`) 이 와이어프레임에 명시된 버튼 위치를 가린다면 표준 출력 사용을 포기하고 슬롯 안에 `sd-button` 으로 직접 배치.
|
|
11
|
+
시각 요소(버튼·필터·시트·탭·검색) 의 존재·영역·순서는 와이어프레임이 1순위 ([client-component.md "시각 요소 배치 기준"](./client-component.md)). `sd-crud-list` 표준 출력과 충돌할 때의 처리는 [client-crud.md "와이어프레임이 표준 버튼 위치와 충돌하면"](./client-crud.md). 데모도 와이어프레임을 따라 구성.
|
|
25
12
|
|
|
26
13
|
## 항목표의 `종류` 컬럼을 입력 컨트롤로 매핑
|
|
27
14
|
|
|
@@ -73,7 +60,7 @@ if (!result) return;
|
|
|
73
60
|
|
|
74
61
|
영역 한정 호출(`→ [화면.Y] 의 <영역> — 선택 전용` 등) 은 모달의 입력 시그널(`selectMode` 등) 로 전달. spec 마커 매핑: "선택 전용"·multiselect 는 `selectMode`(`single`/`multi`) 로, "편집 가능 여부" 는 `readonly` 로 따로 전달. "선택 전용" 은 선택 목적을 뜻할 뿐 편집을 막지 않으므로(readonly 아님), 편집까지 차단하려면 `readonly=true` 를 함께 줄 것.
|
|
75
62
|
|
|
76
|
-
단건 편집을 모달로 띄우는 경우 피호출 화면은 `.detail.ts`(`<sd-crud-detail>` 루트, `viewType='modal'` 자동 주입)이며 모달 표시용 별도 `.modal.ts` 를 만들지 않음. 모달 전용 비-CRUD 화면(`.modal.ts`)은 `sd-crud-detail` 대신 `sd-busy-container` 등으로 자체 구성.
|
|
63
|
+
단건 편집을 모달로 띄우는 경우 피호출 화면은 `.detail.ts`(`<sd-crud-detail>` 루트, `viewType='modal'` 자동 주입)이며 모달 표시용 별도 `.modal.ts` 를 만들지 않음. 모달 전용 비-CRUD 화면(`.modal.ts`)은 `sd-crud-detail` 대신 `sd-busy-container` 등으로 자체 구성.
|
|
77
64
|
|
|
78
65
|
**동반 모달**: 동작 섹션에 `→ [화면.Y] 을 모달로 띄움` 으로 등장하는 모든 모달은 같은 호출에서 함께 생성. 이미 존재하면 재사용.
|
|
79
66
|
|
|
@@ -84,7 +71,7 @@ if (!result) return;
|
|
|
84
71
|
```html
|
|
85
72
|
<ng-template #toolTpl>
|
|
86
73
|
<sd-button [size]="'sm'" [theme]="'link-success'" (click)="onExcelUploadClick()">
|
|
87
|
-
<ng-icon [svg]="
|
|
74
|
+
<ng-icon [svg]="tablerUpload" /> 엑셀 업로드
|
|
88
75
|
</sd-button>
|
|
89
76
|
<sd-button [size]="'sm'" [theme]="'link-success'" (click)="onExcelDownloadClick()">
|
|
90
77
|
<ng-icon [svg]="tablerDownload" /> 엑셀 다운로드
|
|
@@ -115,4 +102,4 @@ list 와 detail 의 합성 view 에서 항목 미선택 빈 상태는 [client-co
|
|
|
115
102
|
|
|
116
103
|
## 라우팅·메뉴 따라가기
|
|
117
104
|
|
|
118
|
-
|
|
105
|
+
라우팅·메뉴 등록 관례(기존 화면 본뜨기·`title`=화면명·`code`=음역 슬러그) 는 [client-app-structure.md "2. 메뉴 추가"](./client-app-structure.md) 를 따름. 데모도 같은 방식으로 등록.
|
|
@@ -184,7 +184,10 @@ sharedProducts = useSharedSignal("품목");
|
|
|
184
184
|
|
|
185
185
|
@let _selectedRole = selectedRole();
|
|
186
186
|
@if (_selectedRole == null) {
|
|
187
|
-
<div class="flex-fill p-xxl"
|
|
187
|
+
<div class="flex-fill tx-theme-gray-default p-xxl" style="font-size: 48px; line-height: 1.5em">
|
|
188
|
+
<ng-icon [svg]="tablerArrowLeft" />
|
|
189
|
+
역할을 선택하세요.
|
|
190
|
+
</div>
|
|
188
191
|
} @else {
|
|
189
192
|
<app-role-permission-detail class="flex-fill" [roleId]="_selectedRole.id" />
|
|
190
193
|
}
|
|
@@ -209,7 +212,7 @@ constructor() {
|
|
|
209
212
|
- 두 이탈 경로를 모두 막음:
|
|
210
213
|
- `[canChangeFn]="checkCanLeave"` — 좌측에서 **다른 항목으로 전환**하기 전 확인.
|
|
211
214
|
- `setupCanDeactivate(checkCanLeave)` — **페이지(라우팅) 이탈** 전 확인.
|
|
212
|
-
- 선택 전(`selectedItem == null`)에는 안내
|
|
215
|
+
- 선택 전(`selectedItem == null`)에는 미선택 빈 상태를 둠 — 아이콘 + 안내 문구 구조와 `NgIcon` 등록은 [client-component.md](./client-component.md) 의 'list + detail 합성' 빈 상태 규약을 따름.
|
|
213
216
|
|
|
214
217
|
## 지킬 것
|
|
215
218
|
|
|
@@ -133,7 +133,7 @@ Queryable.prototype.joinLastDataLog = function (this: Queryable<any, any>, opts)
|
|
|
133
133
|
|
|
134
134
|
### 3. db-context 등록
|
|
135
135
|
|
|
136
|
-
확장 메서드는 `*.ext.ts` 가 한 번이라도 로드돼야 prototype 에 붙음. db-context
|
|
136
|
+
확장 메서드는 `*.ext.ts` 가 한 번이라도 로드돼야 prototype 에 붙음. `*.ext.ts` 가 진입점 import 그래프에 포함되면 로드 보장됨 — 표준 구조에선 `index.ts` 의 배럴 재export(`export * from "./db-main/system-data-log.ext"`)가 이를 충족. 배럴을 거치지 않고 db-context 를 직접 import 하는 경로가 따로 있으면 그 db-context 에 side-effect import 추가. 이력 직접 조회용 queryable 도 db-context 에 등록.
|
|
137
137
|
|
|
138
138
|
```ts
|
|
139
139
|
// main.db-context.ts
|
|
@@ -49,6 +49,13 @@ Claude 에이전트가 반드시 지켜야 할 행동 지침.
|
|
|
49
49
|
|
|
50
50
|
**요청 받은 스코프만 처리. 요청되지 않은 개선·확장 금지** (코드·문서·분석·대화 등 모든 작업).
|
|
51
51
|
|
|
52
|
+
**합의·패턴에 없는 구현 선택은 임의로 정하지 않음**: 합의된 범위가 규정하지 않는 모든 선택(색상·테마·아이콘·문구·메시지 포맷·헬퍼 도입·기존 코드나 계약의 변형 등 구현 세부 포함)은 동일 맥락(같은 파일·같은 레이어)의 [기존 코드 패턴](#결정-근거)을 따름. 따를 패턴이 없으면 사용자에게 묻기. 임의 결정 금지.
|
|
53
|
+
|
|
54
|
+
- "사소한 구현 디테일"·"기술적으로 필요"는 면제 사유가 아님 — 합의·패턴에 근거가 없으면 그 선택 자체가 물어야 할 결정임.
|
|
55
|
+
- 합의에서 건드리지 않기로 한 코드·계약(스키마·시그니처 등)을 바꿔야 할 필요가 생기면, 우회·변형으로 조용히 해결하지 말고 그 필요성을 사용자에게 보고 후 결정 수령.
|
|
56
|
+
- 나쁜 예: 같은 화면의 다운로드 버튼이 success 인데 업로드 버튼 색을 근거 없이 primary 로 정함.
|
|
57
|
+
- 좋은 예: 같은 화면 기존 버튼 테마를 따라 맞춤. 맞출 패턴이 없으면 질문.
|
|
58
|
+
|
|
52
59
|
## 사용자 질의 시
|
|
53
60
|
|
|
54
61
|
에이전트가 사용자에게 묻는 모든 행위 (결정·의견·정보 확인 등)에 적용. 사용자에게 묻고 답을 받아 [결정 근거](#결정-근거)로 확정.
|
|
@@ -1,43 +1,158 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sd-debug
|
|
3
|
-
description: 버그·실패·예외의 원인을 다관점으로 발굴하고 가설별 검증·해결책·적대검증을 거쳐 검증된 해결책을 제안하는 멀티에이전트 디버깅. Use when
|
|
3
|
+
description: 버그·실패·예외의 원인을 다관점으로 발굴하고 가설별 검증·해결책·적대검증을 거쳐 검증된 해결책을 제안하는 멀티에이전트 디버깅. Use when 사용자가 sd-debug 스킬을 직접 지정해 호출할 때만 사용 (자동 트리거 금지).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# sd-debug
|
|
7
7
|
|
|
8
|
-
디버깅 요청 시
|
|
8
|
+
디버깅 요청 시 메인 루프가 직접 오케스트레이션. 관점을 직접 도출한 뒤, 문제 규모에 따라 관점별 가설 발굴과 가설별 검증·해결책·적대검증을 서브에이전트(Agent 도구)로 펼치거나 직접 수행하고, 검증된 해결책을 병합·우선순위화·결정 처리. Workflow 도구는 쓰지 않음 — 효과를 문제 규모에 비례시켜 고정 비용을 피함.
|
|
9
|
+
|
|
10
|
+
## 디버깅 원칙 (전 단계 공통)
|
|
11
|
+
|
|
12
|
+
발굴·검증·적대검증 서브에이전트에 항상 주입하고, 메인 루프가 직접 수행할 때도 동일하게 적용:
|
|
13
|
+
|
|
14
|
+
- 모든 판정은 실제 코드/설정을 Read 하여 확인. 근거 없는 추측·일반론 금지(추측은 '가설'로만 등록, 사실로 단정 금지).
|
|
15
|
+
- 현재 워킹트리만 기준. git log/diff/show/blame 등 과거 조회 금지. `.back/` 및 `.gitignore` 등재 경로(node_modules·dist·.tmp 등) 읽지 말 것.
|
|
16
|
+
- 결측(null/undefined)은 결측대로 다룰 것. 빈 값을 추측으로 채우지 말 것.
|
|
17
|
+
- 입력 정보가 부족하면 Grep/Glob/Read 로 코드베이스를 직접 조사해 보강.
|
|
9
18
|
|
|
10
19
|
## 절차
|
|
11
20
|
|
|
12
|
-
1.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
### 1. 문제 확정
|
|
22
|
+
|
|
23
|
+
증상·기대동작·관찰결과를 확정.
|
|
24
|
+
|
|
25
|
+
- 대화 중 오류를 논의하다 진입했으면, 그때까지의 맥락(증상·에러·스택·시도·관찰)을 요약해 문제 설명으로 합성.
|
|
26
|
+
- 에러 메시지·스택·재현조건·관련 코드 경로·환경은 있으면 함께 모음(선택).
|
|
27
|
+
- 문제 설명조차 불명확하면 사용자에게 묻기.
|
|
28
|
+
|
|
29
|
+
### 2. 의심 관점 도출 (메인 루프 직접)
|
|
30
|
+
|
|
31
|
+
증상을 보고, 원인을 찾을 때 서로 겹치지 않는 '의심 관점(범주)'을 도출:
|
|
32
|
+
|
|
33
|
+
- 증상 성격에 맞춰 동적으로 고름. 예시 범주: 동시성·타이밍, 데이터·결측, 타입·계약, 로직·경계조건, 외부의존·환경·설정, 상태·생명주기. (예시일 뿐 — 증상에 맞게 가감)
|
|
34
|
+
- 각 관점에 key·title·focus(이 관점이 의심하는 구체 지점) 부여.
|
|
35
|
+
- 보통 3~6개. 증상을 좁게 가리키면 적게, 막연하면 넓게.
|
|
36
|
+
|
|
37
|
+
### 3. 규모 판단 → 펼침 여부 결정
|
|
38
|
+
|
|
39
|
+
- **가설 발굴**: 관점이 여러 개이고 코드베이스 조사 범위가 넓으면 → 관점마다 Agent 펼침(각 Agent 는 자기 관점만 보므로 사각지대가 줄고 재현율이 오름). 관점이 적고 조사 범위가 좁으면 메인 루프가 직접 발굴.
|
|
40
|
+
- **검증·적대검증**: 검증자가 가설을 발굴자와 무관하게 코드로 재확인하는 독립성이 핵심. **가설이 1건이라도 검증·적대검증은 독립 Agent 로 수행함이 기본**. 가설이 다수면 가설마다 Agent 병렬.
|
|
41
|
+
- 서브에이전트 다수를 펼칠 때는 **한 메시지에 Agent 호출을 여러 개** 넣어 동시 실행. agentType 은 general-purpose (전수 Read·판정 필요).
|
|
42
|
+
|
|
43
|
+
### 4. 가설 발굴
|
|
44
|
+
|
|
45
|
+
(Agent 펼침 또는 직접) 관점별로 원인 가설을 발굴하고, 어떤 관점에도 매이지 않는 자유탐색 가설도 1벌 추가(사각지대 안전망).
|
|
46
|
+
|
|
47
|
+
관점별 Agent 프롬프트:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
문제: <문제 설명>
|
|
51
|
+
[디버깅 원칙 주입]
|
|
52
|
+
|
|
53
|
+
너의 일: 오직 '<title>' 관점에서만 원인 가설을 발굴하라. 이 관점의 의심 지점: <focus>
|
|
54
|
+
- 코드베이스를 Grep/Glob/Read 로 직접 조사해 이 관점에 해당하는 원인 후보를 빠짐없이 뽑을 것(재현율 우선).
|
|
55
|
+
- 다른 관점의 원인은 무시(중복은 이후 정리).
|
|
56
|
+
- 각 가설에 title·cause·perspective('<title>')·evidenceExpected(맞다면 보일 근거)·refuteSignal(틀렸다면 보일 반증) 채움.
|
|
57
|
+
- 이 관점에서 원인이 안 보이면 비울 것(억지 생성 금지).
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
자유탐색 Agent 는 위에서 `perspective='free'`, 정해진 관점 목록에 잘 안 들어가는 원인일수록 가치가 크다는 지시로 1벌.
|
|
61
|
+
|
|
62
|
+
### 5. 가설 중복제거 (메인 루프 직접)
|
|
63
|
+
|
|
64
|
+
발굴된 가설을 의미 기준으로 병합:
|
|
65
|
+
|
|
66
|
+
- '같은 근본 원인'을 가리키는 가설끼리만 하나로 합침(표현만 다른 중복 제거). 근본 원인이 다르면 절대 합치지 말 것 — 서로 다른 원인을 뭉개면 검증에서 통째 탈락함.
|
|
67
|
+
- 병합 시 evidenceExpected·refuteSignal 은 합쳐 보존, perspective 는 합쳐진 관점들 표기.
|
|
68
|
+
- 개수를 인위적으로 줄이지 말 것(재현율 우선). 진짜 중복만 제거.
|
|
69
|
+
|
|
70
|
+
### 6. 검증 + 해결책 도출
|
|
71
|
+
|
|
72
|
+
각 가설을 (독립 Agent 가 기본) 코드를 Read 해 검증하고, 통과 시 같은 근거 위에서 해결책까지 도출. Agent 프롬프트:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
문제: <문제 설명>
|
|
76
|
+
[디버깅 원칙 주입]
|
|
77
|
+
|
|
78
|
+
너의 일: 아래 원인 가설을 실제 코드를 Read 해 검증하고(관대 기준), 통과하면 같은 코드 근거 위에서 해결책까지 도출하라.
|
|
79
|
+
|
|
80
|
+
1) 검증(verdict):
|
|
81
|
+
- confirmed: 코드에서 근거를 확인함.
|
|
82
|
+
- uncertain: 근거가 부분적·애매하지만 코드에서 일부라도 뒷받침됨 — 통과시킴(이후 적대검증이 거른다).
|
|
83
|
+
- rejected: 코드로 보아 '명백히 틀린' 경우, 또는 코드에서 근거가 전혀 확인되지 않고 반증신호가 우세한 경우. 단 일부라도 뒷받침되면 기각하지 말고 uncertain('근거 전무'만 기각해 누락 방지).
|
|
84
|
+
- reason 에 확인한 코드 위치·내용 인용. 통과면 refinedCause 에 구체화된 원인 기술.
|
|
85
|
+
|
|
86
|
+
2) 해결책(rejected 가 아닐 때만):
|
|
87
|
+
- 확인한 코드 근거 위에서, 근본 원인을 가장 직접적으로 제거하는 정도가 높은 순으로 최대 2개, 서로 접근이 다르게.
|
|
88
|
+
- 각 후보에 approach·mechanism(원인을 어떻게 제거하나)·changeScope(수정 범위) 채움.
|
|
89
|
+
- 과도한 설계(over-engineering)·증상만 가리는 임시방편은 피할 것.
|
|
90
|
+
- rejected 면 solutions 를 비울 것.
|
|
91
|
+
|
|
92
|
+
가설:
|
|
93
|
+
- 제목: <title>
|
|
94
|
+
- 원인: <cause>
|
|
95
|
+
- 관점: <perspective>
|
|
96
|
+
- 예상 근거: <evidenceExpected>
|
|
97
|
+
- 반증 신호: <refuteSignal>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`rejected` 가설은 dropped 로 분리(보고용). `confirmed`/`uncertain` 은 통과로 다음 단계.
|
|
101
|
+
|
|
102
|
+
### 7. 적대검증
|
|
103
|
+
|
|
104
|
+
통과 가설의 각 해결책을 (독립 Agent 가 기본) 4관점에서 적대적으로 공격. 해결책마다 Agent 1개. 프롬프트:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
문제: <문제 설명>
|
|
108
|
+
[디버깅 원칙 주입]
|
|
109
|
+
|
|
110
|
+
너의 일: 아래 (원인 가설 + 해결책) 쌍을 다음 4개 관점 각각에서 적대적으로 공격하라. 기본 입장은 '이 해결책은 결함이 있다'로 두고 약점을 찾을 것. 각 관점을 서로 끌려가지 말고 독립 판정해 관점마다 1개 항목으로 반환.
|
|
111
|
+
|
|
112
|
+
관점:
|
|
113
|
+
- 인과: 해결책이 이 가설의 원인을 실제로 제거하는가. 원인-증상 인과가 성립하는가.
|
|
114
|
+
- 회귀·부작용: 해결책이 새 버그·룰 위반·엣지케이스(결측 null/undefined·동시성/트랜잭션·soft delete 동명 레코드·권한 분기·타입/스키마 제약)를 유발하는가.
|
|
115
|
+
- 증거 정합: 가설의 근거가 실제 코드/스택과 일치하는가. 오인·과장은 없는가.
|
|
116
|
+
- 대안 원인: 이 가설 말고 다른 원인이 진짜일 가능성은 없는가.
|
|
117
|
+
|
|
118
|
+
각 관점 판정:
|
|
119
|
+
- pass=false 로 둘 결함을 찾으면 reason 에 코드 근거와 함께 적고, 치명적(회귀 유발·인과 불성립 등)이면 critical=true. 교정안 있으면 revisedNote 에.
|
|
120
|
+
- 그 관점에서 결함이 없으면 pass=true, critical=false, revisedNote="".
|
|
121
|
+
|
|
122
|
+
원인 가설: <title> — <refinedCause 또는 cause>
|
|
123
|
+
해결책: <approach> / <mechanism> (수정 범위: <changeScope>)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**fail-fast**: 펼친 Agent 중 하나라도 실패(에러)하면 부분 결과로 진행 금지. 실패 사실을 알린 뒤 해당 Agent 만 재실행해 보완. 전건 정상이어야 전 가설·전 해결책 검증 완료로 간주.
|
|
127
|
+
|
|
128
|
+
### 8. 해결책 판정 (메인 루프 집계)
|
|
129
|
+
|
|
130
|
+
각 해결책의 4관점 결과를 다음 규칙으로 판정:
|
|
131
|
+
|
|
132
|
+
- **veto**: 어느 한 관점이라도 `critical=true && pass=false` 면 기각(치명결함 1표면 탈락).
|
|
133
|
+
- **passed**: veto 가 없고, `pass=true` 가 과반(passVotes > total/2)이면 통과(채택 후보).
|
|
134
|
+
- **risks**: `pass=false` 인 관점의 reason 모음(잔존 리스크).
|
|
135
|
+
- **revisions**: 비어있지 않은 revisedNote 모음(교정안).
|
|
136
|
+
- `votes` = `passVotes/total` 로 통과 강도 표기.
|
|
137
|
+
|
|
138
|
+
### 9. 병합·우선순위화 (메인 루프)
|
|
139
|
+
|
|
140
|
+
- 같은 근본 원인의 가설이 중복되면 병합.
|
|
141
|
+
- 각 가설의 해결책 중 `passed=true` 인 것을 채택하되 revisions(교정)를 반영하고 risks 는 잔존 리스크로 보존.
|
|
142
|
+
- (검증 confidence: confirmed>uncertain) + (적대검증 통과 강도 votes) + (원인-증상 직접성)으로 우선순위 정렬.
|
|
16
143
|
|
|
17
|
-
|
|
18
|
-
- `Workflow({ scriptPath: ".claude/skills/sd-debug/workflow.js", args: <문제 설명> })`.
|
|
19
|
-
- `args` 는 1단계의 문제 설명(자연어 문자열 또는 `{ problem, error, repro, paths, env }` 객체).
|
|
20
|
-
- 관점 도출·가설 발굴·검증·해결책 탐색·적대검증·병합은 워크플로가 자율 수행.
|
|
144
|
+
### 10. 결과 렌더
|
|
21
145
|
|
|
22
|
-
|
|
23
|
-
- 같은 근본 원인의 가설이 중복되면 병합.
|
|
24
|
-
- 각 가설의 해결책 중 `passed: true` 인 것을 채택하되 `revisions`(교정)를 반영하고 `risks` 는 잔존 리스크로 보존.
|
|
25
|
-
- (검증 confidence: confirmed>uncertain) + (적대검증 통과 강도 `votes`) + (원인-증상 직접성)으로 우선순위 정렬.
|
|
146
|
+
행동 규칙 "문제 발생 시" 의 3블록으로 제시:
|
|
26
147
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- 해결책: 채택한 해결책(교정 반영) + 잔존 리스크.
|
|
148
|
+
- **원인 가설**: 통과 가설의 hypothesis·cause (+ dropped 로 기각된 가설과 사유).
|
|
149
|
+
- **검증**: 각 항목의 verdict·verifyReason (uncertain 은 "근거 약함" 으로 표시).
|
|
150
|
+
- **해결책**: 채택한 해결책(교정 반영) + 잔존 리스크.
|
|
31
151
|
|
|
32
|
-
|
|
152
|
+
### 11. 결정 진행
|
|
33
153
|
|
|
34
|
-
|
|
154
|
+
채택 해결책 후보가 1건 이상이면 행동 규칙 "사용자 질의 시" 의 결정 진행 모드로 전환(우선순위 순). 사용자가 고른 해결책만 실제 수정에 착수.
|
|
35
155
|
|
|
36
|
-
|
|
156
|
+
### 12. 미해결 보고
|
|
37
157
|
|
|
38
|
-
|
|
39
|
-
- `solutions[]`: `{ approach, mechanism, changeScope, passed, vetoed, votes, risks, revisions }` — `passed: true` 가 채택 후보.
|
|
40
|
-
- `dropped[]`: `{ hypothesis, cause, reason }` — 검증에서 기각된 가설과 사유.
|
|
41
|
-
- `summary`: 집계(가설 수·confirmed/uncertain·dropped·solutionsPassed·noSolution).
|
|
42
|
-
- `perspectives`: 사용한 의심 관점 목록.
|
|
43
|
-
- `problem`: 입력 문제 요약.
|
|
158
|
+
검증된 해결책이 0건이면 통과 가설의 가설·탈락 사유와 dropped 를 제시해 다음 수동 디버깅의 출발점으로 삼게 함.
|