@simplysm/sd-claude 14.0.76 → 14.0.77

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 (66) hide show
  1. package/claude/output-styles/sd-tone.md +128 -0
  2. package/claude/references/sd-simplysm14/apis/angular/README.md +28 -89
  3. package/claude/references/sd-simplysm14/apis/angular/app-structure.md +75 -32
  4. package/claude/references/sd-simplysm14/apis/angular/buttons.md +65 -29
  5. package/claude/references/sd-simplysm14/apis/angular/crud.md +86 -21
  6. package/claude/references/sd-simplysm14/apis/angular/forms.md +168 -42
  7. package/claude/references/sd-simplysm14/apis/angular/infrastructure.md +200 -49
  8. package/claude/references/sd-simplysm14/apis/angular/kanban.md +64 -20
  9. package/claude/references/sd-simplysm14/apis/angular/layout.md +75 -30
  10. package/claude/references/sd-simplysm14/apis/angular/modal.md +92 -40
  11. package/claude/references/sd-simplysm14/apis/angular/routing.md +86 -25
  12. package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +72 -41
  13. package/claude/references/sd-simplysm14/apis/angular/shared-data.md +113 -21
  14. package/claude/references/sd-simplysm14/apis/angular/sheet.md +108 -33
  15. package/claude/references/sd-simplysm14/apis/angular/toast.md +81 -30
  16. package/claude/references/sd-simplysm14/apis/angular/visual.md +140 -32
  17. package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +46 -43
  18. package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +59 -48
  19. package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +17 -7
  20. package/claude/references/sd-simplysm14/apis/core-common/README.md +43 -116
  21. package/claude/references/sd-simplysm14/apis/core-common/extensions.md +74 -109
  22. package/claude/references/sd-simplysm14/apis/core-common/features.md +40 -35
  23. package/claude/references/sd-simplysm14/apis/core-common/types.md +80 -106
  24. package/claude/references/sd-simplysm14/apis/core-common/utils.md +142 -111
  25. package/claude/references/sd-simplysm14/apis/core-node/README.md +7 -16
  26. package/claude/references/sd-simplysm14/apis/core-node/consola.md +33 -38
  27. package/claude/references/sd-simplysm14/apis/core-node/cpx.md +25 -33
  28. package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +27 -38
  29. package/claude/references/sd-simplysm14/apis/core-node/fsx.md +32 -60
  30. package/claude/references/sd-simplysm14/apis/core-node/pathx.md +14 -45
  31. package/claude/references/sd-simplysm14/apis/core-node/worker.md +35 -81
  32. package/claude/references/sd-simplysm14/apis/excel/README.md +178 -80
  33. package/claude/references/sd-simplysm14/apis/lint/README.md +5 -0
  34. package/claude/references/sd-simplysm14/apis/orm-node/README.md +1 -1
  35. package/claude/references/sd-simplysm14/apis/sd-claude/README.md +28 -5
  36. package/claude/references/sd-simplysm14/apis/sd-cli/README.md +1 -1
  37. package/claude/references/sd-simplysm14/apis/service-client/README.md +57 -50
  38. package/claude/references/sd-simplysm14/apis/service-server/README.md +8 -15
  39. package/claude/references/sd-simplysm14/apis/service-server/auth.md +24 -16
  40. package/claude/references/sd-simplysm14/apis/service-server/builtin-services.md +55 -31
  41. package/claude/references/sd-simplysm14/apis/service-server/define-service.md +28 -44
  42. package/claude/references/sd-simplysm14/apis/service-server/internals.md +59 -18
  43. package/claude/references/sd-simplysm14/apis/service-server/server.md +37 -46
  44. package/claude/references/sd-simplysm14/manuals/client-component.md +3 -1
  45. package/claude/references/sd-simplysm14/manuals/logging.md +9 -8
  46. package/claude/rules/sd-base-rules.md +380 -219
  47. package/claude/settings.json +1 -0
  48. package/claude/skills/sd-commit/SKILL.md +31 -8
  49. package/claude/skills/sd-docs/SKILL.md +15 -10
  50. package/claude/skills/sd-docs/references/subagent-prompt.md +26 -8
  51. package/claude/skills/sd-impl/SKILL.md +1 -1
  52. package/claude/skills/sd-skill/references/skill-authoring.md +1 -1
  53. package/claude/skills/sd-spec/SKILL.md +22 -13
  54. package/claude/skills/sd-spec/references/spec-authoring.md +1 -1
  55. package/claude/skills/sd-unpack/SKILL.md +150 -26
  56. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/_common.cpython-314.pyc +0 -0
  57. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/eml_handler.cpython-314.pyc +0 -0
  58. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/office_com.cpython-314.pyc +0 -0
  59. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/pdf_handler.cpython-314.pyc +0 -0
  60. package/claude/skills/sd-unpack/scripts/handlers/_common.py +17 -2
  61. package/claude/skills/sd-unpack/scripts/handlers/eml_handler.py +100 -24
  62. package/claude/skills/sd-unpack/scripts/handlers/msg_handler.py +140 -27
  63. package/claude/skills/sd-unpack/scripts/handlers/office_com.py +698 -107
  64. package/claude/skills/sd-unpack/scripts/handlers/office_worker.py +34 -26
  65. package/claude/skills/sd-unpack/scripts/handlers/pdf_handler.py +130 -8
  66. package/package.json +1 -1
@@ -66,4 +66,4 @@ dialect 분기 union. 공통 필드: `host`, `port?`, `username`, `password`, `d
66
66
  new NodeDbContextExecutor(config: DbConnConfig)
67
67
  ```
68
68
 
69
- `DbContextExecutor` 구현. `connect` 시 내부에서 `createDbConn` + `conn.connect()` 수행. `executeDefs(defs, resultMetas?)` 는 `@simplysm/orm-common` 의 `createQueryBuilder` + `parseQueryResult` 를 사용해 `QueryDef[]` → SQL → 파싱 결과. `resultMetas` 가 전부 `null` 인 경우 단일 결합 SQL 1회로 묶어 실행. 미연결 상태에서 메서드 호출 시 `DB_CONN_ERRORS.NOT_CONNECTED` SdError throw.
69
+ `DbContextExecutor` 구현. `connect` 시 내부에서 `createDbConn` + `conn.connect()` 수행. `executeDefs(defs, resultMetas?)` 는 `@simplysm/orm-common` 의 `createQueryBuilder` + `parseQueryResult` 를 사용해 `QueryDef[]` → SQL → 파싱 결과. `resultMetas` 가 전부 `null` 인 경우 단일 결합 SQL 1회로 묶어 실행하고 `defs.length` 개의 빈 배열 반환(결과 불필요 케이스 최적화). 미연결 상태에서 메서드 호출 시 `DB_CONN_ERRORS.NOT_CONNECTED` SdError throw.
@@ -1,9 +1,32 @@
1
1
  # @simplysm/sd-claude
2
2
 
