@simplysm/sd-claude 14.0.89 → 14.0.91

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.
Files changed (79) hide show
  1. package/claude/references/sd-simplysm14/README.md +16 -17
  2. package/claude/references/sd-simplysm14/apis/angular/README.md +52 -30
  3. package/claude/references/sd-simplysm14/apis/angular/controls.md +200 -38
  4. package/claude/references/sd-simplysm14/apis/angular/crud.md +41 -53
  5. package/claude/references/sd-simplysm14/apis/angular/directives.md +66 -22
  6. package/claude/references/sd-simplysm14/apis/angular/features.md +127 -40
  7. package/claude/references/sd-simplysm14/apis/angular/infra.md +60 -43
  8. package/claude/references/sd-simplysm14/apis/angular/layout.md +56 -20
  9. package/claude/references/sd-simplysm14/apis/angular/overlay.md +74 -74
  10. package/claude/references/sd-simplysm14/apis/angular/routing-appstructure.md +50 -40
  11. package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +55 -15
  12. package/claude/references/sd-simplysm14/apis/angular/shared-data.md +59 -42
  13. package/claude/references/sd-simplysm14/apis/angular/sheet.md +77 -62
  14. package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +8 -7
  15. package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +71 -43
  16. package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +22 -14
  17. package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +19 -19
  18. package/claude/references/sd-simplysm14/apis/core-browser/README.md +17 -17
  19. package/claude/references/sd-simplysm14/apis/core-browser/dom-element.md +28 -28
  20. package/claude/references/sd-simplysm14/apis/core-browser/indexed-db.md +37 -37
  21. package/claude/references/sd-simplysm14/apis/core-common/README.md +87 -219
  22. package/claude/references/sd-simplysm14/apis/core-common/array-ext.md +54 -98
  23. package/claude/references/sd-simplysm14/apis/core-common/async-runtime.md +57 -99
  24. package/claude/references/sd-simplysm14/apis/core-common/datetime.md +60 -103
  25. package/claude/references/sd-simplysm14/apis/core-common/errors.md +42 -47
  26. package/claude/references/sd-simplysm14/apis/core-common/obj.md +42 -88
  27. package/claude/references/sd-simplysm14/apis/core-common/serialization.md +55 -0
  28. package/claude/references/sd-simplysm14/apis/core-node/README.md +6 -7
  29. package/claude/references/sd-simplysm14/apis/core-node/consola.md +17 -12
  30. package/claude/references/sd-simplysm14/apis/core-node/cpx.md +14 -13
  31. package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +9 -8
  32. package/claude/references/sd-simplysm14/apis/core-node/fsx.md +14 -13
  33. package/claude/references/sd-simplysm14/apis/core-node/pathx.md +4 -8
  34. package/claude/references/sd-simplysm14/apis/core-node/worker.md +14 -12
  35. package/claude/references/sd-simplysm14/apis/excel/README.md +22 -22
  36. package/claude/references/sd-simplysm14/apis/excel/cell.md +37 -29
  37. package/claude/references/sd-simplysm14/apis/excel/conditional-format.md +29 -15
  38. package/claude/references/sd-simplysm14/apis/excel/style.md +33 -27
  39. package/claude/references/sd-simplysm14/apis/excel/utils.md +29 -19
  40. package/claude/references/sd-simplysm14/apis/excel/workbook-worksheet.md +78 -55
  41. package/claude/references/sd-simplysm14/apis/excel/wrapper.md +42 -45
  42. package/claude/references/sd-simplysm14/apis/orm-common/README.md +6 -8
  43. package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +118 -67
  44. package/claude/references/sd-simplysm14/apis/orm-common/expr.md +83 -86
  45. package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +102 -93
  46. package/claude/references/sd-simplysm14/apis/orm-common/schema.md +138 -81
  47. package/claude/references/sd-simplysm14/apis/orm-common/types.md +49 -44
  48. package/claude/references/sd-simplysm14/apis/orm-node/README.md +42 -42
  49. package/claude/references/sd-simplysm14/apis/orm-node/db-conn.md +44 -33
  50. package/claude/references/sd-simplysm14/apis/sd-cli/README.md +11 -10
  51. package/claude/references/sd-simplysm14/apis/service-client/README.md +56 -52
  52. package/claude/references/sd-simplysm14/apis/service-client/orm.md +33 -28
  53. package/claude/references/sd-simplysm14/apis/service-client/transport.md +23 -21
  54. package/claude/references/sd-simplysm14/apis/service-common/README.md +83 -48
  55. package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +126 -34
  56. package/claude/references/sd-simplysm14/apis/service-common/protocol.md +109 -54
  57. package/claude/references/sd-simplysm14/apis/service-server/README.md +69 -81
  58. package/claude/references/sd-simplysm14/apis/service-server/service-authoring.md +46 -43
  59. package/claude/references/sd-simplysm14/apis/service-server/transport-internals.md +63 -37
  60. package/claude/references/sd-simplysm14/apis/service-server/v1-legacy.md +40 -30
  61. package/claude/references/sd-simplysm14/apis/storage/README.md +17 -17
  62. package/claude/references/sd-simplysm14/manuals/client-app-structure.md +135 -140
  63. package/claude/references/sd-simplysm14/manuals/client-orm.md +1 -1
  64. package/claude/references/sd-simplysm14/manuals/client-service.md +19 -7
  65. package/claude/references/sd-simplysm14/manuals/client-shared-data.md +2 -2
  66. package/claude/references/sd-simplysm14/manuals/client-system-log.md +16 -4
  67. package/claude/references/sd-simplysm14/manuals/data-log.md +0 -1
  68. package/claude/references/sd-simplysm14/manuals/orm.md +16 -0
  69. package/claude/rules/sd-design-rules.md +10 -0
  70. package/claude/skills/sd-demo/SKILL.md +0 -6
  71. package/claude/skills/sd-docs/SKILL.md +60 -0
  72. package/claude/{workflows/sd-docs.rules.md → skills/sd-docs/references/subagent-prompt.md} +118 -103
  73. package/claude/skills/sd-impl/SKILL.md +7 -4
  74. package/claude/skills/sd-spec/SKILL.md +842 -15
  75. package/claude/skills/sd-spec/references/example-spec.md +26 -36
  76. package/package.json +1 -1
  77. package/claude/references/sd-simplysm14/apis/core-common/json-transfer.md +0 -53
  78. package/claude/skills/sd-spec/references/spec-authoring.md +0 -519
  79. package/claude/workflows/sd-docs.js +0 -84
