@simplysm/sd-claude 14.0.100 → 14.0.101
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 +2 -0
- package/claude/references/sd-simplysm14/apis/angular/features.md +7 -2
- package/claude/references/sd-simplysm14/apis/sd-cli/sd-config-types.md +1 -1
- package/claude/references/sd-simplysm14/apis/service-server/README.md +10 -5
- package/claude/references/sd-simplysm14/manuals/client-app-structure.md +19 -1
- package/claude/references/sd-simplysm14/manuals/client-component.md +35 -3
- package/claude/references/sd-simplysm14/manuals/client-crud.md +25 -0
- package/claude/references/sd-simplysm14/manuals/client-orm.md +1 -0
- package/claude/references/sd-simplysm14/manuals/client-print.md +134 -0
- package/claude/references/sd-simplysm14/manuals/client-shared-data.md +21 -2
- package/claude/references/sd-simplysm14/manuals/client-system-config.md +66 -0
- package/claude/references/sd-simplysm14/manuals/orm.md +1 -1
- package/claude/rules/sd-design-rules.md +18 -19
- package/claude/sd-system-prompt.md +288 -384
- package/claude/settings.json +4 -1
- package/claude/skills/sd-dev/SKILL.md +1 -1
- package/claude/skills/sd-docs/SKILL.md +1 -1
- package/claude/skills/sd-docs/references/subagent-prompt.md +8 -10
- package/claude/skills/sd-impl/SKILL.md +1 -1
- package/claude/skills/sd-spec/references/format.md +1 -1
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ ORM 호출, 파일 변환, 비즈니스 로직 등은 위 두 경우에 해당
|
|
|
34
34
|
| `sd-crud-list` / `sd-crud-detail` 채택한 목록·단건 화면 (편집 진입·삭제/복구·엑셀 다운로드·행 선택 제한) | [client-crud.md](./manuals/client-crud.md) |
|
|
35
35
|
| 클라이언트 데모 컴포넌트 작성 | [client-demo.md](./manuals/client-demo.md) |
|
|
36
36
|
| `<sd-tab>` 사용 | [client-tab.md](./manuals/client-tab.md) |
|
|
37
|
+
| 화면 데이터를 종이 인쇄·PDF 출력 (`.print-template.ts` 작성, `printAsync`/`getPdfBufferAsync`, PDF 다운로드·이메일) | [client-print.md](./manuals/client-print.md) |
|
|
37
38
|
| 앱에서 서버 서비스·이벤트 호출 (provider 정의·항목 추가) | [client-service.md](./manuals/client-service.md) |
|
|
38
39
|
| 앱에서 ORM(DB) 사용 (AppOrmProvider 정의) | [client-orm.md](./manuals/client-orm.md) |
|
|
39
40
|
| 앱에서 공유 마스터 데이터 사용 (provider 정의·항목 추가, 선택 컨트롤의 관리·선택 모달, 좌측 선택+우측 상세 레이아웃) | [client-shared-data.md](./manuals/client-shared-data.md) |
|
|
@@ -45,6 +46,7 @@ ORM 호출, 파일 변환, 비즈니스 로직 등은 위 두 경우에 해당
|
|
|
45
46
|
| CRUD 처리에 데이터 변경 이력 적재·조회·표시 (누가·언제·무엇을 변경, 목록의 수정일시·수정자 컬럼) | [data-log.md](./manuals/data-log.md) |
|
|
46
47
|
| 콘솔 로깅 코드 작성/수정 (모든 패키지) | [logging.md](./manuals/logging.md) |
|
|
47
48
|
| 클라이언트 시스템 에러·로그를 DB 등 외부에 적재·조회 | [client-system-log.md](./manuals/client-system-log.md) |
|
|
49
|
+
| 클라이언트 사용자별 UI·시스템 설정을 DB 등에 영속화 (`SdSystemConfigProvider.fn` 배선, 시트 컬럼 설정 서버 저장) | [client-system-config.md](./manuals/client-system-config.md) |
|
|
48
50
|
| 패키지 테스트·통합 테스트 작성/추가 | [test.md](./manuals/test.md) |
|
|
49
51
|
|
|
50
52
|
## 패키지 인덱스
|
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
### `SdThemeProvider`
|
|
8
8
|
|
|
9
|
-
`@Injectable({ providedIn: "root" })`. (`provideSdAngular` 가 dark/fontSize 를 `SdLocalStorageProvider` 에 영속화)
|
|
9
|
+
`@Injectable({ providedIn: "root" })`. (`provideSdAngular` 가 dark/blueprint/fontSize 를 `SdLocalStorageProvider` 에 영속화)
|
|
10
10
|
|
|
11
11
|
- `dark: WritableSignal<boolean>` (초기 false) — `effect` 로 body `sd-theme-dark` 클래스 토글(브라우저 전용).
|
|
12
|
+
- `blueprint: WritableSignal<boolean>` (초기 false) — `effect` 로 body `sd-theme-blueprint` 클래스 토글. dark 와 직교(독립).
|
|
12
13
|
- `fontSize: WritableSignal<number>` (초기 12) — `effect` 로 `documentElement.style.fontSize` 설정.
|
|
13
14
|
- `fontSizePresets: readonly number[]` = `[12, 14, 16, 20, 24, 28]`.
|
|
14
15
|
- `increaseFontSize()` / `decreaseFontSize()` — 다음/이전 프리셋으로(경계에서 no-op).
|
|
15
16
|
|
|
17
|
+
#### 블루프린트(엔지니어링 도면) 테마
|
|
18
|
+
|
|
19
|
+
`blueprint`·`dark` 직교 조합으로 4면: 디폴트-라이트/다크, 블루프린트-트레이싱지(라이트)/시아노타입(다크). CSS 변수 오버라이드(`.sd-theme-blueprint` / `.sd-theme-blueprint.sd-theme-dark`)로 구현 — primary=제도 블루·danger=리드라인, 직각(라운드 0), 하드 오프셋 섀도(`--elevation-blur-mult: 0`). 모눈 그리드는 body·chrome(`sd-sidebar`·`sd-topbar`)에만, 콘텐츠(카드·시트 등)는 무지. 폰트는 앱 책임(프레임워크 미번들).
|
|
20
|
+
|
|
16
21
|
### `SdThemeSelector` — `<sd-theme-selector>`
|
|
17
22
|
|
|
18
|
-
팔레트 아이콘 드롭다운(폰트 +/- · 다크모드 스위치). input 없음. `isMinFontSize`/`isMaxFontSize: computed` 로 +/- 버튼 비활성.
|
|
23
|
+
팔레트 아이콘 드롭다운(폰트 +/- · 다크모드 스위치 · 블루프린트 스위치). input 없음. `isMinFontSize`/`isMaxFontSize: computed` 로 +/- 버튼 비활성.
|
|
19
24
|
|
|
20
25
|
## 주소
|
|
21
26
|
|
|
@@ -183,7 +183,7 @@ interface SdPostPublishScriptConfig { type: "script"; cmd: string; args: string[
|
|
|
183
183
|
|
|
184
184
|
```typescript
|
|
185
185
|
"excel": { target: "neutral", publish: { type: "npm" } },
|
|
186
|
-
"client": { target: "client", server: "server", publish: { type: "ftp", host: "...", path: "/www" } },
|
|
186
|
+
"client-admin": { target: "client", server: "server", publish: { type: "ftp", host: "...", path: "/www/client-admin" } },
|
|
187
187
|
```
|
|
188
188
|
|
|
189
189
|
## Capacitor 설정 (SdCapacitorConfig)
|
|
@@ -29,17 +29,22 @@ function createServiceServer<TAuthInfo = unknown>(options: ServiceServerOptions)
|
|
|
29
29
|
|
|
30
30
|
- `rootPath: string` — 서버 작업 루트. 정적 파일·업로드·자동업데이트는 `rootPath/www` 하위를, 설정은 `rootPath/.config.json` 을 기준으로 한다. 절대경로 권장.
|
|
31
31
|
- `port: number` — 리슨 포트(바인딩 호스트는 `"0.0.0.0"` 고정). `0` 을 주면 OS 가 임의 포트를 할당하므로 테스트에 쓰고, 실제 포트는 `server.fastify.server.address()` 로 확인한다.
|
|
32
|
-
- `ssl?: { pfxBytes: Uint8Array; passphrase?: string } | { pemKeyBytes: Uint8Array; certBytes: Uint8Array; caBytes?: Uint8Array; passphrase?: string } | { letsencrypt: { domains: string[]; email: string; staging?: boolean } }` — HTTPS 인증서. 형식은 들어온 필드로 구분한다.
|
|
32
|
+
- `ssl?: { pfxBytes: Uint8Array; passphrase?: string } | { pemKeyBytes: Uint8Array; certBytes: Uint8Array; caBytes?: Uint8Array; passphrase?: string } | { letsencrypt: { domains: string[]; email: string; staging?: boolean; cloudflareApiToken?: string } }` — HTTPS 인증서. 형식은 들어온 필드로 구분한다.
|
|
33
33
|
- `pfxBytes` 가 있으면 PFX 방식(인증서+키 번들, `passphrase` 는 PFX 비밀번호).
|
|
34
34
|
- `pemKeyBytes`+`certBytes` 가 있으면 PEM 방식(`pemKeyBytes` 개인키·`certBytes` 인증서, 선택적으로 `caBytes` 중간 CA 체인·`passphrase` 암호화된 키 비밀번호). 바이트는 내부에서 `Buffer` 로 변환.
|
|
35
|
-
- `letsencrypt` 가 있으면 Let's Encrypt 자동 발급/갱신. `domains` 인증서를
|
|
35
|
+
- `letsencrypt` 가 있으면 Let's Encrypt 자동 발급/갱신. `domains` 인증서를 발급해 `rootPath/.acme/`(계정키·인증서·키)에 저장하고, 만료 30일 전 자동 갱신 후 무중단 교체한다. 챌린지 방식은 `cloudflareApiToken` 유무로 갈린다 — **미지정 시 TLS-ALPN-01**(서버가 443 핸드셰이크로 직접 응답), **지정 시 DNS-01**(Cloudflare 에 `_acme-challenge` TXT 를 자동 등록·삭제; 토큰은 `Zone:Read`+`Zone.DNS:Edit` 권한 필요, zone 은 도메인으로 자동 조회). `email` 은 LE 계정 연락처, `staging: true` 면 LE 스테이징(레이트리밋 회피, 테스트용)을 쓴다. 캐시된 유효 인증서가 있으면 즉시 적용하고, 없으면 최초 발급 완료까지 `listen()` 이 대기하며 발급 실패 시 throw 한다(`SD_ACME_DIRECTORY_URL` 로 ACME 디렉토리 URL, `SD_CLOUDFLARE_API_BASE_URL` 로 Cloudflare API base URL 재정의 가능 — 사설 CA·테스트용).
|
|
36
36
|
|
|
37
37
|
지정 시(어느 방식이든) HTTPS 로 기동하고 HSTS·`crossOriginOpenerPolicy` 보안 헤더가 켜진다. 미지정 시 HTTP(평문)로 뜨고 `upgrade-insecure-requests` CSP 가 해제된다. 사내망 평문이면 생략, 외부 노출이면 지정.
|
|
38
38
|
|
|
39
39
|
`letsencrypt` 전제(코드 밖, 운영자 책임):
|
|
40
|
-
- **
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
- **TLS-ALPN-01**(`cloudflareApiToken` 미지정):
|
|
41
|
+
- **Node 20.18.0+ 또는 22.9.0+** — 핸드셰이크 중 인증서를 주입하는 `TLSSocket.setKeyCert` 가 필요하다. 미만이면 기동 시 throw.
|
|
42
|
+
- **와일드카드 불가** — `domains` 는 정확한 FQDN.
|
|
43
|
+
- 검증 connection 이 포트 443 으로 인입되어 이 서버에 도달해야 한다: 공개 DNS A 레코드 → 서버, 앞단에 L4 프록시가 있으면 SNI 기준으로 이 서버에 패스스루(예: nginx `stream` + `ssl_preread`). 지역 차단(geo-block) 방화벽이면 LE 의 해외 검증 노드가 막혀 발급이 실패할 수 있다.
|
|
44
|
+
- **DNS-01**(`cloudflareApiToken` 지정):
|
|
45
|
+
- 인바운드 검증이 없어 방화벽/NAT 환경에서도 발급 가능하며 **와일드카드 가능**.
|
|
46
|
+
- 도메인 DNS 가 Cloudflare 로 관리되어야 하고, 서버에서 Cloudflare API 로 아웃바운드가 가능해야 한다.
|
|
47
|
+
- 공통: 도메인 CAA 가 `letsencrypt.org` 를 허용하고, 서버에서 LE API 로 아웃바운드가 가능해야 한다.
|
|
43
48
|
- `auth?: { jwtSecret: string } | false` — JWT 인증 설정. 객체면 `jwtSecret` 으로 토큰을 서명·검증한다. `false` 면 인증을 **의도적으로 비활성화**(`auth(...)` 래핑 메서드도 인증 검사 스킵). `undefined`(미지정)인데 권한 요구(`auth(...)` 래핑) 서비스가 하나라도 등록돼 있으면 `listen()` 이 throw — 설정 누락과 의도적 비활성화를 구분한다.
|
|
44
49
|
- `services: ServiceDefinition[]` — `defineService` 로 만든 서비스 정의 배열. RPC 로 노출할 서비스 전부를 여기 등록한다. 라우팅은 정의의 `names` 매칭으로 이뤄진다.
|
|
45
50
|
- `legacyV1Handlers?: V1RequestHandler[]` — V1(ver≠2) 레거시 클라이언트용 커스텀 요청 핸들러(선택). 자세히: [v1-legacy.md](./v1-legacy.md).
|
|
@@ -134,4 +134,22 @@ this._sdAppStructure.usableModules.set(["scheduling"]);
|
|
|
134
134
|
- `modules`: 나열한 모듈 중 **하나라도** 활성이면 표시(OR).
|
|
135
135
|
- `requiredModules`: 나열한 모듈이 **모두** 활성이어야 표시(AND).
|
|
136
136
|
- 조건을 건 항목은 해당 모듈이 `usableModules` 에 없으면 메뉴·권한에서 빠짐. 조건이 없는 항목은 모듈 설정과 무관하게 표시됨.
|
|
137
|
-
-
|
|
137
|
+
- 모듈을 쓰지 않는 앱은 `usableModules` 설정을 생략 가능 — 모듈 조건(`modules`/`requiredModules`) 이 없는 항목은 `usableModules` 가 미설정(undefined) 이어도 항상 표시됨.
|
|
138
|
+
|
|
139
|
+
## 6. 정의한 메뉴를 화면에 띄우기
|
|
140
|
+
|
|
141
|
+
정의한 구조는 `SdAppStructureProvider.usableMenus()` 로 읽어 `<sd-sidebar-menu>` 에 바인딩. `usableMenus()` 는 권한(`permRecord`)·모듈(`usableModules`) 필터를 이미 적용한 최종 메뉴 트리를 반환.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
private readonly _sdAppStructure = inject(SdAppStructureProvider);
|
|
145
|
+
|
|
146
|
+
menus = computed(() => this._sdAppStructure.usableMenus());
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```html
|
|
150
|
+
<sd-sidebar-menu [menus]="menus()" />
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `usableMenus()` 는 권한 없는 화면·비활성 모듈을 이미 걸러낸 트리 — 컴포넌트에서 추가 필터를 두지 않음.
|
|
154
|
+
- 권한 필터가 걸리려면 `permRecord` 가 set 돼 있어야 함(§4). 로그인 전에는 빈 권한이라 권한 화면이 메뉴에 안 나옴.
|
|
155
|
+
- 사이드바·탑바 등 앱 셸 레이아웃 컴포넌트(`sd-sidebar`/`sd-sidebar-menu` 등) 자체는 [apis/angular/README.md](../apis/angular/README.md) 참조.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
| 파일명 형식 | 역할 |
|
|
10
10
|
| ---------------------------- | ------------------------------------------------------------------ |
|
|
11
|
-
| `<domain>.view.ts` |
|
|
11
|
+
| `<domain>.view.ts` | 여러 영역을 합성·분할한 화면. list/detail 자식 합성형(트리거 중계) 또는 분할·리포트형(영역 직접 포함·직접 페치). |
|
|
12
12
|
| `<domain>.list.ts` | 목록. `sd-crud-list` 사용. |
|
|
13
13
|
| `<domain>.detail.ts` | 단건 보기/편집. `sd-crud-detail` 사용. |
|
|
14
14
|
| `<domain>.modal.ts` | 모달 전용 화면. |
|
|
@@ -97,7 +97,9 @@ Angular 기본과 다른 부분만 명시:
|
|
|
97
97
|
|
|
98
98
|
- **`*.list.ts`** — 자체 검색·페이지·정렬·재조회를 책임. `selectMode` 같은 입력을 받아 부모가 선택 동작을 제어할 수 있게 노출.
|
|
99
99
|
- **`*.detail.ts`** — 식별자(`input.required`) 를 받아 자체 로드·저장. 변경·삭제 후 `submitted` output 으로 부모에게 알림.
|
|
100
|
-
- **`*.view.ts`** —
|
|
100
|
+
- **`*.view.ts`** — 여러 영역을 합성·분할한 화면. 두 형태:
|
|
101
|
+
- (a) **합성형** — list/detail 자식을 두고 자식 간 트리거만 중계. 데이터 페치는 자식에 위임하고 view 가 직접 페치하지 않음.
|
|
102
|
+
- (b) **분할·리포트형** — 재사용 list/detail 로 나누지 않고 필터·시트 등 영역을 view 가 직접 품는 읽기 전용 리포트·대시보드. 이 경우 view 가 직접 페치함.
|
|
101
103
|
|
|
102
104
|
화면이 list 또는 detail 하나로 끝나면 view 를 만들지 않음. 이 경우 list/detail 자체가 라우팅 진입 단위.
|
|
103
105
|
|
|
@@ -624,6 +626,22 @@ async onSubmit(): Promise<void> {
|
|
|
624
626
|
</sd-sheet-column>
|
|
625
627
|
```
|
|
626
628
|
|
|
629
|
+
**다단(그룹) 헤더**: `[header]` 에 문자열 배열을 주면 다단 헤더가 됨. 배열의 각 원소가 위에서 아래로 헤더 행이 되고, 인접 컬럼이 같은 상위 행 텍스트를 가지면 그 상위 셀이 가로로 병합되어 그룹 헤더로 묶임. 단일 문자열 컬럼은 전체 헤더 높이를 세로로 차지.
|
|
630
|
+
|
|
631
|
+
```html
|
|
632
|
+
<sd-sheet-column [key]="'salesAmount'" [header]="['홈택스', '매출']">
|
|
633
|
+
<ng-template [cell]="items()" let-item="item">
|
|
634
|
+
<div class="p-xs-sm tx-right">{{ item.salesAmount | number }}</div>
|
|
635
|
+
</ng-template>
|
|
636
|
+
</sd-sheet-column>
|
|
637
|
+
<sd-sheet-column [key]="'salesVat'" [header]="['홈택스', '부가세']">
|
|
638
|
+
<ng-template [cell]="items()" let-item="item">...</ng-template>
|
|
639
|
+
</sd-sheet-column>
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
- 두 컬럼이 상위 행에 같은 `'홈택스'` 를 가지므로 상단에 `홈택스` 그룹 헤더가 두 컬럼 위로 병합됨.
|
|
643
|
+
- 배열 길이가 컬럼마다 달라도 됨 — 짧은 쪽은 마지막 원소가 아래 행까지 세로로 채워짐.
|
|
644
|
+
|
|
627
645
|
**폭 약속**:
|
|
628
646
|
|
|
629
647
|
- `[width]` 는 **미명시가 기본** (자동). px 지정은 사용자가 명시 지시한 경우에만 적용.
|
|
@@ -676,12 +694,26 @@ async onSubmit(): Promise<void> {
|
|
|
676
694
|
|
|
677
695
|
- 컬럼 중 하나라도 `#summaryTpl` 을 가지면 요약 행 전체가 활성화됨. 정의 없는 컬럼은 빈 셀로 표시.
|
|
678
696
|
- 셀 본문 약속(`p-xs-sm`, 정렬 클래스 등) 은 요약 셀에도 동일하게 적용.
|
|
679
|
-
- 합계·평균 등 집계 값은 시트가 계산하지 않음.
|
|
697
|
+
- 합계·평균 등 집계 값은 시트가 계산하지 않음. 집계 출처는 목록의 **페이징 여부** 로 갈림.
|
|
698
|
+
|
|
699
|
+
**페이징 없는 전건 로드 목록** (자식·상세 목록 등) — `items()` 가 곧 전체이므로 `computed` 합산.
|
|
680
700
|
|
|
681
701
|
```ts
|
|
682
702
|
totalQuantity = computed(() => this.items().sum((i) => i.quantity) ?? 0);
|
|
683
703
|
```
|
|
684
704
|
|
|
705
|
+
**페이징 목록** — `items()` 는 현재 페이지뿐이라 `computed` 합산 금지 (페이지 합계만 나와 요약이 틀어짐). 전체 결과의 집계값을 별도 시그널로 받아 바인딩. 집계는 데이터 로딩 시 별도 쿼리로 구함 ([client-crud.md](./client-crud.md) 의 "페이징 목록 요약 집계" 참조).
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
summaryData = signal<{ amount: number }>({ amount: 0 });
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
```html
|
|
712
|
+
<ng-template #summaryTpl>
|
|
713
|
+
<div class="p-xs-sm tx-right">{{ summaryData().amount | number }}</div>
|
|
714
|
+
</ng-template>
|
|
715
|
+
```
|
|
716
|
+
|
|
685
717
|
## 폼·입력 컨트롤
|
|
686
718
|
|
|
687
719
|
### 폼 항목 레이아웃
|
|
@@ -291,6 +291,31 @@ if (!this.sortingDefs().some((s) => s.key === "id")) {
|
|
|
291
291
|
- 컬럼마다 `if (sort.key === "X") orderBy((c) => c.X, ...)` 식 분기 금지 — `sort.key` 가 select 별칭과 일치하므로 한 줄로 처리.
|
|
292
292
|
- `obj` 는 `@simplysm/core-common`, `SortingDef` 는 `@simplysm/angular`.
|
|
293
293
|
|
|
294
|
+
### 페이징 목록 요약 집계
|
|
295
|
+
|
|
296
|
+
시트 요약 행([client-component.md](./client-component.md) 의 "요약 행")의 합계·평균은 **전체 필터 결과** 기준이어야 함. `items()` 는 현재 페이지뿐이라 컴포넌트에서 합산하면 페이지 합으로 틀어짐. `_search` 안에서 정렬·`limit` 적용 전 쿼리에 집계 쿼리를 별도로 실행해 받음.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
summaryData = signal<{ amount: number }>({ amount: 0 });
|
|
300
|
+
|
|
301
|
+
// _search 안 — 필터까지 적용한 쿼리(qr)에, orderBy·limit 적용 전 단계에서 집계
|
|
302
|
+
const summary = { amount: 0 };
|
|
303
|
+
if (usePagination) {
|
|
304
|
+
const row = await qr.select((r) => ({ amount: expr.sum(r.amount) })).single();
|
|
305
|
+
summary.amount = row?.amount ?? 0;
|
|
306
|
+
}
|
|
307
|
+
// 이후 qr 에 orderBy·limit 적용(→ qr2)해 items 조회, { items, pageLength, summary } 반환
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
// _refresh 안 — 받은 집계를 시그널에 반영
|
|
312
|
+
this.summaryData.set(r.summary);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- 집계는 `orderBy`·`limit` 적용 전 쿼리에 실행 — 정렬·현재 페이지와 무관한 전체 합. 정렬·페이지를 적용한 `qr2` 에 실행하면 페이지 합으로 잘못 나옴.
|
|
316
|
+
- `usePagination=false`(엑셀 내보내기 등 전건 조회)면 집계 쿼리를 건너뜀 — 받은 전건을 컴포넌트에서 합산하면 됨.
|
|
317
|
+
- 누적값(잔액 등)은 합산하지 않음 — 행별 누적값이라 합계가 의미 없음.
|
|
318
|
+
|
|
294
319
|
## `sd-crud-detail`
|
|
295
320
|
|
|
296
321
|
단일 레코드 편집 화면의 표준 골격. 다음 기능을 일괄 제공: 폼 래핑, CTRL+S 단축키 저장, 저장 버튼, 모달의 "확인" 버튼 자동 처리.
|
|
@@ -32,6 +32,7 @@ export class AppOrmProvider {
|
|
|
32
32
|
- `@Injectable({ providedIn: "root" })`.
|
|
33
33
|
- DbContext 는 앱별로 정의 (예: `@adtek/db-main` 의 `MainDbContext`). 스키마 정의는 [orm.md](./orm.md).
|
|
34
34
|
- 진입 메서드는 `connectAsync` (트랜잭션 포함).
|
|
35
|
+
- `connectAsync` 는 콜백 안에서 난 FK(외래키) 제약 위반을 잡아 사용자 안내 메시지(`SdError`) 로 자동 변환함 — 참조 중인 데이터를 삭제하려 하면 "연관된 작업으로 인해 작업이 거부되었습니다" 류 경고가 화면에 뜸. 화면에서 같은 메시지를 따로 만들지 않음.
|
|
35
36
|
- 콜백의 반환값이 그대로 메서드의 반환값이 됨.
|
|
36
37
|
|
|
37
38
|
## 화면·프로바이더에서 쿼리를 실행하려면
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# 인쇄·PDF 출력 매뉴얼
|
|
2
|
+
|
|
3
|
+
화면 데이터를 종이로 인쇄하거나 PDF 로 받는 작업. `SdPrintProvider` 와 인쇄 템플릿(`<domain>.print-template.ts`) 으로 처리. **인쇄(브라우저 출력)와 PDF(이미지 캡처)는 페이지 분할 방식이 달라** 아래 함정을 함께 적용.
|
|
4
|
+
|
|
5
|
+
## 인쇄 템플릿을 작성하려면
|
|
6
|
+
|
|
7
|
+
`<domain>.print-template.ts` 에 `SdPrint` 를 구현한 컴포넌트를 둠. 인쇄·PDF 가 이 컴포넌트를 화면 밖에 임시로 렌더해 캡처함.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { ChangeDetectionStrategy, Component, input, signal, ViewEncapsulation } from "@angular/core";
|
|
11
|
+
import { DecimalPipe } from "@angular/common";
|
|
12
|
+
import type { SdPrint } from "@simplysm/angular";
|
|
13
|
+
|
|
14
|
+
@Component({
|
|
15
|
+
selector: "app-invoice-print-template",
|
|
16
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17
|
+
encapsulation: ViewEncapsulation.None,
|
|
18
|
+
standalone: true,
|
|
19
|
+
imports: [DecimalPipe],
|
|
20
|
+
template: `
|
|
21
|
+
@for (item of items(); track item.id) {
|
|
22
|
+
<div class="_page">
|
|
23
|
+
<h1 style="text-align: center">거래명세서</h1>
|
|
24
|
+
<!-- ... 본문 ... -->
|
|
25
|
+
</div>
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
styles: [
|
|
29
|
+
/* language=SCSS */ `
|
|
30
|
+
app-invoice-print-template {
|
|
31
|
+
display: block;
|
|
32
|
+
background: white;
|
|
33
|
+
|
|
34
|
+
._page {
|
|
35
|
+
padding: 50px;
|
|
36
|
+
page-break-after: always;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`,
|
|
40
|
+
],
|
|
41
|
+
})
|
|
42
|
+
export class InvoicePrintTemplate implements SdPrint {
|
|
43
|
+
items = input.required<IInvoice[]>();
|
|
44
|
+
initialized = signal(true);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `implements SdPrint` — `initialized: Signal<boolean>` 보유 필수. provider 가 `initialized()` 가 true 가 될 때까지 기다린 뒤 인쇄/캡처함. 정적 템플릿은 `signal(true)` 로 즉시 완료, 템플릿이 자체 데이터 로딩을 하면 로딩 완료 후 `initialized.set(true)`.
|
|
49
|
+
- 데이터는 `input` 으로 받음 — 호출 측이 `inputs` 로 주입.
|
|
50
|
+
- `encapsulation: ViewEncapsulation.None` — 인쇄 시 전역으로 attach 되므로 스타일 캡슐화를 끔.
|
|
51
|
+
- 페이지 단위 div(`._page`) 에 `page-break-after: always` 를 주면 인쇄 시 항목마다 페이지가 나뉨.
|
|
52
|
+
|
|
53
|
+
## 화면에서 인쇄하려면
|
|
54
|
+
|
|
55
|
+
`SdPrintProvider.printAsync` 에 템플릿 타입과 input 을 넘김. 브라우저 인쇄 대화상자가 뜸.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
private readonly _sdPrint = inject(SdPrintProvider);
|
|
59
|
+
|
|
60
|
+
await this._sdPrint.printAsync({
|
|
61
|
+
type: InvoicePrintTemplate,
|
|
62
|
+
inputs: { items: this.items() },
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- 둘째 인자 `options` 로 `{ size, margin }` 조정 가능 (기본 `size: "A4 auto"`, `margin: "0"`). `size` 는 CSS `@page size` 로 들어감.
|
|
67
|
+
- 페이지 분할은 템플릿의 CSS `page-break-after`/`page-break-before` 로 제어 — `printAsync` 는 `window.print()` 를 호출하므로 브라우저 페이지 규칙을 따름.
|
|
68
|
+
|
|
69
|
+
## PDF 로 받으려면
|
|
70
|
+
|
|
71
|
+
`getPdfBufferAsync` 가 `Uint8Array`(PDF) 를 반환. `downloadBlob` 으로 파일 저장.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { downloadBlob } from "@simplysm/core-browser";
|
|
75
|
+
|
|
76
|
+
const pdfBuffer = await this._sdPrint.getPdfBufferAsync({
|
|
77
|
+
type: InvoicePrintTemplate,
|
|
78
|
+
inputs: { items: [item] },
|
|
79
|
+
});
|
|
80
|
+
downloadBlob(
|
|
81
|
+
new Blob([pdfBuffer], { type: "application/pdf" }),
|
|
82
|
+
`${item.code}_거래명세서.pdf`,
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- 둘째 인자 `options` 로 `{ orientation, pageSize }` 조정 (기본 `orientation: "p"`(portrait), `pageSize: "a4"`).
|
|
87
|
+
- **PDF 페이지 분할은 인쇄와 다름** — `getPdfBufferAsync` 는 템플릿 안의 **`.page` 클래스**(언더스코어 없음) 엘리먼트를 각각 한 페이지로 캡처함. `.page` 가 하나도 없으면 **전체를 1페이지**로 만듦.
|
|
88
|
+
- 다중 페이지 PDF 가 필요하면 페이지 div 에 `.page` 클래스를 줌 (인쇄용 `._page` + `page-break` 와는 별개의 클래스).
|
|
89
|
+
- 또는 위 예시처럼 항목마다 `getPdfBufferAsync` 를 **단건 호출**해 1페이지 PDF 를 여러 개 만듦.
|
|
90
|
+
|
|
91
|
+
## PDF 를 이메일로 보내려면
|
|
92
|
+
|
|
93
|
+
생성한 PDF 버퍼를 메일 서비스의 첨부로 보냄. 메일 발송은 롤백 불가하므로, 다건은 **전원 PDF 를 먼저 생성한 뒤 발송**해 생성 단계 실패 시 한 건도 보내지 않게 함.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// 1) 전원 PDF 선생성 — 여기서 실패하면 발송은 시작도 안 함
|
|
97
|
+
const prepared: { item: IInvoice; pdfBuffer: Uint8Array }[] = [];
|
|
98
|
+
for (const item of targets) {
|
|
99
|
+
const pdfBuffer = await this._sdPrint.getPdfBufferAsync({
|
|
100
|
+
type: InvoicePrintTemplate,
|
|
101
|
+
inputs: { items: [item] },
|
|
102
|
+
});
|
|
103
|
+
prepared.push({ item, pdfBuffer });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2) 발송 — 중간 실패 시 성공/실패/미발송 경계를 담아 throw
|
|
107
|
+
const sentCodes: string[] = [];
|
|
108
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
109
|
+
const { item, pdfBuffer } = prepared[i];
|
|
110
|
+
try {
|
|
111
|
+
await this._appService.mail.send({
|
|
112
|
+
to: item.email,
|
|
113
|
+
subject: "거래명세서",
|
|
114
|
+
html: "",
|
|
115
|
+
attachments: [{ filename: "거래명세서.pdf", content: pdfBuffer }],
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const notSent = prepared.slice(i + 1).map((p) => p.item.code);
|
|
119
|
+
throw new Error(
|
|
120
|
+
`전송 중단 — 성공 ${sentCodes.length}건, 실패 ${item.code}, 미발송 ${notSent.length}건`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
sentCodes.push(item.code);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- 메일 서비스(`mail.send`) 는 서버 서비스 — 호출 컨벤션은 [client-service.md](./client-service.md).
|
|
128
|
+
- 첨부 대상이 누락된 항목(예: 이메일 미설정) 이 있으면 발송 전에 막고 대상 목록을 안내 — 일부만 보내고 나머지를 건너뛰지 않음.
|
|
129
|
+
|
|
130
|
+
## 지킬 것
|
|
131
|
+
|
|
132
|
+
- 인쇄 템플릿은 `implements SdPrint` + `initialized` 신호를 둠 — 누락 시 provider 가 렌더 완료를 못 기다려 빈 출력이 됨.
|
|
133
|
+
- 인쇄 페이지 분할은 CSS `page-break`, PDF 페이지 분할은 `.page` 클래스 — 둘은 별개. PDF 가 한 장으로 뭉치면 `.page` 클래스 여부부터 확인.
|
|
134
|
+
- 인쇄/PDF 호출은 `busyCount` + `_sdToast.try` 로 감쌈 ([client-component.md](./client-component.md) 의 '에러·토스트').
|
|
@@ -69,7 +69,7 @@ export interface ISharedCustomer extends SharedDataBase<number> {
|
|
|
69
69
|
|
|
70
70
|
## 부트스트랩에 연결하려면 (새 앱 1회성)
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
CRUD 기반 컨테이너(`SdBaseContainer`)가 base 토큰 `SdSharedDataProvider` 를 optional inject(`inject(SdSharedDataProvider, { optional: true })`) 하므로, 부트스트랩 providers 에 앱 provider 를 그 토큰의 별칭으로 등록.
|
|
73
73
|
|
|
74
74
|
```ts
|
|
75
75
|
// 앱 부트스트랩 (main.ts)
|
|
@@ -81,7 +81,7 @@ bootstrapApplication(AppRoot, {
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
- 이 별칭이 없으면
|
|
84
|
+
- 이 별칭이 없으면 `SdBaseContainer` 의 optional inject 가 `null` 이 되어(추상 provider 라 `providedIn` 없음) 공유데이터 로딩 대기(`wait()`)를 건너뜀. select 컨트롤 자체는 화면의 `useSharedSignal(...)` → `items` 입력으로 데이터를 받으므로 이 별칭과 무관함.
|
|
85
85
|
|
|
86
86
|
## 마스터 데이터 항목을 추가하려면
|
|
87
87
|
|
|
@@ -155,6 +155,24 @@ sharedProducts = useSharedSignal("품목");
|
|
|
155
155
|
|
|
156
156
|
- `register` 에 쓴 이름 문자열을 그대로 넘기면 `TAppSharedData` 에서 타입이 추론됨.
|
|
157
157
|
|
|
158
|
+
## 변경을 통지하려면
|
|
159
|
+
|
|
160
|
+
마스터를 CRUD(등록·수정·삭제·복구) 한 뒤 `emitAsync("<이름>", [changeKeys])` 로 통지하면, 그 마스터를 구독 중인 모든 화면의 공유 시그널이 자동 갱신됨. `register` 의 `getter(changeKeys)` 수신측과 짝을 이루는 발신측 — `emitAsync` 가 그 `changeKeys` 분기를 발동시킴.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
private readonly _appSharedData = inject(AppSharedDataProvider);
|
|
164
|
+
|
|
165
|
+
// 데이터 변경 트랜잭션이 커밋된 뒤 통지
|
|
166
|
+
await this._appOrm.connectAsync(async (db) => {
|
|
167
|
+
// ... insert / update / soft delete ...
|
|
168
|
+
});
|
|
169
|
+
await this._appSharedData.emitAsync("역할", changedIds);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- 둘째 인자 `changeKeys` — 변경된 항목의 키 배열. 주면 수신측이 그 키들만 다시 조회해 부분 갱신(incremental refresh), 생략(`undefined`) 하면 전체 리로드.
|
|
173
|
+
- 호출 위치는 변경 트랜잭션이 커밋된 뒤(= `connectAsync` 콜백 밖). 변경과 통지가 한 사용자 동작 안에서 이어짐.
|
|
174
|
+
- `register` 안 한 이름을 넘기면 throw — 등록한 이름과 일치시킬 것.
|
|
175
|
+
|
|
158
176
|
## 선택 컨트롤에서 관리·선택 모달 띄우기
|
|
159
177
|
|
|
160
178
|
공유데이터 선택 컨트롤(`sd-shared-data-select` · `sd-shared-data-select-list`)은 그 자리에서 해당 마스터를 관리·선택하는 모달을 여는 입력을 가짐. 마스터 목록 화면(`sd-crud-list` 기반)을 모달로 재사용해, 선택 컨트롤 옆에서 등록·수정·선택을 끝낼 수 있음.
|
|
@@ -235,4 +253,5 @@ constructor() {
|
|
|
235
253
|
- 항목 추가 시 세 곳(`register` · `TAppSharedData` · 인터페이스)을 모두 갱신. 하나라도 빠지면 타입 불일치 또는 미등록 데이터가 됨.
|
|
236
254
|
- select 결과에 매직 필드(`__valueKey` · `__searchText` · `__isHidden`)를 빠짐없이 포함.
|
|
237
255
|
- `changeKeys` 분기를 생략하지 않음 — incremental refresh 가 동작하지 않으면 변경 시 전체 재조회가 됨.
|
|
256
|
+
- 마스터 CRUD 후 `emitAsync` 통지를 빠뜨리지 않음 — 통지가 없으면 다른 화면의 공유 시그널이 옛 데이터를 유지함.
|
|
238
257
|
- 공유데이터는 서버 연결을 전제하므로 프리렌더(SSG) 대상 화면의 초기화 경로에서 사용 금지 — 제약은 [client-ssg.md](./client-ssg.md) 참조.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# 클라이언트 시스템 설정 영속화 매뉴얼
|
|
2
|
+
|
|
3
|
+
클라이언트(Angular) 의 사용자별 UI·시스템 설정(시트 컬럼 너비/순서/숨김, 모달 크기·위치 등) 을 저장·복원하려 할 때 참조.
|
|
4
|
+
|
|
5
|
+
`SdSystemConfigProvider`(`@simplysm/angular`, `providedIn: "root"`) 가 그 통로. `setAsync(key, data)` / `getAsync(key)` 로 키-값을 저장·조회하며, 프레임워크 컴포넌트(`sd-sheet` 컬럼 설정·`sd-modal` 크기 등) 가 이를 통해 상태를 보존함.
|
|
6
|
+
|
|
7
|
+
저장 위치는 앱이 `fn` 을 배선했는지로 갈림:
|
|
8
|
+
|
|
9
|
+
- `fn` **미배선(기본)** — 브라우저 `localStorage`. 그 기기·브라우저에만 남음.
|
|
10
|
+
- `fn` **배선** — 앱이 준 `set`/`get` 으로 위임. 서버 DB 에 사용자별로 저장하면 기기가 바뀌어도 설정이 유지됨.
|
|
11
|
+
|
|
12
|
+
## 사용자별로 서버(DB)에 저장하려면
|
|
13
|
+
|
|
14
|
+
`provideAppInitializer` 안에서 `SdSystemConfigProvider.fn` 에 `set`/`get` 을 할당. 로그인 사용자(employee) 별로 설정을 DB 에 저장·조회.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { inject, provideAppInitializer } from "@angular/core";
|
|
18
|
+
import { json } from "@simplysm/core-common";
|
|
19
|
+
import { expr } from "@simplysm/orm-common";
|
|
20
|
+
import { SdSystemConfigProvider } from "@simplysm/angular";
|
|
21
|
+
|
|
22
|
+
provideAppInitializer(() => {
|
|
23
|
+
const sdSystemConfig = inject(SdSystemConfigProvider);
|
|
24
|
+
const appAuth = inject(AppAuthProvider);
|
|
25
|
+
const appOrm = inject(AppOrmProvider);
|
|
26
|
+
|
|
27
|
+
sdSystemConfig.fn = {
|
|
28
|
+
set: async (key, val) => {
|
|
29
|
+
const employeeId = appAuth.authInfo()?.employeeId;
|
|
30
|
+
if (employeeId == null) return; // 로그인 전에는 저장하지 않음
|
|
31
|
+
|
|
32
|
+
await appOrm.connectAsync(async (db) => {
|
|
33
|
+
await db
|
|
34
|
+
.employeeConfig()
|
|
35
|
+
.where((item) => [expr.eq(item.employeeId, employeeId), expr.eq(item.code, key)])
|
|
36
|
+
.upsert(
|
|
37
|
+
() => ({ valueJson: json.stringify(val) }),
|
|
38
|
+
(updateRecord) => ({ ...updateRecord, employeeId, code: key }),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
get: async (key) => {
|
|
43
|
+
const employeeId = appAuth.authInfo()?.employeeId;
|
|
44
|
+
if (employeeId == null) return; // 로그인 전에는 조회 불가 → undefined
|
|
45
|
+
|
|
46
|
+
return appOrm.connectAsync(async (db) => {
|
|
47
|
+
const row = await db
|
|
48
|
+
.employeeConfig()
|
|
49
|
+
.where((item) => [expr.eq(item.employeeId, employeeId), expr.eq(item.code, key)])
|
|
50
|
+
.single();
|
|
51
|
+
return row?.valueJson != null ? json.parse(row.valueJson) : undefined;
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `key` 는 설정 항목 식별자(예: 시트 키), 값은 `json.stringify` 로 저장하고 `get` 에서 `json.parse` 로 복원.
|
|
59
|
+
- 로그인 전(`employeeId == null`) 에는 `set` 을 건너뛰고 `get` 은 `undefined` 반환 — 인증 사용자별 설정이라 비로그인 상태로 저장하지 않음.
|
|
60
|
+
- DB 에 둘 땐 `(employeeId, code)` 로 항목을 식별하는 사용자별 설정 테이블이 필요 — 스키마 정의는 [orm.md](./orm.md).
|
|
61
|
+
|
|
62
|
+
## 지킬 것
|
|
63
|
+
|
|
64
|
+
- `fn` 은 부트스트랩(`provideAppInitializer`) 에서 1회만 할당. 화면·서비스 코드에서 재할당하지 않음.
|
|
65
|
+
- `fn` 미배선이면 자동으로 `localStorage` 폴백 — 기기 로컬 저장으로 충분하면 배선이 불필요.
|
|
66
|
+
- 설정값은 `json` 으로 직렬화/역직렬화하여 컴포넌트가 넘긴 객체를 그대로 보존.
|
|
@@ -101,7 +101,7 @@ WHERE 와 SELECT 양쪽에서 동일 도출 산식을 쓰겠다고 `buildDerived
|
|
|
101
101
|
|
|
102
102
|
## 삭제 전략
|
|
103
103
|
|
|
104
|
-
- **기초정보(마스터)**: soft delete (`
|
|
104
|
+
- **기초정보(마스터)**: soft delete (`isDeleted` 등) 사용. FK 참조 무결성 보존.
|
|
105
105
|
- **프로세스 문서(트랜잭션)**: 물리 delete. 상세 행을 포함해 캐스케이드. 단, 다른 테이블이 FK 로 참조 중이면 삭제를 차단하고 최종 사용자에게 toast 등으로 사유 안내.
|
|
106
106
|
|
|
107
107
|
## 유니크 전략 (활성 유니크 vs 완전 유니크)
|
|
@@ -4,20 +4,18 @@ Claude 에이전트가 코드 작성·설계·변경 시 따라야 할 행동
|
|
|
4
4
|
|
|
5
5
|
## 인터페이스 설계 시
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
- 정확한 구현 부담이 크면 단순화안을 사용자에게 보고 후 사용자 합의에 따름.
|
|
7
|
+
사용자가 접하는 면(공개 API·props·옵션·UI·출력)은 단순하고 정의에 충실하게 유지. 그 대가로 내부 구현이 복잡해지는 것은 당연히 떠안음 — 사용자 직관이 구현 편의보다 우선.
|
|
8
|
+
|
|
9
|
+
- 사용자 면의 단순함이 내부 구현의 단순함보다 우선. 사용자가 직관적으로 쓰게 하려면 구현이 복잡해져야 하며, 이는 회피 대상이 아니라 당연한 일.
|
|
10
|
+
- **구현이 복잡하다·부담된다는 사실은 기능을 빼거나, 사용자 요구를 완화·근사화하거나, 명시된 정의를 임의로 단순화하거나, 우회하자고 제안할 근거가 될 수 없음.** 구현 복잡도에 대한 불안을 결정 근거로 쓰지 않음.
|
|
11
|
+
- "어렵다" 와 "불가능하다" 를 구분 — 어려우면 그냥 구현. 정말 불가능할 때만 그 사실을 중립적으로 보고("X 때문에 불가, 원인 Y")하고, 단순화·축소 여부는 사용자가 결정. "안 하자·우회하자" 식 추천 금지.
|
|
12
|
+
- 나쁜 예: "구현이 복잡해지니 이 기능은 빼거나 A 방식으로 우회하는 게 어떨까요?"
|
|
13
|
+
- 좋은 예: 복잡해도 정의대로 구현. 진짜 불가능하면 원인만 사실 보고.
|
|
15
14
|
|
|
16
15
|
## 불필요한 래핑·추상화 금지
|
|
17
16
|
|
|
18
|
-
API·함수가 단순 입력(리터럴·기본값·직접 인자)을 그대로 받으면 그대로 전달. "타입 안전"·"방어" 등을 명분으로 래핑·변환·간접층을 덧대지 말
|
|
17
|
+
API·함수가 단순 입력(리터럴·기본값·직접 인자)을 그대로 받으면 그대로 전달. "타입 안전"·"방어" 등을 명분으로 래핑·변환·간접층을 덧대지 말 것 — 호출부 가독성을 해치고, 읽는 사람이 타입에 문제가 있나 의심하게 만듦.
|
|
19
18
|
|
|
20
|
-
- 입력 타입이 단순 값을 허용하는데 래퍼로 감싸는 것 금지 — 호출부 가독성을 해치고, 읽는 사람이 타입에 문제가 있나 의심하게 만듦.
|
|
21
19
|
- 래핑·변환은 타입이 래퍼만 받는 등 실제로 요구되는 자리에서만 사용.
|
|
22
20
|
|
|
23
21
|
- 나쁜 예: 입력 타입이 `string | Wrapper<string>` 인데 항상 `wrap("재고", ...)` 로 감싸 전달.
|
|
@@ -42,14 +40,12 @@ API·함수가 단순 입력(리터럴·기본값·직접 인자)을 그대로
|
|
|
42
40
|
|
|
43
41
|
## 라이브러리 우회 금지
|
|
44
42
|
|
|
45
|
-
- 의존 라이브러리 동작에 이상이
|
|
46
|
-
|
|
47
|
-
- 우회 코드 작성 금지:
|
|
48
|
-
- 불가피할 경우 사용자에게 보고 후 사용자 결정에 따름.
|
|
43
|
+
- 의존 라이브러리 동작에 이상이 있으면, 우회 코드를 짜기 전에 라이브러리 측 원인부터 조사. 버그·누락으로 판단되면 사용자에게 보고 후 수정 방법 또는 이슈 발행을 제안.
|
|
44
|
+
- 우회 코드는 작성하지 않음. 불가피하면 사용자에게 보고 후 그 결정에 따름.
|
|
49
45
|
|
|
50
46
|
## 에러 처리 시 throw 원칙
|
|
51
47
|
|
|
52
|
-
|
|
48
|
+
코드에서 예외·오류 상황을 만나면 throw:
|
|
53
49
|
|
|
54
50
|
- silent skip 금지 — 예외를 잡은 후 대안 없이 진행하면 후속 프로세스가 결손된 채 동작함.
|
|
55
51
|
- **자동 복구** (예: 의존성 미설치 → 설치·재시도 = 완전한 동작 회복) 는 silent skip 아님.
|
|
@@ -73,8 +69,11 @@ API·함수가 단순 입력(리터럴·기본값·직접 인자)을 그대로
|
|
|
73
69
|
|
|
74
70
|
사용자에게 노출되는 알림(로그·토스트·다이얼로그 등)의 심각도 분류 기준:
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
| 심각도 | 분류 기준 |
|
|
73
|
+
| --------------- | ----------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `error`(danger) | 문제 발생. 예외를 잡았는지·무시했는지·재시도했는지 여부와 무관하게 "문제가 일어난 사실" 이면 전부 해당. |
|
|
75
|
+
| `warn` | 문제는 아니지만 사용자가 인지해야 할 중요한 알림. |
|
|
76
|
+
| `info` | 알면 좋은 일반 알림. |
|
|
77
|
+
| `success` | 정상 완료 알림. |
|
|
78
|
+
|
|
80
79
|
- 안티패턴: 중단 없이 복구가 되었다는 이유로 `error` 대신 `warn` 을 선택 — 분류 기준 오용.
|