3
- Claude Code 셋업 자산(`.claude/` 의 sd-* 스킬·룰·훅) 배포 및 `sd-claude` CLI 제공 패키지.
3
+ Claude Code 셋업 자산(`.claude/` 의 sd-* 스킬·룰·훅) 배포 및 `sd-claude` CLI 제공 패키지. 코드 API 없음 — 외부 import 가능한 라이브러리 심볼 미노출.
4
4
 
5
- 코드 API 없음 (npm 배포용). 외부에서 import 할 수 있는 라이브러리 심볼은 노출하지 않는다.
5
+ ## 사용 트리거 인덱스
6
6
 
7
- - `bin`: `sd-claude` `auth save` (현재 Claude 계정 저장) / `auth switch` (저장된 계정 목록에서 선택해 전환).
8
- - `postinstall`: 패키지 설치 `claude/` sd-\* 에셋을 소비 프로젝트 루트의 `.claude/` 로 복사 (기존 sd-\* 항목은 정리 후 재복사, `settings.json`·`simplysm.json` 포함).
9
- - `prepack`: 배포 워크스페이스의 `.claude/` `packages/sd-claude/claude/` 증분 복사 (mtime+size 비교, `evals/`·`SKILL.eval.md`·`eval_*` 제외).
7
+ - 현재 로그인된 Claude 계정을 프로필로 저장 [auth save](#cli-sd-claude)
8
+ - 저장된 다른 Claude 계정으로 전환 [auth switch](#cli-sd-claude)
9
+ - 소비 프로젝트 설치 `.claude/` 자산 자동 배치 [postinstall](#패키지-훅)
10
+ - 워크스페이스 `.claude/` 변경분을 패키지 `claude/` 로 동기화(배포 직전) → [prepack / sync](#패키지-훅)
11
+
12
+ ## CLI `sd-claude`
13
+
14
+ `bin: sd-claude` → `scripts/cli.mjs`. 사용법: `sd-claude <subcommand> <action>`.
15
+
16
+ - `auth save`: 현재 `claude auth status` 의 `orgName` 을 프로필 키로, `~/.claude/.credentials.json` 의 `claudeAiOauth.refreshToken` 과 `~/.claude/statusline-cache.json` 의 사용량 스냅샷을 `~/.claude/profiles.json` 에 저장하고 `current` 를 해당 프로필로 설정. orgName 또는 refreshToken 미획득 시 stderr 출력 후 exit 1.
17
+ - `auth switch`: `profiles.json` 의 계정 목록을 번호 + 현재 마커(`*`) + 사용량(현재 계정은 live, 그 외는 저장 시점) 으로 출력. 번호 입력으로 대상 선택 → 현재 계정의 최신 refreshToken·사용량을 백업 저장 → `CLAUDE_CODE_OAUTH_REFRESH_TOKEN`/`CLAUDE_CODE_OAUTH_SCOPES` 환경변수로 `claude auth login` 을 spawn → 갱신된 refreshToken 저장 + `current` 업데이트. TTY 아니면 exit 1.
18
+ - 그 외 인자: 사용법 출력 후 exit 1.
19
+
20
+ 저장 위치 식별자 풀이:
21
+ - `~/.claude/profiles.json`: `{ current: string, accounts: { [orgName]: { refreshToken, usage } } }` 구조. 패키지가 관리.
22
+ - `~/.claude/.credentials.json`: Claude Code 본체가 관리. `claudeAiOauth.refreshToken` 만 읽음.
23
+ - `~/.claude/statusline-cache.json`: Claude Code 본체가 관리. `rate_limits.five_hour` / `seven_day` 의 `used_percentage` + `resets_at` 만 읽음.
24
+
25
+ ## 패키지 훅
26
+
27
+ - `postinstall` (`scripts/postinstall.mjs`): 소비 프로젝트의 `node_modules` 설치 시 자동 실행. `INIT_CWD` 또는 `node_modules` 경로 역추적으로 프로젝트 루트 결정 → `<projectRoot>/.claude/` 의 기존 sd-* 항목(root 1단계 + 하위 디렉토리 1단계의 `^sd[-_]` 매칭) 정리 후 패키지 `claude/` 의 동일 항목 + `settings.json` + `simplysm.json` 을 복사. 소비 프로젝트가 `name: "simplysm"` 이고 메이저 버전이 같으면 skip(모노레포 자기 자신 보호). 실패해도 throw 하지 않음(install 차단 방지).
28
+ - `prepack` (`scripts/sync.mjs`): `pnpm pub`/`npm pack` 직전 실행. 워크스페이스 루트(`../../`)의 `.claude/` 에서 sd-* 항목(+ `settings.json`/`simplysm.json`) 을 패키지 `claude/` 로 증분 복사. 동일 파일은 mtime+size 비교로 skip, 변경분은 unlink 후 copy + src mtime 보존, src 에 없는 dest 항목은 prune. 제외: `evals/` 하위, 파일명 `SKILL.eval.md`, `eval_*` 접두 파일. (Windows EPERM 회피 위해 일괄 rmSync 미사용.)
29
+
30
+ 식별자 풀이:
31
+ - sd-* 항목 스캔 범위: 디렉토리 root + 1단계 하위. 정규식 `^sd[-_]`. 즉 `.claude/skills/sd-foo/`, `.claude/rules/sd-bar.md`, `.claude/sd-baz/` 등 매칭.
32
+ - watch 훅 연동: 모노레포에서는 `sd.config.ts` 의 `scripts` 타겟이 `.claude/sd-*` 변경 감지 시 본 `sync.mjs` 를 호출(패키지 외부 설정).
@@ -16,7 +16,7 @@
16
16
  - **`init`** — 인터랙티브 프롬프트로 SI 워크스페이스 골격 생성.
17
17
 
18
18
  공통 옵션:
19
- - `-t / --target <pkg>` (반복 가능) — 대상 패키지. `sd.config.ts.packages` 키 (= `@simplysm/` 접두사 **제외**한 짧은 이름). 미지정 시 전체.
19
+ - `-t / --target <pkg>` (반복 가능) — 대상 패키지. `sd.config.ts.packages` 키 (= `@simplysm/` 접두사 **제외**한 짧은 이름). 미지정 시 전체. `check`/`watch`/`dev`/`build`/`publish` 에서 지원. `device` 는 단수(`-t <pkg>` 1회). `replace-deps`/`init` 미지원.
20
20
  - `-o / --opt <val>` (반복 가능) — `sd.config.ts` 함수의 `params.opt[]` 로 전달. `check` 와 `init` 제외 전 커맨드에서 지원.
21
21
  - `--debug` — 디버그 로그 출력 (모든 커맨드).
22
22
  - `--help / -h` — 단독 호출 시 모든 서브커맨드 종합 도움말.
@@ -1,49 +1,52 @@
1
1
  # @simplysm/service-client
2
2
 
3
- `@simplysm/service-server` 와 WebSocket 으로 통신하는 클라이언트. RPC 호출·이벤트 구독·파일 업/다운로드·원격 ORM 실행을 단일 `ServiceClient` 에서 제공한다 (Node/브라우저 공용).
3
+ `@simplysm/service-server` 와 WebSocket(`/ws`) 으로 통신하는 클라이언트. RPC 호출·서버 push 이벤트 구독·파일 업/다운로드·원격 ORM 트랜잭션 실행을 단일 `ServiceClient` 에서 제공한다 (Node/브라우저 공용).
4
4
 
5
5
  ## 사용 트리거 인덱스
6
6
 
7
- - **createServiceClient / ServiceConnectionOptions** — 서버 접속용 `ServiceClient` 만들고 `connect()`/`close()` 로 연결을 열고 닫을 때.
8
- - **getService / send / ServiceProxy** — 서버에 등록된 서비스의 메서드를 타입 안전 RPC 로 호출할 때 (Proxy 기반 메서드 호출 또는 저수준 `send`).
9
- - **auth** — 서버 인증 토큰을 등록하고 재연결 시 자동 재인증되도록 토큰을 내부에 보관할 때.
10
- - **getEvent / addListener / removeListener / emitEvent** — 서버 push 이벤트를 구독·해지하거나, 다른 클라이언트에게 이벤트를 발행할 때.
7
+ - **createServiceClient / ServiceClient / ServiceConnectionOptions** — 서버 접속 클라이언트를 생성하고 `connect()` / `close()` 로 연결 수명을 관리할 때.
8
+ - **getService / send / ServiceProxy** — 서버 등록 서비스의 메서드를 타입 안전 RPC 로 호출할 때 (Proxy 호출 또는 저수준 `send`).
9
+ - **auth** — 서버 인증 토큰을 등록하고 재연결 시 자동 재인증 + 파일 업로드 인증을 활성화할 때.
10
+ - **getEvent / addListener / removeListener / emitEvent / ClientEventProxy** — 서버 push 이벤트를 구독·해지하거나 다른 클라이언트에게 발행할 때.
11
11
  - **uploadFile / downloadFileBuffer** — 서버에 multipart 파일을 올리거나 정적 경로에서 바이트(`Uint8Array`)로 받을 때.
12
- - **createOrmClientConnector / OrmConnectOptions** — 서버 `"Orm"` 서비스를 통해 `DbContext` 트랜잭션을 원격 실행할 때.
13
- - **ServiceClient 이벤트 (`state` / `request-progress` / `response-progress` / `server-progress`)** 연결 상태 변화와 전송/응답/서버 진행률을 인스턴스 레벨에서 구독할 때.
14
- - **BlobInput / FileCollection / isWorkerSupported 외** Node/브라우저 양쪽 환경에서 동일한 코드를 쓰기 위해 DOM 의존 타입을 대체하거나 Worker 지원 여부를 미리 확인할 때.
12
+ - **createOrmClientConnector / OrmClientConnector / OrmConnectOptions** — 서버 `"Orm"` 서비스를 통해 `DbContext` 트랜잭션을 원격 실행할 때.
13
+ - **OrmClientDbContextExecutor** `DbContext` 직접 구성해야 때만. 보통 `OrmClientConnector` 내부에서 사용함.
14
+ - **ServiceClient 인스턴스 이벤트 (`state` / `request-progress` / `response-progress` / `server-progress`)** 연결 상태 변화와 전송/응답/서버 진행률을 인스턴스 레벨에서 구독할 때.
15
+ - **BlobInput / FileCollection / BrowserWorker / isBrowserWorkerSupported / isNodeWorkerSupported / isWorkerSupported** — Node/브라우저 공용 코드에서 DOM 의존 타입을 대체하거나 Worker 오프로딩 가용성을 확인할 때.
15
16
 
16
- ## 연결/생성
17
+ ## 연결 / 생성
17
18
 
18
19
  ```ts
19
20
  createServiceClient(name: string, options: ServiceConnectionOptions): ServiceClient
20
21
  interface ServiceConnectionOptions { port: number; host: string; ssl?: boolean; maxReconnectCount?: number; }
21
22
  client.connect(): Promise<void>
22
- client.close(): Promise<void> // protocol wrapper 의 worker 자원도 dispose
23
+ client.close(): Promise<void> // protocol wrapper 의 worker·resolver 자원도 dispose
23
24
  client.connected: boolean
24
- client.hostUrl: string // `${ssl?https:http}://host:port`
25
+ client.hostUrl: string // `${ssl?https:http}://host:port`
25
26
  ```
26
27
 
27
- - WebSocket URL `${ws|wss}://host:port/ws?ver=2&clientId=<uuid>&clientName=<name>` 으로 접속한다.
28
- - `maxReconnectCount` 기본 10, `0` 이면 재연결 비활성.
29
- - 5s 마다 ping, 30s 무응답 시 강제 재연결. 재연결 성공 시 인증 토큰(`auth()`)과 이벤트 리스너(`addListener`)가 자동 복구된다.
28
+ - WebSocket URL: `${ws|wss}://host:port/ws?ver=2&clientId=<uuid>&clientName=<name>`.
29
+ - `maxReconnectCount` 기본 10. `0` 이면 재연결 비활성.
30
+ - 5s 마다 ping(`0x01`), 30s 무응답 시 강제 재연결. 재연결 성공 시 인증 토큰과 이벤트 리스너가 자동 복구된다.
31
+ - Node 환경에서 글로벌 `WebSocket` 이 없으면 `ws` 패키지로 polyfill (모듈 로드 시 1회).
30
32
 
31
33
  ## RPC 호출
32
34
 
33
35
  ```ts
34
- client.getService<TService>(serviceName): ServiceProxy<TService>
35
- client.send(serviceName, methodName, params, progress?): Promise<unknown>
36
- type ServiceProxy<T> = { [K in keyof T]: T[K] extends (...a:infer P)=>infer R ? (...a:P)=>Promise<Awaited<R>> : never }
37
- interface ServiceProgress { request?(s); response?(s); server?(s); }
38
- interface ServiceProgressState { uuid: string; totalSize: number; completedSize: number; }
36
+ client.getService<TService>(serviceName: string): ServiceProxy<TService>
37
+ client.send(serviceName, methodName, params: unknown[], progress?: ServiceProgress): Promise<unknown>
38
+ type ServiceProxy<T> = { [K in keyof T]: T[K] extends (...a: infer P) => infer R ? (...a: P) => Promise<Awaited<R>> : never }
39
+ interface ServiceProgress { request?(s: ServiceProgressState): void; response?(s): void; server?(s): void }
40
+ interface ServiceProgressState { uuid: string; totalSize: number; completedSize: number }
39
41
  ```
40
42
 
41
- - `getService` 는 Proxy 임의 메서드명 호출 가능. 타입은 `TService` 로만 보장된다.
43
+ - `getService` 는 Proxy. 임의 메서드명 호출 가능하며 타입 보장은 `TService` 로만.
42
44
  - 와이어 메시지 이름은 `"<serviceName>.<methodName>"`.
43
- - 인코딩/디코딩의 Worker 오프로딩 임계값:
44
- - **인코드**: body 가 `Uint8Array`, 30KB 초과 문자열, 길이 100 초과 배열, 또는 첫 요소가 `Uint8Array` 인 배열일 때 Worker 사용.
45
- - **디코드**: 수신 바이트가 30KB 초과일 Worker 사용.
46
- - Worker 미지원 환경(또는 초기화 실패)에선 메인 스레드 fallback.
45
+ - 인코드/디코드의 Worker 오프로딩 임계값:
46
+ - **인코드**: body 가 `Uint8Array`, 30KB 초과 문자열, 길이 100 초과 배열, 또는 첫 원소가 `Uint8Array` 인 배열일 때 Worker.
47
+ - **디코드**: 수신 바이트 30KB 초과 Worker.
48
+ - Worker 미지원/초기화 실패 환경은 메인 스레드 fallback.
49
+ - 소켓 끊김·재연결 시 대기 중인 모든 요청은 `"요청 취소됨: 소켓 연결이 끊어졌습니다"` 로 reject.
47
50
 
48
51
  ## 인증
49
52
 
@@ -51,22 +54,23 @@ interface ServiceProgressState { uuid: string; totalSize: number; completedSize:
51
54
  client.auth(token: string): Promise<void>
52
55
  ```
53
56
 
54
- - 서버에 `auth` 메시지를 보내고 토큰을 내부 저장. 재연결 시 자동으로 같은 토큰으로 재인증.
55
- - `uploadFile` 은 인증 토큰이 없으면 에러를 던진다.
57
+ - 서버에 `auth` 메시지를 보내고 토큰을 내부 저장. 재연결 시 동일 토큰으로 자동 재인증.
58
+ - `uploadFile` 은 토큰이 없으면 즉시 throw.
56
59
 
57
60
  ## 이벤트 (서버 push)
58
61
 
59
62
  ```ts
60
- client.getEvent<TEventDef>(eventName): ClientEventProxy<TEventDef>
61
- client.addListener<TEventDef>(eventName, info, cb): Promise<string> // key 반환
62
- client.removeListener(key): Promise<void>
63
- client.emitEvent<TEventDef>(eventName, infoSelector, data): Promise<void>
64
- interface ClientEventProxy<T> { addListener(info, cb): Promise<string>; removeListener(key): Promise<void>; emit(infoSelector, data): Promise<void>; }
63
+ client.getEvent<TEventDef>(eventName: string): ClientEventProxy<TEventDef>
64
+ client.addListener<TEventDef>(eventName, info: TEventDef["$info"], cb): Promise<string> // listener key
65
+ client.removeListener(key: string): Promise<void>
66
+ client.emitEvent<TEventDef>(eventName, infoSelector: (info) => boolean, data): Promise<void>
67
+ interface ClientEventProxy<T> { addListener(info, cb): Promise<string>; removeListener(key): Promise<void>; emit(infoSelector, data): Promise<void> }
65
68
  ```
66
69
 
67
- - `addListener` 는 연결 상태에서만 호출 가능 (`!connected` throw). 로컬 맵에도 보관해 재연결 시 자동 재구독.
68
- - `emitEvent` 는 서버에 `evt:gets` 로 후보를 받아 `infoSelector` 통과분에만 `evt:emit` 호출. 대상이 0건이면 emit 생략.
69
- - 핸들러 내부 에러는 로깅만 되고 다른 핸들러 호출은 계속 진행.
70
+ - `addListener` 는 `connected` 상태에서만 호출 가능 ( throw). 로컬 맵에 보관해 재연결 시 자동 재구독.
71
+ - `emitEvent` 는 서버에 `evt:gets` 로 후보 목록을 받아 `infoSelector` 통과분에만 `evt:emit` 호출. 대상 0건이면 emit 생략.
72
+ - 핸들러 내부 에러는 로깅만, 다른 핸들러 호출은 계속.
73
+ - `removeListener` 는 서버 전송 실패를 무시함 (서버가 연결 끊김 시 자동 정리하므로).
70
74
 
71
75
  ## 파일 업/다운로드
72
76
 
@@ -74,11 +78,11 @@ interface ClientEventProxy<T> { addListener(info, cb): Promise<string>; removeLi
74
78
  client.uploadFile(files: File[] | FileCollection | { name: string; data: BlobInput }[]): Promise<ServiceUploadResult[]>
75
79
  client.downloadFileBuffer(relPath: string): Promise<Uint8Array>
76
80
  type BlobInput = Blob | Uint8Array<ArrayBuffer> | ArrayBuffer | string
77
- interface FileCollection { length; item(i); [i]; [Symbol.iterator]() } // 브라우저 FileList 호환
81
+ interface FileCollection { readonly length; item(i); [i]; [Symbol.iterator]() } // 브라우저 FileList 와 구조적 호환
78
82
  ```
79
83
 
80
84
  - 업로드: `POST {hostUrl}/upload` (multipart). 헤더 `x-sd-client-name`, `Authorization: Bearer <token>`. 인증 필수.
81
- - 다운로드: `GET {hostUrl}/{relPath}` 응답 본문을 `Uint8Array` 로 반환. `relPath` 가 `/` 로 시작하지 않으면 자동으로 붙임.
85
+ - 다운로드: `GET {hostUrl}/{relPath}` 응답 본문을 `Uint8Array` 로 반환. `relPath` 가 `/` 로 시작하지 않으면 자동 prepend.
82
86
  - 브라우저 전용 `File`/`FileList` 대신 `BlobInput`/`FileCollection` 으로 Node 환경도 지원.
83
87
 
84
88
  ## ORM 클라이언트
@@ -86,30 +90,31 @@ interface FileCollection { length; item(i); [i]; [Symbol.iterator]() } // 브
86
90
  ```ts
87
91
  createOrmClientConnector(client: ServiceClient): OrmClientConnector
88
92
  interface OrmConnectOptions<T extends DbContext> {
89
- DbClass: new (executor, { database, schema? }) => T;
93
+ DbClass: new (executor: DbContextExecutor, opt: { database: string; schema?: string }) => T;
90
94
  connOpt: DbConnOptions & { configName: string };
91
- dbContextOpt?: { database: string; schema: string };
95
+ dbContextOpt?: { database: string; schema: string }; // 지정 시 둘 다 필수
92
96
  }
93
97
  connector.connect(config, async db => ...) // 트랜잭션 래핑
94
98
  connector.connectWithoutTransaction(config, async db => ...)
95
99
  ```
96
100
 
97
- - 서버의 `"Orm"` 서비스에 RPC 를 걸어 `getInfo` → `connect` → `executeDefs`/`executeParametrized`/`bulkInsert` 등을 실행.
98
- - `dbContextOpt` 생략 시 서버가 알려준 `database`/`schema` 사용. 최종 `database` 가 빈 값이면 에러.
99
- - 외래키 참조 위반 에러(`a parent row: a foreign key constraint` / `conflicted with the REFERENCE`)는 "경고! 연관된 작업으로 인해 작업이 거부되었습니다..." 사용자 메시지로 변환되어 throw (원본은 `cause`).
101
+ - 서버 `"Orm"` 서비스에 RPC 를 걸어 `getInfo` → `connect` → `executeDefs` / `executeParametrized` / `bulkInsert` / `begin·commit·rollbackTransaction` / `close` 를 위임.
102
+ - `dbContextOpt` 생략 시 서버가 알려준 `database` / `schema` 사용. 최종 `database` 가 빈 값이면 throw.
103
+ - 외래키 참조 위반 메시지(`a parent row: a foreign key constraint` / `conflicted with the REFERENCE`)는 "경고! 연관된 작업으로 인해 작업이 거부되었습니다. 후속 작업을 확인해 주세요." 변환되어 throw (원본은 `cause`).
104
+ - `OrmClientDbContextExecutor` 는 `DbContextExecutor` 구현체. 보통 connector 가 내부 생성하며 직접 인스턴스화는 커스텀 `DbContext` 구성 시에만.
100
105
 
101
- ## 상태/진행률 이벤트
106
+ ## 인스턴스 이벤트 (상태/진행률)
102
107
 
103
108
  ```ts
104
109
  client.on("state", (s: "connected" | "closed" | "reconnecting") => ...)
105
- client.on("request-progress", (s: ServiceProgressState) => ...) // 클라이언트 → 서버 전송 청크 진행
106
- client.on("response-progress", (s: ServiceProgressState) => ...) // 서버 → 클라이언트 응답 청크 진행
110
+ client.on("request-progress", (s: ServiceProgressState) => ...) // 클라 → 서버 전송 청크 진행
111
+ client.on("response-progress", (s: ServiceProgressState) => ...) // 서버 → 클라 응답 청크 진행
107
112
  client.on("server-progress", (s: ServiceProgressState) => ...) // 서버가 명시적으로 보내는 진행률
108
113
  ```
109
114
 
110
- - `request-progress` 는 인코드 결과 chunk 가 2개 이상일 때만 초기 0% 발행됨. 단일 청크는 progress 없음.
111
- - `response-progress` 는 분할 수신 중에는 서버 progress 메시지로, 응답 완료 시 100% 보정값으로 발행됨.
112
- - `send` 호출 전달한 `progress?: ServiceProgress` 콜백은 인스턴스 이벤트와 함께 호출된다.
115
+ - `request-progress` 는 인코드 결과 chunk 가 2개 이상일 때만 0% 초기값 발행. 단일 청크면 발행 없음.
116
+ - `response-progress` 는 분할 수신 중에는 서버 progress 메시지, 응답 완료 시 100% 보정값으로 발행.
117
+ - `send` 호출에 전달한 `progress` 콜백은 인스턴스 이벤트와 함께 호출됨.
113
118
 
114
119
  ## 환경 호환 유틸
115
120
 
@@ -119,6 +124,8 @@ import { BlobInput, FileCollection, BrowserWorker,
119
124
  from "@simplysm/service-client";
120
125
  ```
121
126
 
122
- - `BrowserWorker` — DOM `Worker` 의 구조적 호환 인터페이스. Node 환경 typecheck 통과용.
123
- - `isBrowserWorkerSupported()` / `isNodeWorkerSupported()` / `isWorkerSupported()` — 현재 런타임에서 Worker 오프로딩이 가능한지 사전 확인.
124
- - 보통 직접 호출할 필요 없음. `ServiceClient` 내부에서 분기하므로, 환경별 분기 로직을 사용자 코드에서 직접 짤 때만 사용.
127
+ - `BrowserWorker` — DOM `Worker` 의 최소 구조 호환 인터페이스. Node 환경 typecheck 통과용.
128
+ - `isBrowserWorkerSupported()` `"Worker" in globalThis`.
129
+ - `isNodeWorkerSupported()` `process.versions.node` 존재 여부.
130
+ - `isWorkerSupported()` — 위 둘 중 하나.
131
+ - 보통 직접 호출할 필요 없음 (`ServiceClient` 내부에서 분기). 환경 분기 로직을 사용자 코드에서 직접 짤 때만 사용.
@@ -1,20 +1,13 @@
1
1
  # @simplysm/service-server
2
2
 
3
- Fastify 기반 서비스 서버. HTTP/WebSocket 양 전송 위에서 "서비스(`이름`+`메서드 맵`)" 단위로 RPC, JWT 인증, 정적 파일, 업로드, 이벤트 브로드캐스트, V1 레거시 호환을 제공한다.
3
+ Fastify + WebSocket 기반 서비스 서버. 클라이언트(`@simplysm/service-client`) 짝으로 RPC·이벤트 푸시·파일 업로드·정적 서빙·JWT 인증을 단일 포트로 제공한다.
4
4
 
5
5
  ## 사용 트리거 인덱스
6
6
 
7
- - **`ServiceServer` / `createServiceServer` / `ServiceServerOptions`** — 서버 인스턴스 생성·기동·종료·이벤트 브로드캐스트·JWT 발급. 자세히: [server.md](./server.md)
8
- - **`defineService` / `auth` / `ServiceContext` / `ServiceMethods`** — 서비스 정의 및 메서드/팩토리 인증 래퍼, 컨텍스트 헬퍼. 자세히: [define-service.md](./define-service.md)
9
- - **`AuthTokenPayload` / `signJwt` / `verifyJwt` / `decodeJwt`** — JWT 토큰 페이로드 타입 직접 서명/검증 유틸. 자세히: [auth.md](./auth.md)
10
- - **빌트인 서비스 (`OrmService` / `AutoUpdateService` / `AppStructureService`)** `services` 옵션에 그대로 등록해 ORM 프록시·자동 업데이트·앱 메뉴 트리 제공. 자세히: [builtin-services.md](./builtin-services.md)
11
- - **전송/프로토콜/레거시 내부** (`handleHttpRequest`, `handleUpload`, `handleStaticFile`, `createWebSocketHandler`, `createServiceSocket`, `createServerProtocolWrapper`, `handleV1Connection` 등) — `ServiceServer.listen()` 이 자동 사용. 커스텀 Fastify 라우트 직조 시에만 직접 호출. 자세히: [internals.md](./internals.md)
12
-
13
- ## `getConfig`
14
-
15
- ```ts
16
- import { getConfig } from "@simplysm/service-server";
17
- const conf = await getConfig<{ orm: { default: DbConnConfig } }>(filePath);
18
- ```
19
-
20
- `<rootPath>/.config.json` 같은 JSON 설정 파일을 읽고 LazyGcMap 캐시 + `FsWatcher` 로 핫리로드한다. 보통은 `ctx.getConfig(section)` 으로 충분하며, 이 함수를 직접 호출할 일은 root/client 경로 외 설정 파일을 읽을 때만 있다.
7
+ | 트리거 | 자료 |
8
+ | --- | --- |
9
+ | 서버 인스턴스 생성·기동·종료, 이벤트 emit, JWT 토큰 발급/검증, `ServiceServerOptions`(`rootPath`/`port`/`ssl`/`auth`/`services`/`legacyV1Handlers`) | [server.md](./server.md) |
10
+ | 사용자 서비스 정의(`defineService`), `ServiceContext`, `ServiceMethods` 타입 추출, `createServiceContext`, `getServiceAuthPermissions` | [define-service.md](./define-service.md) |
11
+ | 인증 래퍼 `auth()` (서비스/메서드 레벨, 역할 제한), `AuthTokenPayload`, `signJwt`/`verifyJwt`/`decodeJwt` | [auth.md](./auth.md) |
12
+ | 빌트인 서비스: `OrmService`, `AutoUpdateService`, `AppStructureService`, V1 레거시 자동 업데이트 핸들러 | [builtin-services.md](./builtin-services.md) |
13
+ | 내부 전송 계층(`/api/:service/:method`·`/upload`·정적 서빙, WebSocket 핸들러, 프로토콜 래퍼/워커, `ServiceSocket`, `getConfig`) — 직접 사용 시 | [internals.md](./internals.md) |
@@ -1,29 +1,37 @@
1
- # @simplysm/service-server — auth
1
+ # @simplysm/service-server — 인증
2
2
 
3
- JWT 페이로드 타입 및 서명/검증 유틸. 일반 시나리오에서는 `server.signAuthToken` / `server.verifyAuthToken` 이 동일 알고리즘으로 감싸므로 그쪽을 쓴다. 이 모듈을 직접 import 하는 경우는 토큰 발급/검증을 서버 인스턴스 없이 수행해야 할 때 (테스트, 별도 인증 서비스, 디코드 등).
3
+ ## `auth(fn)` / `auth(permissions, fn)`
4
4
 
5
- ## `AuthTokenPayload<TAuthInfo>`
5
+ 서비스 factory 또는 개별 메서드를 래핑해 인증 요구사항을 부착. 래핑 함수는 호출 동작을 그대로 유지하며 내부 심볼에 `permissions: string[]` 저장.
6
6
 
7
- ```ts
8
- interface AuthTokenPayload<TAuthInfo = unknown> extends jose.JWTPayload {
9
- roles: string[]; // auth(["role"], fn) 검사 대상
10
- data: TAuthInfo; // ctx.authInfo 노출
11
- }
12
- ```
7
+ - 서비스 수준: `defineService("X", auth((ctx) => ({ ... })))` — 모든 메서드 로그인 필요.
8
+ - 서비스 수준 + 역할: `defineService("X", auth(["admin"], (ctx) => ({ ... })))`.
9
+ - 메서드 수준: factory 내부에서 `someMethod: auth(() => result)`.
10
+ - 메서드 수준 + 역할: `someMethod: auth(["admin", "owner"], () => result)` — 배열 중 하나라도 매칭하면 통과(OR).
13
11
 
14
- ## `signJwt(jwtSecret, payload) → Promise<string>`
12
+ 실행 시(`executeServiceMethod`) 동작:
13
+
14
+ - 메서드 권한이 있으면 서비스 권한을 **덮어쓴다**. 메서드 권한 미부착 시 서비스 권한 사용.
15
+ - 권한 요구 + `options.auth == null` → "auth 설정이 필요합니다" throw(서버 설정 오류).
16
+ - 권한 요구 + `options.auth === false` → 인증 스킵.
17
+ - 권한 요구 + `options.auth = { jwtSecret }` → payload 없으면 "로그인이 필요합니다" throw. `permissions.length > 0` 이면 `payload.roles` 중 하나라도 일치해야 통과, 아니면 "권한이 부족합니다" throw. `permissions.length === 0` (= `auth(fn)`) 이면 토큰만 있으면 통과.
18
+
19
+ ## `AuthTokenPayload<TAuthInfo>`
15
20
 
16
- HS256, `iat` 자동, 만료 `12h` 고정. 토큰 수명을 바꿔야 하면 이 함수 대신 직접 `jose.SignJWT` 를 쓴다.
21
+ `jose.JWTPayload` 확장.
17
22
 
18
- ## `verifyJwt<TAuthInfo>(jwtSecret, token) Promise<AuthTokenPayload<TAuthInfo>>`
23
+ - `roles: string[]` — `auth(["role"], fn)` 검사 대상.
24
+ - `data: TAuthInfo` — 임의 사용자 데이터. `ctx.authInfo` 로 노출.
19
25
 
20
- 검증 실패 시 `토큰이 만료되었습니다.` 또는 `유효하지 않은 토큰입니다.` 를 throw.
26
+ ## JWT 헬퍼
21
27
 
22
- ## `decodeJwt<TAuthInfo>(token) AuthTokenPayload<TAuthInfo>`
28
+ `server.signAuthToken`/`verifyAuthToken` 이 동일 알고리즘으로 감싸므로 일반 시나리오에선 그쪽을 쓴다. 직접 import 는 서버 인스턴스 없이 토큰을 다루는 경우(테스트, 별도 인증 서버, 디코드 등)만.
23
29
 
24
- 서명 검증 없이 페이로드만 디코드. 클라이언트 만료 표시 비보안 용도에만.
30
+ - `signJwt<T>(jwtSecret, payload): Promise<string>` HS256, `iat` 자동, `exp = 12h` 고정. 수명 변경 필요 시 `jose.SignJWT` 직접 사용.
31
+ - `verifyJwt<T>(jwtSecret, token): Promise<AuthTokenPayload<T>>` — 만료: "토큰이 만료되었습니다", 그 외 위조: "유효하지 않은 토큰입니다" throw.
32
+ - `decodeJwt<T>(token): AuthTokenPayload<T>` — 서명 검증 없이 페이로드만 디코드. 클라이언트 측 만료 표시 등 비보안 용도에만.
25
33
 
26
- ## 예제
34
+ ##
27
35
 
28
36
  ```ts
29
37
  const token = await signJwt(secret, { roles: ["admin"], data: { userId: "U1" } });
@@ -1,47 +1,71 @@
1
- # @simplysm/service-server — builtin-services
1
+ # @simplysm/service-server — 빌트인 서비스
2
2
 
3
- `ServiceServerOptions.services` 에 그대로 푸시해서 쓰는 사전 정의 서비스. 각각의 `*ServiceType` 은 클라이언트 측에서 `client.getService<*ServiceType>("<이름>")` 타입 공유한다.
3
+ `options.services` 에 추가해 사용. `*ServiceType` 은 클라이언트의 `client.getService<*ServiceType>("<이름>")` 그대로 사용.
4
4
 
5
- ## `OrmService` / `OrmServiceType`
5
+ ## `OrmService`
6
6
 
7
- 이름 alias: `["Orm", "SdOrmService"]`. **WebSocket 전용** (HTTP 호출 throw — 트랜잭션을 소켓 라이프타임에 묶기 위함). `auth()` 로 감싸져 있어 로그인 필요.
7
+ DB 연결을 WebSocket 세션과 묶어 원격 ORM 사용.
8
8
 
9
- 설정: `ctx.getConfig("orm")` 으로 `Record<string, DbConnConfig>` 를 읽고 `opt.configName` 으로 선택. 클라이언트가 `opt.config` 로 일부 필드 override 가능.
9
+ - 별칭: `["Orm", "SdOrmService"]`.
10
+ - **WebSocket 전용**. HTTP 호출 시 "WebSocket 연결이 필요합니다…" throw(트랜잭션을 소켓 라이프타임에 묶기 위함).
11
+ - `auth()` 래핑이라 로그인 필수. 역할 제한은 없음.
12
+ - 설정 소스: `ctx.getConfig<Record<string, DbConnConfig>>("orm")` → `opt.configName` 키 조회. 호출 시 `opt.config` 가 base 설정을 부분 override.
13
+ - 연결 ID 는 소켓별 카운터(1부터, 가장 큰 값+1). 소켓 close 시 해당 소켓의 모든 열린 DB 연결 자동 정리(에러는 warn 후 무시).
14
+ - `dialect === "mssql-azure"` 는 외부 노출 시 `"mssql"` 로 정규화.
10
15
 
11
- 메서드: `getInfo`, `connect → connId`, `close(connId)`, `beginTransaction(connId, isolation?)`, `commitTransaction`, `rollbackTransaction`, `executeParametrized(connId, sql, params?)`, `executeDefs(connId, defs, optionsMeta?)`, `bulkInsert(connId, table, colDefs, records)`. 소켓 close 시 그 소켓의 모든 연결을 자동 정리.
16
+ 메서드:
12
17
 
13
- ## `AutoUpdateService` / `AutoUpdateServiceType`
18
+ - `getInfo(opt)` `{ dialect, database?, schema? }` (연결 안 만들고 설정 조회만).
19
+ - `connect(opt): Promise<number>` — connId 반환.
20
+ - `close(connId)`.
21
+ - `beginTransaction(connId, isolationLevel?)`, `commitTransaction(connId)`, `rollbackTransaction(connId)`.
22
+ - `executeParametrized(connId, query, params?)` — `unknown[][]` (멀티 결과셋).
23
+ - `executeDefs(connId, defs: QueryDef[], options?: (ResultMeta | undefined)[])` — `options` 가 전부 null/undefined 면 단일 multi-SQL 실행 후 빈 배열들, 아니면 def 별로 build → execute → `pickResultSets` → opt 있으면 `parseQueryResult`, 없으면 raw.
24
+ - `bulkInsert(connId, tableName, columnDefs, records)`.
14
25
 
15
- 이름 alias: `["AutoUpdate", "SdAutoUpdateService"]`. 인증 없음.
26
+ `opt: DbConnOptions & { configName: string }`. 타입 export: `OrmServiceType`.
16
27
 
17
- `getLastVersion(platform)` — `<clientPath>/<platform>/updates/` 디렉토리에서 `android` 는 `.apk`, 그 외는 `.exe` 중 `semver.maxSatisfying("*")` 으로 최신을 골라 `{ version, downloadPath }` 반환. 없으면 `undefined`.
28
+ ## `AutoUpdateService`
18
29
 
19
- ## `AppStructureService(itemsMap) ServiceDefinition` / `AppStructureServiceType`
30
+ - 별칭: `["AutoUpdate", "SdAutoUpdateService"]`. 인증 없음.
31
+ - `getLastVersion(platform: string): Promise<{ version, downloadPath } | undefined>` — `<clientPath>/<platform>/updates/` 에서 `platform === "android"` → `.apk`, 그 외 → `.exe` 중 파일명이 `^[0-9.]*$` 인 것만 후보. `semver.maxSatisfying(versions, "*")` 로 최신 선정. clientName 미설정 시 throw, 디렉터리/매칭 파일 없으면 undefined.
32
+ - `downloadPath` = `/<clientName>/<platform>/updates/<fileName>` (POSIX 정규화).
33
+ - 타입 export: `AutoUpdateServiceType`.
20
34
 
21
- 다른 두 서비스와 달리 **팩토리 호출 결과**를 등록한다. 이름 `"AppStructure"`, 인증 없음.
35
+ ## `AppStructureService(itemsMap)`
22
36
 
23
- ```ts
24
- import { AppStructureService } from "@simplysm/service-server";
25
- import type { AppStructureItem } from "@simplysm/service-common";
26
-
27
- const itemsMap: Record<string, AppStructureItem[]> = { admin: [...], shop: [...] };
28
- services: [AppStructureService(itemsMap)];
29
- ```
37
+ 다른 두 서비스와 달리 **팩토리 함수**(서비스 정의가 아님). 호출 결과를 `services` 에 등록.
30
38
 
31
- `getItems()` 메서드만 노출. 클라이언트 메뉴/라우트 트리 공급용.
39
+ - 시그니처: `AppStructureService(itemsMap: Record<string, AppStructureItem[]>): ServiceDefinition`
40
+ - 서비스 이름: `"AppStructure"`. 인증 없음.
41
+ - 메서드: `getItems(): Record<string, AppStructureItem[]>` — 주입된 map 그대로 반환.
42
+ - 타입 export: `AppStructureServiceType`.
32
43
 
33
- ## 등록 예제
44
+ `AppStructureItem` `@simplysm/service-common` 정의(메뉴/라우트 트리 항목).
34
45
 
35
46
  ```ts
36
- import { createServiceServer, OrmService, AutoUpdateService, AppStructureService } from "@simplysm/service-server";
37
-
38
- createServiceServer({
39
- rootPath, port: 50080, auth: { jwtSecret },
40
- services: [
41
- OrmService,
42
- AutoUpdateService,
43
- AppStructureService(appItemsMap),
44
- MyAppService,
45
- ],
46
- });
47
+ services: [AppStructureService({ admin: [...], user: [...] })];
47
48
  ```
49
+
50
+ ## V1 레거시 자동 업데이트
51
+
52
+ ver≠"2" 로 접속한 구버전 클라이언트 호환. JSON 라인 프로토콜.
53
+
54
+ ### `handleV1Connection(socket, autoUpdateMethods, clientNameSetter?)` / `handleV1Connection(socket, options)`
55
+
56
+ `ServiceServer` 가 자동 호출. `services` 에 `AutoUpdate` 가 있으면 그 factory 의 `getLastVersion` 을 V1 메서드로 자동 매핑. `legacyV1Handlers` 도 그대로 전달. 둘 다 없으면 1008 거부.
57
+
58
+ `V1ConnectionOptions`:
59
+
60
+ - `serviceContext?` 또는 `serviceContextFactory?: (req: V1Request) => ServiceContext` — 핸들러용 컨텍스트(둘 중 하나).
61
+ - `handlers?: V1RequestHandler[]` — 우선 실행. 각 핸들러는 `{ handled: true, state?: "success"|"error", body } | { handled: false }` 반환. handled=true 면 즉시 응답.
62
+ - `autoUpdateMethods?` 또는 `autoUpdateMethodsFactory?: (ctx) => V1AutoUpdateMethods` — 모든 사용자 핸들러가 미처리 + `command === "SdAutoUpdateService.getLastVersion"` 일 때 fallback.
63
+ - `clientNameSetter?: (clientName: string | undefined) => void` — 매 요청의 `clientName` 알림 콜백.
64
+ - 어디서도 처리 안 되면 `{ code: "UPGRADE_REQUIRED", message: "앱 업그레이드가 필요합니다." }` 응답.
65
+
66
+ 타입:
67
+
68
+ - `V1Request = { uuid, command, params: unknown[], clientName? }`
69
+ - `V1Response = { name: "response", reqUuid, state: "success" | "error", body }`
70
+ - `V1AutoUpdateMethods = { getLastVersion(platform): Promise<unknown> | unknown }`
71
+ - `V1RequestHandler`, `V1RequestHandlerContext`, `V1RequestHandlerResult`.
@@ -1,70 +1,54 @@
1
- # @simplysm/service-server — define-service
1
+ # @simplysm/service-server — 서비스 정의
2
2
 
3
- 서비스 정의 + 인증 래퍼 + 컨텍스트. `ServiceServerOptions.services` 들어갈 단위를 만든다.
3
+ ## `defineService(name, factory): ServiceDefinition<TMethods>`
4
4
 
5
- ## `defineService(name, factory) ServiceDefinition<TMethods>`
5
+ - `name: string | string[]` — 단일 또는 별칭 목록. 첫 요소가 primary(`def.name`), 전체가 `def.names`. RPC 라우팅은 `names.includes(요청서비스명)` 매칭. 빈 배열이면 throw.
6
+ - `factory: (ctx: ServiceContext) => TMethods` — 매 요청마다 호출되어 메서드 객체 생성(컨텍스트 캡처). factory 자체가 `auth(...)` 래핑이면 `authPermissions` 가 자동 추출돼 서비스 수준 인증으로 승격.
6
7
 
7
- ```ts
8
- defineService<TMethods extends Record<string, (...args: any[]) => any>>(
9
- name: string | string[], // 다중 이름 = alias (예: ["Orm", "SdOrmService"])
10
- factory: (ctx: ServiceContext) => TMethods,
11
- ): ServiceDefinition<TMethods>;
12
- ```
13
-
14
- `factory` 는 호출마다 실행돼 메서드 객체를 생성한다 (요청별 컨텍스트 캡처). `factory` 가 `auth(...)` 로 감싸져 있으면 서비스 수준 인증으로 승격된다.
8
+ 반환 `ServiceDefinition<TMethods>`:
15
9
 
16
- ## `auth(...)` — 인증 래퍼
17
-
18
- ```ts
19
- auth(fn) // 로그인 필요
20
- auth(["admin", "owner"], fn) // 해당 역할 중 하나 필요 (OR)
21
- ```
22
-
23
- - 서비스 수준: `defineService("X", auth((ctx) => ({ ... })))`
24
- - 메서드 수준: 팩토리 안에서 메서드를 `auth(["admin"], () => result)` 로 감싼다. 메서드 권한이 있으면 서비스 권한을 **덮어쓴다**.
25
- - `auth: false` 옵션 시 검증 스킵, `auth: undefined` 인데 auth 필요 서비스 등록 시 `listen()` throw, auth 설정됐는데 토큰 없거나 권한 부족 시 메서드 호출에서 throw (`로그인이 필요합니다.` / `권한이 부족합니다.`).
10
+ - `name: string`
11
+ - `names: string[]`
12
+ - `factory`
13
+ - `authPermissions?: string[]`
26
14
 
27
15
  ## `ServiceContext<TAuthInfo>`
28
16
 
29
- 서비스 메서드가 받는 요청별 컨텍스트.
17
+ 서비스 factory 받는 요청별 컨텍스트.
30
18
 
31
- ```ts
32
- interface ServiceContext<TAuthInfo = unknown> {
33
- server: ServiceServer<TAuthInfo>;
34
- socket?: ServiceSocket; // WS 경로일 때만
35
- http?: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> };
36
- legacy?: { clientName?: string }; // V1 레거시 경로
19
+ - `server: ServiceServer<TAuthInfo>`
20
+ - `socket?: ServiceSocket` WebSocket 경로일 때만.
21
+ - `http?: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> }` — HTTP 경로.
22
+ - `legacy?: { clientName?: string }` — V1 경로.
23
+ - `get authInfo(): TAuthInfo | undefined` — socket 의 payload.data 우선, 없으면 http.
24
+ - `get clientName(): string | undefined` socket → http → legacy 순. 빈 문자/`..`/`/`/`\\` 포함 시 throw.
25
+ - `get clientPath(): string | undefined` — `<rootPath>/www/<clientName>`. clientName 없으면 undefined.
26
+ - `getConfig<T>(section: string): Promise<T>` — `<rootPath>/.config.json` + `<clientPath>/.config.json` 을 `obj.merge` 한 뒤 `section` 키 반환. 섹션 없으면 throw. 설정 파일은 `FsWatcher` 로 변경 감시되어 자동 리로드.
37
27
 
38
- get authInfo(): TAuthInfo | undefined; // payload.data
39
- get clientName(): string | undefined; // socket → http → legacy 순. 위험문자(.. / \) throw
40
- get clientPath(): string | undefined; // <rootPath>/www/<clientName>
41
- getConfig<T>(section: string): Promise<T>; // root + clientPath 의 .config.json merge, 누락 시 throw
42
- }
43
- ```
28
+ ## `ServiceMethods<TDefinition>` 타입
44
29
 
45
- ## `ServiceMethods<TDefinition>`
46
-
47
- 클라이언트에서 메서드 시그니처만 공유하기 위한 추출 타입.
30
+ `ServiceDefinition<M>` 에서 `M` 추출. 클라이언트의 `client.getService<MyServiceType>("MyService")` 에 사용.
48
31
 
49
32
  ```ts
50
- export const UserService = defineService("User", (ctx) => ({ ... }));
33
+ export const UserService = defineService("User", (ctx) => ({
34
+ getProfile: () => ({ name: "kim" }),
35
+ }));
51
36
  export type UserServiceType = ServiceMethods<typeof UserService>;
52
- // 클라이언트: client.getService<UserServiceType>("User")
53
37
  ```
54
38
 
55
- ## 보조
39
+ ## 보조 export
56
40
 
57
- - `createServiceContext(server, socket?, http?, legacy?)` — 컨텍스트 직조 (커스텀 라우트용).
58
- - `getServiceAuthPermissions(fn)` — `auth()` 가 함수에 심볼로 심어둔 권한 배열을 읽는다 (내부 executor 가 사용).
41
+ - `createServiceContext(server, socket?, http?, legacy?): ServiceContext` — 컨텍스트 직조. 커스텀 라우트/테스트용.
42
+ - `getServiceAuthPermissions(fn): string[] | undefined` — `auth()` 가 함수에 심어둔 권한 배열 추출. 래핑 안 됐으면 undefined. 내부 executor 가 사용.
59
43
 
60
- ## 예제
44
+ ##
61
45
 
62
46
  ```ts
63
47
  const HealthService = defineService("Health", () => ({
64
48
  check: () => ({ status: "ok" }),
65
49
  }));
66
50
 
67
- const UserService = defineService("User", auth((ctx) => ({
51
+ const UserService = defineService(["User", "MyUser"], auth((ctx) => ({
68
52
  me: () => ctx.authInfo,
69
53
  adminOnly: auth(["admin"], () => "ok"),
70
54
  })));