@@ -17,9 +17,9 @@ FTP / FTPS / SFTP 원격 스토리지에 연결해 파일·디렉토리를 업
17
17
  type StorageProtocol = "ftp" | "ftps" | "sftp";
18
18
  ```
19
19
 
20
- - `"ftp"` — 평문 FTP. 보안 채널 없이 접속(`basic-ftp` `secure: false`). 내부망·테스트 환경에서만 권장.
21
- - `"ftps"` — TLS 로 암호화된 FTP(`basic-ftp` `secure: true`). 외부망 FTP 접속 시.
22
- - `"sftp"` — SSH 기반 SFTP. password 미지정 시 `~/.ssh/id_ed25519` 키 + SSH agent 로 인증.
20
+ - `"ftp"` — 평문 FTP. 보안 채널 없이 접속(`FtpStorageClient` `secure=false` 로 생성). 내부망·테스트 환경에서만 권장.
21
+ - `"ftps"` — TLS 로 암호화된 FTP(`FtpStorageClient` `secure=true` 로 생성). 외부망 FTP 접속 시.
22
+ - `"sftp"` — SSH 기반 SFTP(`SftpStorageClient`). password 미지정 시 `~/.ssh/id_ed25519` 키 + SSH agent 로 인증. 가장 일반적인 보안 전송.
23
23
 
24
24
  ## StorageConnConfig
25
25
 
@@ -28,9 +28,9 @@ interface StorageConnConfig { host: string; port?: number; user?: string; passwo
28
28
  ```
29
29
 
30
30
  - `host: string` — 접속 대상 서버 호스트명 또는 IP. 필수.
31
- - `port?: number` — 접속 포트. 미지정 시 각 라이브러리 기본값(FTP 21, SFTP 22) 사용.
31
+ - `port?: number` — 접속 포트. 미지정 시 각 라이브러리 기본값(FTP 21, SFTP 22) 사용. 비표준 포트면 명시.
32
32
  - `user?: string` — 로그인 사용자명. 미지정 시 익명/기본 사용자.
33
- - `password?: string` — 로그인 비밀번호. **SFTP 에서 이 값이 `null`/미지정이면** password 인증 대신 `~/.ssh/id_ed25519` 개인키와 SSH agent(`SSH_AUTH_SOCK` 환경변수가 설정된 경우 `agent` 옵션) 로 인증 시도하고, 키 파싱 실패(암호화 키 등) 시 agent 단독으로 재시도. FTP/FTPS 에서는 미지정 시 라이브러리 기본(익명) 처리.
33
+ - `password?: string` — 로그인 비밀번호. **SFTP 에서 이 값이 `null`/미지정이면** password 인증 대신 `~/.ssh/id_ed25519` 개인키와 SSH agent(`SSH_AUTH_SOCK` 환경변수가 설정된 경우 `agent` 옵션) 로 인증 시도하고, 키 파싱 실패(암호화 키 등) 시 agent 단독으로 재시도. FTP/FTPS 에서는 미지정 시 라이브러리 기본(익명) 처리. 키 인증을 쓰려면 password 를 넘기지 말 것.
34
34
 
35
35
  ## StorageFactory
36
36
 
@@ -46,9 +46,9 @@ class StorageFactory {
46
46
  }
47
47
  ```
48
48
 
49
- - `type: StorageProtocol` — 사용할 프로토콜. `"sftp"` → `SftpStorageClient`, `"ftps"` → `FtpStorageClient(secure=true)`, `"ftp"` → `FtpStorageClient(secure=false)` 를 내부 생성.
50
- - `config: StorageConnConfig` — 접속 설정.
51
- - `fn: (storage: StorageClient) => R | Promise<R>` — 연결된 `StorageClient` 를 받아 파일 작업을 수행하는 콜백. 반환값이 그대로 `connect` 의 결과(`Promise<R>`) 가 됨.
49
+ - `type: StorageProtocol` — 사용할 프로토콜. `"sftp"` → `SftpStorageClient`, `"ftps"` → `FtpStorageClient(secure=true)`, `"ftp"` → `FtpStorageClient(secure=false)` 를 내부 생성. 보안 전송이 필요하면 `"ftps"` 또는 `"sftp"`.
50
+ - `config: StorageConnConfig` — 접속 설정. 위 StorageConnConfig 참조.
51
+ - `fn: (storage: StorageClient) => R | Promise<R>` — 연결된 `StorageClient` 를 받아 파일 작업을 수행하는 콜백. 반환값이 그대로 `connect` 의 결과(`Promise<R>`) 가 됨. 동기·비동기 모두 허용.
52
52
  - 동작: `client.connect()` 후 `fn` 실행, `finally` 에서 `client.close()` 호출하며 종료 오류는 무시(이미 종료된 경우 대비). 콜백에서 예외가 나도 연결은 반드시 닫히고 예외는 그대로 전파됨. 직접 클라이언트를 다루는 것보다 권장.
53
53
 
54
54
  ```ts
@@ -61,7 +61,7 @@ const names = await StorageFactory.connect("sftp", { host: "10.0.0.1", user: "u"
61
61
 
62
62
  ## StorageClient
63
63
 
64
- `connect` 콜백 안에서 받는 파일 조작 인터페이스. `FtpStorageClient`·`SftpStorageClient` 가 구현한다. 모든 메서드는 `Promise` 반환.
64
+ `connect` 콜백 안에서 받는 파일 조작 인터페이스. `FtpStorageClient`·`SftpStorageClient` 가 구현한다. 모든 메서드는 `Promise` 반환. 경로는 원격 기준 경로 문자열.
65
65
 
66
66
  ```ts
67
67
  interface StorageClient {
@@ -80,12 +80,12 @@ interface StorageClient {
80
80
 
81
81
  - `connect(config)` — 서버에 연결. 이미 연결된 인스턴스에서 재호출하면 `SdError` throw(먼저 `close()` 필요). `StorageFactory.connect` 사용 시 직접 호출 불필요. 연결 도중 실패하면 내부 라이브러리 연결을 닫고 예외를 다시 throw.
82
82
  - `mkdir(dirPath)` — 디렉토리 생성. 부모 디렉토리가 없으면 함께 생성(FTP `ensureDir`, SFTP 재귀 `mkdir`).
83
- - `rename(fromPath, toPath)` — 파일/디렉토리 경로 이동·이름 변경.
84
- - `list(dirPath)` — 디렉토리 내 항목을 `FileInfo[]` 로 반환.
83
+ - `rename(fromPath, toPath)` — 파일/디렉토리 경로 이동·이름 변경. `toPath` 로 옮기거나 새 이름을 부여할 때.
84
+ - `list(dirPath)` — 디렉토리 내 항목을 `FileInfo[]` 로 반환. 목록을 파일/폴더로 갈라 처리할 때.
85
85
  - `readFile(filePath)` — 원격 파일 전체를 `Bytes`(Uint8Array) 로 메모리에 다운로드(스트리밍 아님 — 큰 파일은 메모리 부담). 텍스트가 필요하면 호출 측에서 디코딩. SFTP 는 응답이 예상 타입(Buffer/string) 이 아니면 `SdError` throw.
86
- - `exists(filePath)` — 파일/디렉토리 존재 여부. **모든 예외(부모 없음·권한·네트워크 오류 포함) 에 대해 `false` 반환** — true/false 외 throw 없음. 따라서 `true` 만 "확실히 존재"로 신뢰한다. FTP 는 `size()` 로 파일을 O(1) 확인 후 실패 시 부모 디렉토리 목록으로 디렉토리 확인(슬래시 없는 경로는 루트 `/` 기준이라 항목 많은 디렉토리에서는 느려질 수 있음). SFTP 는 `exists()` 결과가 문자열(`'d'`/`'-'`/`'l'`)이면 존재로 판정.
87
- - `put(localPathOrBuffer, storageFilePath)` — 단일 파일 업로드. 첫 인자가 `string` 이면 로컬 파일 경로, `Bytes` 면 메모리 바이트를 업로드 대상으로 사용.
88
- - `uploadDir(fromPath, toPath)` — 로컬 디렉토리 전체를 원격 디렉토리로 재귀 업로드.
86
+ - `exists(filePath)` — 파일/디렉토리 존재 여부. **모든 예외(부모 없음·권한·네트워크 오류 포함) 에 대해 `false` 반환** — true/false 외 throw 없음. 따라서 `true` 만 "확실히 존재"로 신뢰한다. FTP 는 `size()` 로 파일을 O(1) 확인 후 실패 시 부모 디렉토리 목록으로 디렉토리 확인(슬래시 없는 경로는 루트 `/` 기준이라 항목 많은 디렉토리에서는 느려질 수 있음). SFTP 는 `exists()` 결과가 문자열(`'d'`/`'-'`/`'l'`)이면 존재로 판정. 작업 전 분기 확인에 사용.
87
+ - `put(localPathOrBuffer, storageFilePath)` — 단일 파일 업로드. 첫 인자가 `string` 이면 로컬 파일 경로에서, `Bytes` 면 메모리 바이트에서 업로드. 생성된 콘텐츠를 바로 올릴 땐 `Bytes` 로 전달.
88
+ - `uploadDir(fromPath, toPath)` — 로컬 디렉토리 전체를 원격 디렉토리로 재귀 업로드. 빌드 산출물 폴더 통째로 배포할 때.
89
89
  - `remove(filePath)` — 원격 파일 삭제.
90
90
  - `close()` — 연결 종료. 이미 종료/미연결 상태에서 호출해도 오류 없음. 종료 후 같은 인스턴스에서 `connect()` 로 재연결 가능. `StorageFactory.connect` 사용 시 직접 호출 불필요.
91
91
 
@@ -98,18 +98,18 @@ interface FileInfo { name: string; isFile: boolean; }
98
98
  ```
99
99
 
100
100
  - `name: string` — 항목 이름(파일명 또는 디렉토리명, 경로 아님).
101
- - `isFile: boolean` — 파일이면 `true`, 디렉토리면 `false`. 디렉토리 재귀 탐색 시 파일만 골라 처리하는 분기 기준으로 사용. SFTP 는 항목 type 이 `"-"` 인 경우만 `true`(디렉토리·심볼릭 링크는 `false`).
101
+ - `isFile: boolean` — 파일이면 `true`, 디렉토리면 `false`. 디렉토리 탐색 시 파일만 골라 처리하는 분기 기준으로 사용. SFTP 는 항목 type 이 `"-"` 인 경우만 `true`(디렉토리·심볼릭 링크는 `false`).
102
102
 
103
103
  ## FtpStorageClient / SftpStorageClient (직접 사용, 비권장)
104
104
 
105
- `StorageClient` 직접 구현체. 보통은 `StorageFactory.connect` 로 충분하며, 연결 수명을 콜백 밖에서 수동으로 다뤄야 할 때만 직접 생성한다.
105
+ `StorageClient` 직접 구현체. 보통은 `StorageFactory.connect` 로 충분하며, 연결 수명을 콜백 밖에서 수동으로 다뤄야 할 때만 직접 생성한다. 두 구현체 모두 Node 전용(`basic-ftp`, `ssh2-sftp-client`, `fs`/`os`/`path`/`stream` 의존)이라 브라우저에서 사용 불가.
106
106
 
107
107
  ```ts
108
108
  new FtpStorageClient(secure?: boolean) // secure=true → FTPS, 생략/false → 평문 FTP
109
109
  new SftpStorageClient()
110
110
  ```
111
111
 
112
- - `FtpStorageClient` 의 `secure` 생성자 인자 — `true` 면 TLS(FTPS), 생략/`false` 면 평문 FTP. (팩토리는 `ftps`→`true`, `ftp`→`false` 로 매핑.)
112
+ - `FtpStorageClient` 의 `secure` 생성자 인자 — `true` 면 TLS(FTPS), 생략/`false` 면 평문 FTP. 팩토리는 `ftps`→`true`, `ftp`→`false` 로 매핑.
113
113
  - `SftpStorageClient` 는 생성자 인자 없음. password 미지정 시 키/agent 인증 경로를 탄다(StorageConnConfig 의 `password` 풀이 참조).
114
114
  - 직접 사용 시 `connect()` → 작업 → `close()` 순으로 호출하고, 예외 발생 시에도 `close()` 가 호출되도록 `try/finally` 로 감쌀 것. 동일 인스턴스에서 `close()` 없이 `connect()` 를 재호출하면 연결 누수로 throw.
115
115
 
@@ -1,140 +1,135 @@
1
- # 앱 구조(AppStructure) 매뉴얼
2
-
3
- 앱의 메뉴·권한·기능 모듈을 한 군데서 정의하는 방법. 메뉴 트리, 화면 접근 권한, 모듈별 on/off 가 모두 이 구조 하나에서 나옴. 새 화면을 메뉴에 올리거나 권한을 거는 작업 시 참조.
4
-
5
- ## 1. 앱 구조 정의 위치
6
-
7
- common 패키지에 클라이언트별 `AppStructureItem[]` 상수를 두고, 앱 부트스트랩에서 `SdAppStructureProvider.initialize(items)` 로 연결함.
8
-
9
- ```ts
10
- // common/src/app-structure-items.ts
11
- import type { AppStructureItem } from "@simplysm/service-common";
12
- import { tablerBox } from "@ng-icons/tabler-icons"; // 아이콘은 @ng-icons 의 SVG 문자열 상수
13
-
14
- export const adminAppStructureItems: AppStructureItem[] = [
15
- /* ... */
16
- ];
17
- ```
18
-
19
- ```ts
20
- // 앱 부트스트랩 (main.ts)
21
- provideAppInitializer(async () => {
22
- const appService = inject(AppServiceProvider);
23
- const sdAppStructure = inject(SdAppStructureProvider);
24
-
25
- await appService.connectAsync();
26
- sdAppStructure.initialize(adminAppStructureItems); // 동기 await 불필요
27
- });
28
- ```
29
-
30
- - 한 서버가 여러 앱(admin·pda 등)을 서비스해도, **클라이언트마다 자기 배열만** 정의해 import.
31
- - 서버에 등록하지 않음 common 에서 클라이언트가 직접 import 함.
32
-
33
- ## 2. 메뉴 추가
34
-
35
- 배열에 항목을 추가하면 메뉴에 올라감. **그룹**(하위 메뉴를 묶음)과 **화면**(실제 라우팅 대상)으로 나뉨.
36
-
37
- ```ts
38
- export const adminAppStructureItems: AppStructureItem[] = [
39
- {
40
- title: "재고관리", // 그룹: children 보유
41
- code: "inventory",
42
- icon: tablerBox,
43
- children: [
44
- { title: "품목별 재고", code: "goods-inventory", perms: ["use"] }, // 화면(leaf)
45
- { title: "재고 실사", code: "stock-take", perms: ["use", "edit"] },
46
- ],
47
- },
48
- ];
49
- ```
50
-
51
- - `code` 부모부터 dot 으로 이어져 화면을 식별함 (위 예: `inventory.goods-inventory`). 라우팅 경로·권한 키가 모두 이 코드 기준.
52
- - 그룹은 `children` 두고 `perms`·`url` 을 두지 않음. 표시 가능한 자식이 하나도 없으면 그룹도 메뉴에서 자동으로 빠짐.
53
- - 외부 링크 화면은 `url` 지정.
54
-
55
- | 필드 | 위치 | 용도 |
56
- | ---------- | --------- | ------------------------------------------ |
57
- | `title` | 그룹·화면 | 메뉴에 표시할 이름 |
58
- | `code` | 그룹·화면 | 항목 코드 (부모와 dot 으로 이어 화면 식별) |
59
- | `icon` | 그룹·화면 | 메뉴 아이콘 |
60
- | `children` | 그룹 | 하위 항목 배열 |
61
- | `url` | 화면 | 외부 링크 이동 경로 |
62
-
63
- ## 3. 메뉴에 안 띄우고 화면만 두기
64
-
65
- 라우팅·내부 이동용이라 사이드 메뉴에는 노출하고 싶지 않은 화면은 `isNotMenu: true`.
66
-
67
- ```ts
68
- export const adminAppStructureItems: AppStructureItem[] = [
69
- { title: "메인메뉴", code: "main", isNotMenu: true }, // 홈/메인 화면
70
- { title: "내 정보 수정", code: "my-info", isNotMenu: true }, // 사용자 본인 정보 화면
71
-
72
- { title: "재고관리", code: "inventory", children: [ /* ... */ ] }, // 이하 실제 메뉴 그룹
73
- ];
74
- ```
75
-
76
- - 메뉴에서만 숨고, 화면(라우팅 대상) 자체는 그대로 존재함.
77
- - 홈(`main`)·내 정보 수정처럼 메뉴를 거치지 않고 직접 진입하는 화면은 배열 **맨 앞**에 root-level leaf(그룹·`children` 없이)로 모아두는 게 관례. 권한을 걸지 않으므로 `perms` 도 두지 않음.
78
-
79
- ## 4. 권한으로 접근 제한
80
-
81
- 권한을 걸려면 화면에 `perms` 를 정의함. 그러면 ① 권한 관리 페이지에 체크 항목으로 나오고 ② 권한 없는 사용자에게는 메뉴가 자동으로 숨겨지고 ③ 화면 안에서 권한 보유 여부를 체크할 수 있음.
82
-
83
- ```ts
84
- {
85
- title: "입고지시",
86
- code: "inbound-instruction",
87
- perms: ["use", "edit"],
88
- subPerms: [
89
- { code: "document", title: "문서작업", perms: ["edit"] }, // 화면 내 세부 권한
90
- ],
91
- },
92
- ```
93
-
94
- - `perms`: 부여할 권한 종류. `"use"`(조회) / `"edit"`(편집). `perms` 를 지정한 화면만 권한 페이지·권한 체크 대상이 됨.
95
- - `subPerms`: 한 화면 안의 세부 기능 권한.
96
-
97
- **권한을 사용자에게 부여** — 권한 관리 화면은 `getPermissionsByStructure(items)` 결과를 `<sd-permission-table>` 넘김. 저장한 결과는 사용자별 권한 레코드로 서버에 저장됨.
98
-
99
- ```ts
100
- permissions = computed(() =>
101
- this._sdAppStructure.getPermissionsByStructure(this._sdAppStructure.items()),
102
- );
103
- // template: <sd-permission-table [items]="permissions()" [(value)]="data" />
104
- ```
105
-
106
- **로그인 시 권한 연결** — 인증 후 사용자의 권한 레코드를 `permRecord` 에 set 하면 메뉴 필터·권한 체크에 반영됨.
107
-
108
- ```ts
109
- this._sdAppStructure.permRecord.set(this.authInfo()!.user.permissionRecord);
110
- ```
111
-
112
- **화면 안에서 권한 체크** — `injectPermsSignal` 로 현재 사용자의 활성 권한을 읽음.
113
-
114
- ```ts
115
- perms = injectPermsSignal(["base.user-permission"], ["use", "edit"]);
116
- canEdit = computed(() => this.perms().includes("edit"));
117
- // → 권한 없으면 빈 배열. 예: ["use"]
118
- ```
119
-
120
- - 인자는 화면의 fullCode(들), 둘째 인자는 확인할 권한 종류.
121
- - `perms` 를 정의하지 않은 화면은 제약이 없으므로 항상 모든 권한이 활성으로 나옴.
122
-
123
- ## 5. 기능 모듈로 메뉴 on/off
124
-
125
- 계약·라이선스 등으로 앱마다 켜고 끄는 기능 묶음이 있으면 `modules`/`requiredModules` 로 조건을 걸고, 앱에서 활성 모듈을 `usableModules` 에 set 함.
126
-
127
- ```ts
128
- { title: "스케쥴링", code: "scheduling", modules: ["scheduling"], children: [ /* ... */ ] },
129
- { title: "고급분석", code: "advanced", requiredModules: ["analytics", "pro"] },
130
- ```
131
-
132
- ```ts
133
- // 초기화 활성 모듈 지정
134
- this._sdAppStructure.usableModules.set(["scheduling"]);
135
- ```
136
-
137
- - `modules`: 나열한 모듈 중 **하나라도** 활성이면 표시(OR).
138
- - `requiredModules`: 나열한 모듈이 **모두** 활성이어야 표시(AND).
139
- - 조건을 건 항목은 해당 모듈이 `usableModules` 에 없으면 메뉴·권한에서 빠짐. 조건이 없는 항목은 모듈 설정과 무관하게 표시됨.
140
- - 모듈 기능을 쓰지 않는 앱은 `usableModules.set([])` 로 둠.
1
+ # 앱 구조(AppStructure) 매뉴얼
2
+
3
+ 앱의 메뉴·권한·기능 모듈을 한 군데서 정의하는 방법. 메뉴 트리, 화면 접근 권한, 모듈별 on/off 가 모두 이 구조 하나에서 나옴. 새 화면을 메뉴에 올리거나 권한을 거는 작업 시 참조.
4
+
5
+ ## 1. 앱 구조 정의 위치
6
+
7
+ common 패키지에 클라이언트별 `AppStructureItem[]` 상수를 두고, 앱 부트스트랩에서 `SdAppStructureProvider.initialize(items)` 로 연결함.
8
+
9
+ ```ts
10
+ // common/src/app-structure-items.ts
11
+ import type { AppStructureItem } from "@simplysm/service-common";
12
+ import { tablerBox } from "@ng-icons/tabler-icons"; // 아이콘은 @ng-icons 의 SVG 문자열 상수
13
+
14
+ export const adminAppStructureItems: AppStructureItem[] = [
15
+ /* ... */
16
+ ];
17
+ ```
18
+
19
+ ```ts
20
+ // 앱 부트스트랩 (main.ts)
21
+ provideAppInitializer(() => {
22
+ inject(SdAppStructureProvider).initialize(adminAppStructureItems);
23
+ });
24
+ ```
25
+
26
+ - 한 서버가 여러 앱(admin·pda 등) 서비스해도, **클라이언트마다 자기 배열만** 정의해 import.
27
+ - 서버에 등록하지 않음 — common 에서 클라이언트가 직접 import 함.
28
+
29
+ ## 2. 메뉴 추가
30
+
31
+ 배열에 항목을 추가하면 메뉴에 올라감. **그룹**(하위 메뉴를 묶음)과 **화면**(실제 라우팅 대상)으로 나뉨.
32
+
33
+ ```ts
34
+ export const adminAppStructureItems: AppStructureItem[] = [
35
+ {
36
+ title: "재고관리", // 그룹: children 보유
37
+ code: "inventory",
38
+ icon: tablerBox,
39
+ children: [
40
+ { title: "품목별 재고", code: "goods-inventory", perms: ["use"] }, // 화면(leaf)
41
+ { title: "재고 실사", code: "stock-take", perms: ["use", "edit"] },
42
+ ],
43
+ },
44
+ ];
45
+ ```
46
+
47
+ - `code` 는 부모부터 dot 으로 이어져 화면을 식별함 (위 예: `inventory.goods-inventory`). 라우팅 경로·권한 키가 모두 이 코드 기준.
48
+ - 그룹은 `children` 만 두고 `perms`·`url` 을 두지 않음. 표시 가능한 자식이 하나도 없으면 그룹도 메뉴에서 자동으로 빠짐.
49
+ - 외부 링크 화면은 `url` 지정.
50
+
51
+ | 필드 | 위치 | 용도 |
52
+ | ---------- | --------- | ------------------------------------------ |
53
+ | `title` | 그룹·화면 | 메뉴에 표시할 이름 |
54
+ | `code` | 그룹·화면 | 항목 코드 (부모와 dot 으로 이어 화면 식별) |
55
+ | `icon` | 그룹·화면 | 메뉴 아이콘 |
56
+ | `children` | 그룹 | 하위 항목 배열 |
57
+ | `url` | 화면 | 외부 링크 등 이동 경로 |
58
+
59
+ ## 3. 메뉴에 띄우고 화면만 두기
60
+
61
+ 라우팅·내부 이동용이라 사이드 메뉴에는 노출하고 싶지 않은 화면은 `isNotMenu: true`.
62
+
63
+ ```ts
64
+ export const adminAppStructureItems: AppStructureItem[] = [
65
+ { title: "메인메뉴", code: "main", isNotMenu: true }, // 홈/메인 화면
66
+ { title: "내 정보 수정", code: "my-info", isNotMenu: true }, // 사용자 본인 정보 화면
67
+
68
+ {
69
+ title: "재고관리",
70
+ code: "inventory",
71
+ children: [
72
+ /* ... */
73
+ ],
74
+ }, // 이하 실제 메뉴 그룹
75
+ ];
76
+ ```
77
+
78
+ - 메뉴에서만 숨고, 화면(라우팅 대상) 자체는 그대로 존재함.
79
+ - 홈(`main`)·내 정보 수정처럼 메뉴를 거치지 않고 직접 진입하는 화면은 배열 **맨 앞**에 root-level leaf(그룹·`children` 없이)로 모아두는 게 관례. 권한을 걸지 않으므로 `perms` 도 두지 않음.
80
+
81
+ ## 4. 권한으로 접근 제한
82
+
83
+ 권한을 걸려면 화면에 `perms` 를 정의함. 그러면 ① 권한 관리 페이지에 체크 항목으로 나오고 ② 권한 없는 사용자에게는 메뉴가 자동으로 숨겨지고 ③ 화면 안에서 권한 보유 여부를 체크할 수 있음.
84
+
85
+ ```ts
86
+ {
87
+ title: "입고지시",
88
+ code: "inbound-instruction",
89
+ perms: ["use", "edit"],
90
+ subPerms: [
91
+ { code: "document", title: "문서작업", perms: ["edit"] }, // 화면 내 세부 권한
92
+ ],
93
+ },
94
+ ```
95
+
96
+ - `perms`: 부여할 권한 종류. `"use"`(조회) / `"edit"`(편집). `perms` 를 지정한 화면만 권한 페이지·권한 체크 대상이 됨.
97
+ - `subPerms`: 화면 안의 세부 기능 권한.
98
+
99
+ **권한을 사용자에게 부여** — 권한 관리 화면은 `getPermissionsByStructure(items)` 결과를 `<sd-permission-table>` 에 넘김. 저장한 결과는 사용자별 권한 레코드로 서버에 저장됨.
100
+
101
+ ```ts
102
+ permissions = computed(() =>
103
+ this._sdAppStructure.getPermissionsByStructure(this._sdAppStructure.items()),
104
+ );
105
+ // template: <sd-permission-table [items]="permissions()" [(value)]="data" />
106
+ ```
107
+
108
+ **로그인 시 권한 연결** — 인증 후 사용자의 권한 레코드를 `permRecord` 에 set 하면 메뉴 필터·권한 체크에 반영됨.
109
+
110
+ ```ts
111
+ this._sdAppStructure.permRecord.set(this.authInfo()!.user.permissionRecord);
112
+ ```
113
+
114
+ **화면 안에서 권한 체크** — 화면 컴포넌트에서 `injectPermsSignal(<path>, <actions>)` 로 정의된 권한을 읽어 체크함. 첫 인자(권한 path)는 이 구조의 화면 fullCode(들). 체크 작성 관례(단순 체크는 인라인, `computed` 사용 기준)는 [client-component.md](./client-component.md) 의 '권한' 참조.
115
+
116
+ - `perms` 정의하지 않은 화면은 제약이 없으므로 항상 모든 권한이 활성으로 나옴.
117
+
118
+ ## 5. 기능 모듈로 메뉴 on/off
119
+
120
+ 계약·라이선스 등으로 앱마다 켜고 끄는 기능 묶음이 있으면 `modules`/`requiredModules` 로 조건을 걸고, 앱에서 활성 모듈을 `usableModules` 에 set 함.
121
+
122
+ ```ts
123
+ { title: "스케쥴링", code: "scheduling", modules: ["scheduling"], children: [ /* ... */ ] },
124
+ { title: "고급분석", code: "advanced", requiredModules: ["analytics", "pro"] },
125
+ ```
126
+
127
+ ```ts
128
+ // 초기화 활성 모듈 지정
129
+ this._sdAppStructure.usableModules.set(["scheduling"]);
130
+ ```
131
+
132
+ - `modules`: 나열한 모듈 중 **하나라도** 활성이면 표시(OR).
133
+ - `requiredModules`: 나열한 모듈이 **모두** 활성이어야 표시(AND).
134
+ - 조건을 건 항목은 해당 모듈이 `usableModules` 에 없으면 메뉴·권한에서 빠짐. 조건이 없는 항목은 모듈 설정과 무관하게 표시됨.
135
+ - 모듈 기능을 쓰지 않는 앱은 `usableModules.set([])` 로 둠.
@@ -19,7 +19,7 @@ export class AppOrmProvider {
19
19
  {
20
20
  DbClass: MainDbContext,
21
21
  connOpt: { configName: "MAIN" },
22
- dbContextOpt: { database: "...", schema: "dbo" },
22
+ dbContextOpt: { database: "..." },
23
23
  },
24
24
  callback,
25
25
  );
@@ -16,22 +16,21 @@ ORM 사용은 [client-orm.md](./client-orm.md), 이벤트 정의·발생 메커
16
16
  export class AppServiceProvider {
17
17
  private readonly _sdServiceClientFactory = inject(SdServiceClientFactoryProvider);
18
18
 
19
- private _orm?: OrmClientConnector;
20
- private _user?: ServiceProxy<UserServiceMethods>;
21
- private _authInfoEvent?: ClientEventProxy<typeof AuthInfoEvent>;
22
-
23
19
  get client() {
24
20
  return this._sdServiceClientFactory.get("MAIN");
25
21
  }
26
22
 
23
+ private _orm?: OrmClientConnector;
27
24
  get orm(): OrmClientConnector {
28
25
  return (this._orm ??= createOrmClientConnector(this.client));
29
26
  }
30
27
 
28
+ private _user?: ServiceProxy<UserServiceMethods>;
31
29
  get user(): ServiceProxy<UserServiceMethods> {
32
30
  return (this._user ??= this.client.getService<UserServiceMethods>("User"));
33
31
  }
34
32
 
33
+ private _authInfoEvent?: ClientEventProxy<typeof AuthInfoEvent>;
35
34
  get authInfoEvent(): ClientEventProxy<typeof AuthInfoEvent> {
36
35
  return (this._authInfoEvent ??= this.client.getEvent(AuthInfoEvent));
37
36
  }
@@ -49,6 +48,19 @@ export class AppServiceProvider {
49
48
  - `orm` getter — `createOrmClientConnector(this.client)` 결과. DB 설정을 얹는 `AppOrmProvider` 가 이 위에 올라감 ([client-orm.md](./client-orm.md)).
50
49
  - `connectAsync()` — 앱 부트스트랩 시점에 서버 연결 수행. `addListener` 등 통신은 이 호출 이후에만 가능.
51
50
 
51
+ ## 부트스트랩에서 서버에 연결하려면
52
+
53
+ `provideAppInitializer` 안에서 `AppServiceProvider.connectAsync()` 를 호출하고 그 Promise 를 반환. Angular 가 이 Promise 를 기다린 뒤 앱을 띄우므로, 화면·프로바이더가 통신을 시작하는 시점에는 연결이 이미 끝나 있음.
54
+
55
+ ```ts
56
+ // 앱 부트스트랩 (main.ts)
57
+ provideAppInitializer(async () => {
58
+ await inject(AppServiceProvider).connectAsync();
59
+ });
60
+ ```
61
+
62
+ - `connectAsync()` 의 Promise 를 **반환**해야 Angular 가 연결 완료까지 부트스트랩을 대기. 반환을 빠뜨리면 연결 전에 화면이 떠 통신 호출이 실패함.
63
+
52
64
  ## 새 서비스 호출을 추가하려면
53
65
 
54
66
  `client.getService<XxxServiceMethods>("XxxName")` 결과를 캐시 필드 + getter 로 노출.
@@ -56,7 +68,7 @@ export class AppServiceProvider {
56
68
  ```ts
57
69
  @Injectable({ providedIn: "root" })
58
70
  export class AppServiceProvider {
59
- // ... 기존 필드 ...
71
+ // ... 기존 멤버 ...
60
72
  private _order?: ServiceProxy<OrderServiceMethods>;
61
73
 
62
74
  get order(): ServiceProxy<OrderServiceMethods> {
@@ -76,7 +88,7 @@ export class AppServiceProvider {
76
88
  ```ts
77
89
  @Injectable({ providedIn: "root" })
78
90
  export class AppServiceProvider {
79
- // ... 기존 필드 ...
91
+ // ... 기존 멤버 ...
80
92
  private _orderStatusChangedEvent?: ClientEventProxy<typeof OrderStatusChangedEvent>;
81
93
 
82
94
  get orderStatusChangedEvent(): ClientEventProxy<typeof OrderStatusChangedEvent> {
@@ -91,6 +103,6 @@ export class AppServiceProvider {
91
103
 
92
104
  ## 지킬 것
93
105
 
94
- - 캐시 필드는 `private _xxx?`, 노출은 getter, 초기화는 `??=` 항목마다 동일 패턴 유지.
106
+ - 캐시 필드(`private _xxx?`)와 getter(`??=`)를 항목별로 인접 배치하고, 항목마다 동일 패턴 유지.
95
107
  - 서비스 이름·이벤트 정의 객체는 단일 소스(server `defineService` 이름 / 공통 `defineEvent` 객체)를 그대로 따름. 호출부에서 문자열·제네릭을 중복으로 적지 않음.
96
108
  - `connectAsync()` 이전에는 통신 호출 불가 — 부트스트랩 순서 준수.
@@ -34,7 +34,7 @@ export class AppSharedDataProvider extends SdSharedDataProvider<TAppSharedData>
34
34
  isDisabled: item.isDisabled,
35
35
 
36
36
  __valueKey: item.id,
37
- __searchText: expr.concat(item.code, "|_|", item.name),
37
+ __searchText: expr.concat(item.code, "|", item.name),
38
38
  __isHidden: item.isDisabled,
39
39
  }));
40
40
 
@@ -85,7 +85,7 @@ this.register("품목", {
85
85
  isDisabled: item.isDisabled,
86
86
 
87
87
  __valueKey: item.id,
88
- __searchText: expr.concat(item.code, "|_|", item.name),
88
+ __searchText: expr.concat(item.code, "|", item.name),
89
89
  __isHidden: item.isDisabled,
90
90
  }));
91
91
 
@@ -36,21 +36,33 @@ export const SystemLog = Table("SystemLog")
36
36
 
37
37
  ## 부트스트랩에서 외부 적재를 배선하려면
38
38
 
39
- `provideAppInitializer` 안에서 `SdSystemLogProvider.writeFn` 에 적재 함수를 할당. 트랜잭션이 필요 없는 단순 insert 이므로 `connectWithoutTransAsync` 를 사용.
39
+ `provideAppInitializer` 안에서 `SdSystemLogProvider.writeFn` 에 적재 함수를 할당.
40
40
 
41
41
  ```ts
42
+ import { inject, provideAppInitializer } from "@angular/core";
43
+ import { DateTime, json } from "@simplysm/core-common";
44
+ import { SdSystemLogProvider } from "@simplysm/angular";
45
+
42
46
  provideAppInitializer(() => {
43
47
  const appService = inject(AppServiceProvider);
44
48
  const appOrm = inject(AppOrmProvider);
45
49
  const appAuth = inject(AppAuthProvider);
46
50
 
47
51
  inject(SdSystemLogProvider).writeFn = async (severity, ...data) => {
48
- await appOrm.connectWithoutTransAsync(async (db) => {
52
+ await appOrm.connectAsync(async (db) => {
49
53
  await db.systemLog().insert([
50
54
  {
51
55
  dateTime: new DateTime(),
52
56
  severity,
53
- message: JSON.stringify(data),
57
+ message: data
58
+ .map((l) =>
59
+ typeof l === "string"
60
+ ? l
61
+ : l instanceof Error
62
+ ? (l.stack ?? l.message)
63
+ : json.stringify(l, { space: 2 }),
64
+ )
65
+ .join(" "),
54
66
  clientName: CLIENT_NAME,
55
67
  employeeId: appAuth.authInfo()?.employeeId,
56
68
  },
@@ -63,7 +75,7 @@ provideAppInitializer(() => {
63
75
  ```
64
76
 
65
77
  - `CLIENT_NAME` 은 `provideSdAngular({ clientName: CLIENT_NAME })` 에 넘긴 값과 동일하게 두어 어느 앱에서 난 로그인지 구분.
66
- - `data` 는 가변 인자 배열이므로 `JSON.stringify(data)` 로 통째로 저장.
78
+ - `data` 는 가변 인자 배열. 각 인자를 string→그대로 / Error→`stack` / 객체→`json.stringify` 로 문자열화해 공백으로 join 하여 저장(Error 의 `stack` 보존).
67
79
  - `writeFn` 미설정 시 외부 적재는 일어나지 않고 콘솔 출력만 수행됨. DB 적재가 필요한 앱에서만 배선.
68
80
 
69
81
  ## 자동으로 적재되는 로그
@@ -137,7 +137,6 @@ Queryable.prototype.joinLastDataLog = function (this: Queryable<any, any>, opts)
137
137
 
138
138
  ```ts
139
139
  // main.db-context.ts
140
- import "./system-data-log.ext"; // side-effect: Queryable.prototype 확장 로드 보장
141
140
  // ...
142
141
  export class MainDbContext extends DbContext {
143
142
  role = this.queryable(Role);
@@ -61,6 +61,22 @@ WHERE 와 SELECT 양쪽에서 동일 도출 산식을 쓰겠다고 `buildDerived
61
61
 
62
62
  "Layer 1 = materialize, Layer 2 = derive" 같은 다층 wrap 구조도 군더더기. 단일 select 안에서 로컬 `const` 로 산식을 분리하면 동일한 SQL 이 생성됨.
63
63
 
64
+ ### 불필요한 `expr.val` 사용 금지
65
+
66
+ `where` 비교·`update`/`upsert`/`insert` 값은 `ExprInput`(= `ExprUnit | T`) 자리라 리터럴을 그대로 넘김 — `expr.val` 로 감싸지 말 것.
67
+
68
+ ```ts
69
+ // 나쁜 예 — 불필요한 래핑
70
+ .update((u) => ({ name: expr.val("string", "새이름") }))
71
+ .where((u) => [expr.eq(u.status, expr.val("string", "active"))])
72
+
73
+ // 좋은 예 — 리터럴 직접 전달
74
+ .update((u) => ({ name: "새이름" }))
75
+ .where((u) => [expr.eq(u.status, "active")])
76
+ ```
77
+
78
+ `expr.val` 은 `select` 콜백에서 리터럴 상수 컬럼을 만들 때처럼 `ExprUnit` 이 요구되는 자리에서만 사용.
79
+
64
80
  ## 스키마 정의
65
81
 
66
82
  컬럼은 `NOT NULL` 기본. `.nullable()` / `.default(...)` 는 도메인 근거가 있을 때만 사용.
@@ -13,6 +13,16 @@ Claude 에이전트가 코드 작성·설계·변경 시 따라야 할 행동
13
13
  - 명시된 정의 자체를 임의로 단순화하여 처리 금지:
14
14
  - 정확한 구현 부담이 크면 단순화안을 사용자에게 보고 후 사용자 합의에 따름.
15
15
 
16
+ ## 불필요한 래핑·추상화 금지
17
+
18
+ API·함수가 단순 입력(리터럴·기본값·직접 인자)을 그대로 받으면 그대로 전달. "타입 안전"·"방어" 등을 명분으로 래핑·변환·간접층을 덧대지 말 것.
19
+
20
+ - 입력 타입이 단순 값을 허용하는데 래퍼로 감싸는 것 금지 — 호출부 가독성을 해치고, 읽는 사람이 타입에 문제가 있나 의심하게 만듦.
21
+ - 래핑·변환은 타입이 래퍼만 받는 등 실제로 요구되는 자리에서만 사용.
22
+
23
+ - 나쁜 예: 입력 타입이 `string | Wrapper<string>` 인데 항상 `wrap("재고", ...)` 로 감싸 전달.
24
+ - 좋은 예: `"재고"` 를 그대로 전달, 래퍼만 받는 자리에서만 `wrap(...)` 사용.
25
+
16
26
  ## 결측(null/undefined) 보존
17
27
 
18
28
  데이터가 "값 없음"(null/undefined)을 가질 수 있으면, 그 결측을 타입·계산·출력 전 계층에서 그대로 전파. 결측을 임의 값으로 치환하거나 타입에서 비결측으로 숨기지 말 것.
@@ -30,12 +30,6 @@ spec.md 의 화면 1개를 클라이언트 패키지의 화면 컴포넌트 자
30
30
 
31
31
  **충돌 확인**: 대상 화면(또는 동반 모달 화면) 의 파일이 이미 존재하면 묻기 — ① 덮어쓰기 / ② spec 변경분만 보강 / ③ 취소. 호출자 발화에 명시된 분기가 있으면 묻지 않고 그 분기로 진행.
32
32
 
33
- **마커 점검**: 대상 화면(또는 동반 모달 화면) 의 헤더 마커 확인.
34
-
35
- - 헤더 미확정(마커 없음) / `[확정: 날짜]` → 본문에 와이어프레임·항목 초안이 있으면 그대로 진행 (미확정은 미검토 초안이지만 데모가 그 검토를 돕는 시각화이므로 정상).
36
- - 단 미확정 섹션의 본문이 와이어프레임·항목 없이 분석 방법뿐이면 → 시각화할 내용 없음. 사용자에게 보고하고 중단 (sd-spec 으로 해당 화면을 먼저 채우도록 안내).
37
- - 본문 인라인 `[OPEN]` 항목 → 4단계의 더미 마커로 placeholder 처리.
38
-
39
33
  ### 2단계: 대상 화면 항목 분석
40
34
 
41
35
  spec.md 의 대상 화면 항목에서 다음을 추출: