@simplysm/sd-claude 14.0.88 → 14.0.90

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 (135) hide show
  1. package/claude/references/sd-simplysm14/README.md +16 -17
  2. package/claude/references/sd-simplysm14/apis/angular/README.md +39 -43
  3. package/claude/references/sd-simplysm14/apis/angular/controls.md +174 -80
  4. package/claude/references/sd-simplysm14/apis/angular/crud.md +41 -50
  5. package/claude/references/sd-simplysm14/apis/angular/directives.md +60 -26
  6. package/claude/references/sd-simplysm14/apis/angular/features.md +109 -37
  7. package/claude/references/sd-simplysm14/apis/angular/infra.md +61 -44
  8. package/claude/references/sd-simplysm14/apis/angular/layout.md +39 -31
  9. package/claude/references/sd-simplysm14/apis/angular/overlay.md +73 -85
  10. package/claude/references/sd-simplysm14/apis/angular/routing-appstructure.md +54 -39
  11. package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +55 -30
  12. package/claude/references/sd-simplysm14/apis/angular/shared-data.md +71 -67
  13. package/claude/references/sd-simplysm14/apis/angular/sheet.md +82 -72
  14. package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +35 -36
  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 +38 -30
  17. package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +45 -50
  18. package/claude/references/sd-simplysm14/apis/core-browser/README.md +42 -55
  19. package/claude/references/sd-simplysm14/apis/core-browser/dom-element.md +62 -0
  20. package/claude/references/sd-simplysm14/apis/core-browser/indexed-db.md +39 -38
  21. package/claude/references/sd-simplysm14/apis/core-common/README.md +95 -103
  22. package/claude/references/sd-simplysm14/apis/core-common/array-ext.md +59 -54
  23. package/claude/references/sd-simplysm14/apis/core-common/async-runtime.md +86 -0
  24. package/claude/references/sd-simplysm14/apis/core-common/datetime.md +57 -66
  25. package/claude/references/sd-simplysm14/apis/core-common/errors.md +86 -0
  26. package/claude/references/sd-simplysm14/apis/core-common/obj.md +60 -42
  27. package/claude/references/sd-simplysm14/apis/core-common/serialization.md +55 -0
  28. package/claude/references/sd-simplysm14/apis/core-node/README.md +10 -8
  29. package/claude/references/sd-simplysm14/apis/core-node/consola.md +29 -32
  30. package/claude/references/sd-simplysm14/apis/core-node/cpx.md +34 -22
  31. package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +29 -25
  32. package/claude/references/sd-simplysm14/apis/core-node/fsx.md +40 -53
  33. package/claude/references/sd-simplysm14/apis/core-node/pathx.md +22 -29
  34. package/claude/references/sd-simplysm14/apis/core-node/worker.md +31 -31
  35. package/claude/references/sd-simplysm14/apis/excel/README.md +26 -26
  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/lint/README.md +27 -21
  43. package/claude/references/sd-simplysm14/apis/lint/rules.md +89 -49
  44. package/claude/references/sd-simplysm14/apis/orm-common/README.md +6 -62
  45. package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +149 -67
  46. package/claude/references/sd-simplysm14/apis/orm-common/expr.md +111 -99
  47. package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +115 -72
  48. package/claude/references/sd-simplysm14/apis/orm-common/schema.md +134 -92
  49. package/claude/references/sd-simplysm14/apis/orm-common/types.md +67 -52
  50. package/claude/references/sd-simplysm14/apis/orm-node/README.md +63 -26
  51. package/claude/references/sd-simplysm14/apis/orm-node/db-conn.md +51 -40
  52. package/claude/references/sd-simplysm14/apis/sd-cli/README.md +10 -12
  53. package/claude/references/sd-simplysm14/apis/sd-cli/SdTsCompiler.md +92 -45
  54. package/claude/references/sd-simplysm14/apis/sd-cli/sd-config-types.md +226 -108
  55. package/claude/references/sd-simplysm14/apis/service-client/README.md +90 -88
  56. package/claude/references/sd-simplysm14/apis/service-client/orm.md +37 -29
  57. package/claude/references/sd-simplysm14/apis/service-client/transport.md +45 -20
  58. package/claude/references/sd-simplysm14/apis/service-common/README.md +89 -40
  59. package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +126 -34
  60. package/claude/references/sd-simplysm14/apis/service-common/protocol.md +109 -54
  61. package/claude/references/sd-simplysm14/apis/service-server/README.md +70 -66
  62. package/claude/references/sd-simplysm14/apis/service-server/service-authoring.md +47 -47
  63. package/claude/references/sd-simplysm14/apis/service-server/transport-internals.md +71 -34
  64. package/claude/references/sd-simplysm14/apis/service-server/v1-legacy.md +31 -32
  65. package/claude/references/sd-simplysm14/apis/storage/README.md +34 -28
  66. package/claude/references/sd-simplysm14/manuals/client-app-structure.md +142 -140
  67. package/claude/references/sd-simplysm14/manuals/client-orm.md +1 -1
  68. package/claude/references/sd-simplysm14/manuals/client-service.md +19 -7
  69. package/claude/references/sd-simplysm14/manuals/client-shared-data.md +2 -2
  70. package/claude/references/sd-simplysm14/manuals/client-system-log.md +11 -3
  71. package/claude/references/sd-simplysm14/manuals/data-log.md +0 -1
  72. package/claude/references/sd-simplysm14/manuals/orm.md +16 -0
  73. package/claude/rules/sd-design-rules.md +10 -0
  74. package/claude/skills/sd-docs/SKILL.md +58 -46
  75. package/claude/skills/sd-docs/references/{doc-rules.md → subagent-prompt.md} +103 -103
  76. package/claude/skills/sd-impl/SKILL.md +1 -1
  77. package/claude/skills/sd-spec/SKILL.md +858 -858
  78. package/claude/skills/sd-spec/references/example-spec.md +26 -36
  79. package/package.json +1 -1
  80. package/claude/references/sd-simplysm14/apis/core-common/json-transfer.md +0 -47
  81. package/claude/references/sd-simplysm14/apis/orm-common/query-builder.md +0 -29
  82. package/claude/skills/sd-demo/evals/fixtures/inventory-list/.specs/inventory/spec.md +0 -99
  83. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/package.json +0 -12
  84. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/index.ts +0 -3
  85. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inbound/inbound.list.ts +0 -150
  86. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inventory/inventory-master.list.ts +0 -143
  87. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/outbound/outbound.list.ts +0 -150
  88. package/claude/skills/sd-demo/evals/fixtures/inventory-list/pnpm-workspace.yaml +0 -2
  89. package/claude/skills/sd-demo/evals/fixtures/inventory-list/sd.config.ts +0 -12
  90. package/claude/skills/sd-demo/evals/golden.jsonl +0 -1
  91. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/package.json +0 -8
  92. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/src/.gitkeep +0 -0
  93. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tests/.gitkeep +0 -0
  94. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tsconfig.json +0 -10
  95. package/claude/skills/sd-dev/evals/golden.jsonl +0 -1
  96. package/claude/skills/sd-docs/evals/fixtures/new-write/.claude/references/sd-simplysm14/README.md +0 -7
  97. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/bar/package.json +0 -5
  98. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/bar/src/index.ts +0 -3
  99. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/baz/package.json +0 -6
  100. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/baz/src/index.ts +0 -1
  101. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/foo/package.json +0 -5
  102. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/foo/src/index.ts +0 -8
  103. package/claude/skills/sd-docs/evals/fixtures/update-mixed/.claude/references/sd-simplysm14/README.md +0 -7
  104. package/claude/skills/sd-docs/evals/fixtures/update-mixed/.claude/references/sd-simplysm14/apis/foo/README.md +0 -3
  105. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/bar/package.json +0 -5
  106. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/bar/src/index.ts +0 -3
  107. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/baz/package.json +0 -6
  108. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/baz/src/index.ts +0 -1
  109. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/foo/package.json +0 -5
  110. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/foo/src/index.ts +0 -8
  111. package/claude/skills/sd-docs/evals/golden.jsonl +0 -2
  112. package/claude/skills/sd-impl/evals/fixtures/case-a-new-screen/.specs/260513120000_warehouse/spec.md +0 -101
  113. package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/.specs/260513120000_warehouse/spec.md +0 -101
  114. package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/packages/app/src/screens/box-register/box-register.view.ts +0 -46
  115. package/claude/skills/sd-impl/evals/fixtures/case-c-new-cross/.specs/260513120000_warehouse/spec.md +0 -89
  116. package/claude/skills/sd-impl/evals/fixtures/case-d-spec-modify/.specs/260513120000_warehouse/spec.md +0 -101
  117. package/claude/skills/sd-impl/evals/golden.jsonl +0 -4
  118. package/claude/skills/sd-manual/evals/fixtures/new-manual/src/notification.ts +0 -25
  119. package/claude/skills/sd-manual/evals/fixtures/update-manual/.claude/references/sd-simplysm14/manuals/notification.md +0 -14
  120. package/claude/skills/sd-manual/evals/fixtures/update-manual/src/notification.ts +0 -37
  121. package/claude/skills/sd-manual/evals/golden.jsonl +0 -2
  122. package/claude/skills/sd-review/evals/fixtures/code-review/src/foo.ts +0 -7
  123. package/claude/skills/sd-review/evals/fixtures/doc-review/docs/foo.md +0 -4
  124. package/claude/skills/sd-review/evals/golden.jsonl +0 -2
  125. package/claude/skills/sd-skill/evals/fixtures/existing-skill/.claude/skills/todo-format/SKILL.md +0 -14
  126. package/claude/skills/sd-skill/evals/fixtures/new-skill/.gitkeep +0 -0
  127. package/claude/skills/sd-skill/evals/golden.jsonl +0 -2
  128. package/claude/skills/sd-spec/evals/fixtures/case-a-split//355/232/214/354/235/230/353/241/235.md +0 -20
  129. package/claude/skills/sd-spec/evals/fixtures/case-b-detail/.specs/260513120000_warehouse/spec.md +0 -95
  130. package/claude/skills/sd-spec/evals/golden.jsonl +0 -2
  131. package/claude/skills/sd-unpack/evals/fixtures/eml-with-text-attachment/meeting.eml +0 -21
  132. package/claude/skills/sd-unpack/evals/fixtures/simple-eml/meeting.eml +0 -10
  133. package/claude/skills/sd-unpack/evals/golden.jsonl +0 -2
  134. package/claude/skills/sd-use/evals/fixtures/empty/.gitkeep +0 -0
  135. package/claude/skills/sd-use/evals/golden.jsonl +0 -6
@@ -1,102 +1,106 @@
1
1
  # @simplysm/service-server
2
2
 
3
- Fastify 기반 서비스 서버. WebSocket(v2)/HTTP RPC, JWT 인증, 정적 파일·업로드, 서버→클라이언트 이벤트 푸시를 서버로 제공한다. 클라이언트는 `defineService` 정의한 서비스의 메서드를 원격 호출한다.
3
+ Fastify 기반 RPC 서비스 서버. WebSocket/HTTP 전송 계층으로 서비스 메서드를 노출하고, JWT 인증·정적 파일·파일 업로드·서버측 이벤트 브로드캐스팅·내장 ORM/자동업데이트 서비스를 프로세스에서 제공한다.
4
4
 
5
5
  ## 사용 트리거 인덱스
6
6
 
7
- - **createServiceServer / ServiceServer / ServiceServerOptions / ServerEventProxy** 서버를 부팅(listen)·종료하고, 토큰을 서명/검증하고, 클라이언트로 이벤트를 푸시할 때.
8
- - **defineService / auth / ServiceContext / ServiceMethods / ServiceDefinition** — RPC 서비스(메서드 묶음)를 정의하고 인증을 때. 서비스 작성 시 항상 함께 읽힘. 자세히: [service-authoring.md](./service-authoring.md)
9
- - **signJwt / verifyJwt / decodeJwt / AuthTokenPayload**JWT 토큰을 서버 밖에서 직접 서명·검증·디코드할 때(서버의 `signAuthToken`/`verifyAuthToken` 으로 충분하면 불필요).
10
- - **OrmService / AutoUpdateService**DB 원격 실행·앱 자동업데이트를 `services` 끼워 넣을 때.
11
- - **getConfig** `.config.json` 캐시·워치와 함께 직접 읽을 때(보통 `ctx.getConfig` 간접 사용).
12
- - **전송·프로토콜 내부 (WebSocketHandler, ServiceSocket, handleHttpRequest, handleUpload, handleStaticFile, ServerProtocolWrapper)**ServiceServer 내부에서만 쓰는 저수준 핸들러. 직접 서버를 조립·진단할 때만. 자세히: [transport-internals.md](./transport-internals.md)
13
- - **V1 레거시 (handleV1Connection, V1RequestHandler 등)**ver≠2 구버전 클라이언트(자동업데이트) 받을 때. 자세히: [v1-legacy.md](./v1-legacy.md)
7
+ - **서버 부트스트랩** (`createServiceServer`, `ServiceServer`, `ServiceServerOptions`) 서버 진입점에서 옵션을 주고 서버를 띄울 때. 아래 인라인 섹션.
8
+ - **서비스 작성** (`defineService`, `auth`, `ServiceContext`, `ServiceMethods` 등) — RPC 로 노출할 서비스 메서드를 정의하고 인증·권한을 붙일 때. 자세히: [service-authoring.md](./service-authoring.md)
9
+ - **JWT 인증 토큰** (`signJwt`, `verifyJwt`, `decodeJwt`, `AuthTokenPayload`)로그인 처리에서 토큰을 서명·검증할 때. 아래 인라인 섹션.
10
+ - **서버측 이벤트 발생** (`ServiceServer.emitEvent`, `getEvent`, `ServerEventProxy`) 서비스 메서드 안에서 구독 클라이언트에 이벤트를 브로드캐스트할 때. 아래 인라인 섹션.
11
+ - **내장 서비스** (`OrmService`, `AutoUpdateService`) DB 접근·자동업데이트를 서버 옵션의 `services` 끼워넣을 때. 아래 인라인 섹션.
12
+ - **전송 계층 내부** (`executeServiceMethod`, `createServiceContext`, `ServiceSocket`, `WebSocketHandler`, `ServerProtocolWrapper`, `getConfig` ) — 커스텀 전송·테스트·디버깅에서 내부 구성요소를 직접 다룰 때. 자세히: [transport-internals.md](./transport-internals.md)
13
+ - **V1 레거시 지원** (`handleV1Connection`, `V1RequestHandler` 등) — 구버전(ver=1) 클라이언트의 WebSocket 연결을 받아 자동업데이트만 응대할 때. 자세히: [v1-legacy.md](./v1-legacy.md)
14
14
 
15
- ## 서버 부팅 (createServiceServer / ServiceServer)
15
+ ## 서버 부트스트랩
16
16
 
17
- `createServiceServer<TAuthInfo>(options): ServiceServer<TAuthInfo>` `new ServiceServer(options)` 래퍼. `TAuthInfo` 는 `ctx.authInfo` JWT 페이로드 `data` 의 타입.
17
+ 서버 진입점에서 `createServiceServer(options)` 인스턴스를 만들고 `await server.listen()` 으로 기동한다.
18
18
 
19
- `class ServiceServer<TAuthInfo> extends EventEmitter<{ ready: void; close: void }>`:
19
+ ### createServiceServer / ServiceServer
20
20
 
21
- - `options: ServiceServerOptions`생성자에 넘긴 옵션(readonly).
22
- - `isOpen: boolean` — listen 성공 후 true, close 후 false. 가동 여부 판단에 사용.
23
- - `fastify: FastifyInstance` — 내부 Fastify 인스턴스. 임의 포트 조회(`fastify.server.address()`)·직접 라우트 추가에 사용.
24
- - `listen(): Promise<void>` — 플러그인 등록 후 `0.0.0.0:options.port` 수신 시작. 완료 시 `ready` emit. `auth` 미설정인데 auth 요구 서비스가 있으면 throw. listen 중 SIGINT/SIGTERM graceful shutdown(10초 타임아웃 후 강제 종료) 1회 등록.
25
- - `close(): Promise<void>` — 모든 WebSocket 연결을 닫고 Fastify 종료. `close` emit.
26
- - `getEvent<TEventDef>(eventDef): ServerEventProxy<TEventDef>` — eventDef 를 바인딩한 emit 전용 프록시 획득. 같은 이벤트를 반복 emit 할 때 편의.
27
- - `emitEvent<TEventDef>(eventDef, infoSelector, data): Promise<void>` — `infoSelector(info) === true` 인 리스너에게만 `data` 전송. `infoSelector` = 리스너 등록 시의 info 로 수신 대상 선별.
28
- - `signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>` — JWT 발급. `auth` 미설정 시 throw. 로그인 처리에 사용.
29
- - `verifyAuthToken(token): Promise<AuthTokenPayload<TAuthInfo>>` — JWT 검증·페이로드 반환. `auth` 미설정 시 throw.
21
+ `createServiceServer<TAuthInfo = unknown>(options: ServiceServerOptions): ServiceServer<TAuthInfo>` 옵션을 받아 서버 인스턴스 생성(아직 리슨 안 함). `TAuthInfo` 는 인증 토큰의 `data` 페이로드 타입으로, `server.signAuthToken`·`ctx.authInfo` 에 그대로 흐른다. `new ServiceServer(options)` 직접 생성과 동일.
30
22
 
31
- `ServerEventProxy<TEventDef>`:
23
+ `ServiceServerOptions` 필드:
32
24
 
33
- - `emit(infoSelector: (info: TEventDef["$info"]) => boolean, data: TEventDef["$data"]): Promise<void>` `getEvent` 반환하는 emit 헬퍼. `emitEvent` 동작 동일하되 eventDef 가 이미 바인딩됨.
25
+ - `rootPath: string` 서버 작업 루트. 정적 파일·업로드·자동업데이트는 모두 `rootPath/www` 하위를, 설정은 `rootPath/.config.json` 기준으로 한다. 절대경로를 권장.
26
+ - `port: number` — 리슨 포트. `0` 을 주면 OS 가 임의 포트를 할당(테스트용); 실제 포트는 `server.fastify.server.address()` 로 확인.
27
+ - `ssl?: { pfxBytes: Uint8Array; passphrase: string }` — HTTPS 인증서. 지정 시 HTTPS 로 기동하고 HSTS·COOP 보안 헤더가 켜진다. 미지정 시 HTTP(평문)로 뜨고 `upgrade-insecure-requests` CSP 가 해제된다. 사내망 평문이면 생략, 외부 노출이면 지정.
28
+ - `auth?: { jwtSecret: string } | false` — JWT 인증 설정. 객체면 해당 시크릿으로 토큰 서명·검증; `false` 면 인증을 의도적으로 비활성(권한 요구 메서드도 인증 검사 스킵); `undefined`(미지정)이면서 권한 요구 서비스가 하나라도 있으면 `listen()` 이 에러로 중단. 인증 쓰는 앱이면 객체, 개발·내부 도구로 인증을 끄려면 `false`.
29
+ - `services: ServiceDefinition[]` — `defineService` 로 만든 서비스 정의 배열. RPC 로 노출할 서비스 전부를 여기 등록.
30
+ - `legacyV1Handlers?: V1RequestHandler[]` — V1(ver=1) 레거시 클라이언트의 커스텀 요청 핸들러. 자세히: [v1-legacy.md](./v1-legacy.md).
31
+
32
+ `ServiceServer` 의 멤버:
33
+
34
+ - `readonly options: ServiceServerOptions` — 생성 시 받은 옵션 원본.
35
+ - `readonly fastify: FastifyInstance` — 내부 Fastify 인스턴스. 포트 조회·추가 라우트 등록 등에 직접 접근.
36
+ - `isOpen: boolean` — 리슨 성공 후 `true`, `close()` 후 `false`.
37
+ - `listen(): Promise<void>` — 플러그인 등록(websocket/helmet/multipart/static/cors) 후 `0.0.0.0:port` 리슨. 완료 시 `"ready"` 이벤트 발생, `SIGINT`/`SIGTERM` 정상 종료 핸들러 등록(10초 내 미종료 시 강제 exit).
38
+ - `close(): Promise<void>` — 모든 WebSocket 종료 후 Fastify 종료, `"close"` 이벤트 발생.
39
+ - `signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>` — `auth.jwtSecret` 으로 토큰 서명. 시크릿 미설정 시 throw.
40
+ - `verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>` — 토큰 검증·디코드. 시크릿 미설정 시 throw.
41
+ - `getEvent`/`emitEvent` — 아래 "서버측 이벤트 발생" 참조.
42
+ - `on("ready" | "close", handler)` — `EventEmitter` 상속. 기동·종료 시점 후킹.
34
43
 
35
44
  ```ts
36
- const server = createServiceServer<MyAuth>({
45
+ const server = createServiceServer<AuthInfo>({
37
46
  rootPath: process.cwd(),
38
47
  port: 50080,
39
48
  auth: { jwtSecret: env("JWT_SECRET")! },
40
- services: [OrmService, AutoUpdateService, MyService],
49
+ services: [UserService, OrmService, AutoUpdateService],
41
50
  });
42
51
  await server.listen();
43
- const token = await server.signAuthToken({ roles: ["admin"], data: authInfo });
44
- await server.getEvent(MyEventDef).emit((info) => info.room === "lobby", payload);
45
52
  ```
46
53
 
47
- ## ServiceServerOptions
48
-
49
- - `rootPath: string` — 서버 루트. 정적파일은 `<rootPath>/www`, 업로드는 `<rootPath>/www/uploads`, 설정은 `<rootPath>/.config.json` 및 `www/<client>/.config.json` 기준.
50
- - `port: number` — 수신 포트. `0` 이면 OS가 임의 할당(테스트용, `fastify.server.address()` 로 확인).
51
- - `ssl?: { pfxBytes: Uint8Array; passphrase: string }` — HTTPS 설정. `pfxBytes` = PFX 인증서 바이트, `passphrase` = 암호. 설정 시 HSTS·COOP 켜짐, 미설정 시 HTTP(`upgrade-insecure-requests` 해제).
52
- - `auth?: { jwtSecret: string } | false` — 인증 모드. `{ jwtSecret }` = JWT 활성(서명/검증 키), `false` = auth 요구 서비스를 두되 인증 검사를 의도적으로 스킵, `undefined`(미지정) = 인증 비활성이되 auth 요구 서비스가 있으면 `listen()` 에서 throw.
53
- - `services: ServiceDefinition[]` — 등록할 서비스 목록(`defineService` 산출물).
54
- - `legacyV1Handlers?: V1RequestHandler[]` — ver≠2 구버전 클라이언트용 커스텀 핸들러. 미지정 시 빈 배열.
55
-
56
- ## 인증 토큰 (AuthTokenPayload / signJwt / verifyJwt / decodeJwt)
57
-
58
- `ServiceServer.signAuthToken`/`verifyAuthToken` 의 저수준 구현. 서버 밖에서 토큰을 직접 다룰 때 사용. HS256, 발급시각 자동, 만료 12시간 고정.
54
+ 주의: `auth` 를 미지정한 채 권한 요구(`auth(...)` 래핑) 서비스를 등록하면 `listen()` 이 즉시 throw 한다. 인증을 끄려면 `auth: false` 를 명시할 것.
59
55
 
60
- `interface AuthTokenPayload<TAuthInfo> extends jose.JWTPayload`:
56
+ ## JWT 인증 토큰
61
57
 
62
- - `roles: string[]` 권한 역할 목록. `auth(["admin"], ...)` 권한 검사 대상.
63
- - `data: TAuthInfo` — 애플리케이션 인증 정보. `ctx.authInfo` 로 노출됨.
58
+ 로그인 서비스 메서드에서 자격 확인 토큰을 발급하고, 다른 메서드에서 토큰을 검증할 때. 보통은 `server.signAuthToken`/`server.verifyAuthToken`(시크릿 자동 사용) 쓰고, 시크릿을 직접 다룰 때만 아래 함수를 호출.
64
59
 
65
- - `signJwt<TAuthInfo>(jwtSecret, payload): Promise<string>` HS256·발급시각 자동·만료 12h서명.
66
- - `verifyJwt<TAuthInfo>(jwtSecret, token): Promise<AuthTokenPayload<TAuthInfo>>`검증·디코드. 만료 "토큰이 만료되었습니다.", 실패 시 "유효하지 않은 토큰입니다." throw.
67
- - `decodeJwt<TAuthInfo>(token): AuthTokenPayload<TAuthInfo>`서명 검증 없이 페이로드만 디코드.
60
+ - `AuthTokenPayload<TAuthInfo>` — JWT 페이로드. `jose` 의 `JWTPayload`(`exp`/`iat` ) 를 확장하며 `roles: string[]`(권한 역할 목록, `auth(["admin"], ...)` 의 권한 매칭 대상)과 `data: TAuthInfo`(앱 정의 사용자 정보, `ctx.authInfo` 노출)를 추가.
61
+ - `signJwt<TAuthInfo>(jwtSecret: string, payload: AuthTokenPayload<TAuthInfo>): Promise<string>` HS256 으로 서명. `iat` 자동 설정, 만료 12시간 고정.
62
+ - `verifyJwt<TAuthInfo>(jwtSecret: string, token: string): Promise<AuthTokenPayload<TAuthInfo>>`서명·만료 검증 페이로드 반환. 만료 시 "토큰이 만료되었습니다.", 그 외 검증 실패 시 "유효하지 않은 토큰입니다." 로 throw.
63
+ - `decodeJwt<TAuthInfo>(token: string): AuthTokenPayload<TAuthInfo>` — 서명 검증 없이 페이로드만 디코드. 검증이 끝난 토큰의 내용만 다시 읽을 때(만료·위변조 판정에는 쓰지 말 것).
68
64
 
69
65
  ```ts
70
- const token = await signJwt(secret, { roles: ["admin"], data: { userId: "u1" } });
71
- const payload = await verifyJwt<MyAuth>(secret, token); // payload.data, payload.roles
66
+ const login = defineService("Auth", (ctx) => ({
67
+ login: async (id: string, pw: string) => {
68
+ const user = await authenticate(id, pw); // 앱 로직
69
+ return ctx.server.signAuthToken({ roles: user.roles, data: user });
70
+ },
71
+ }));
72
72
  ```
73
73
 
74
- ## 빌트인 서비스 (OrmService / AutoUpdateService)
74
+ ## 서버측 이벤트 발생
75
75
 
76
- `services` 배열에 넣어 등록. 서비스는 별칭 2개로 노출된다.
76
+ 서비스 메서드 처리 결과를 구독 중인 클라이언트에 브로드캐스트할 때. 이벤트 정의 객체(`@simplysm/service-common` 의 `defineEvent`)는 클라이언트·서버가 공유한다.
77
77
 
78
- `OrmService` (이름 `"Orm"`, `"SdOrmService"`) — `auth()` 래핑(로그인 필요). WebSocket 전용(HTTP 호출 throw). 소켓별 DB 연결을 `connId` 관리하며 소켓 종료 자동 정리. 연결 설정은 `ctx.getConfig("orm")[configName]` 에서 읽음. 메서드:
78
+ - `ServiceServer.emitEvent<TEventDef>(eventDef: TEventDef, infoSelector: (info: TEventDef["$info"]) => boolean, data: TEventDef["$data"]): Promise<void>` — `eventDef.eventName` 구독한 클라이언트 리스너 `infoSelector(info)` `true` 대상에게만 `data` 전송. 전체 전송은 `() => true`, 어느 구독에도 걸리면 전송 자체 생략.
79
+ - `ServiceServer.getEvent<TEventDef>(eventDef): ServerEventProxy<TEventDef>` — 같은 이벤트를 반복 발생시킬 때 쓰는 프록시. `proxy.emit(infoSelector, data)` 는 `emitEvent` 와 동일.
80
+ - `ServerEventProxy<TEventDef>` — `{ emit(infoSelector, data): Promise<void> }` 형태.
79
81
 
80
- - `getInfo(opt): Promise<{ dialect; database?; schema? }>` — 설정의 dialect·DB·스키마 조회(`mssql-azure` → `mssql` 로 표준화). `opt` = `DbConnOptions & { configName }`.
81
- - `connect(opt): Promise<number>` DB 연결 `connId` 반환. 첫 연결 시 소켓 close 훅 등록(누수 방지).
82
- - `close(connId): Promise<void>` 연결 종료. 종료 중 에러는 warn 로그만 남기고 무시.
83
- - `beginTransaction(connId, isolationLevel?): Promise<void>` — 트랜잭션 시작. `isolationLevel` = 격리수준(미지정 시 드라이버 기본).
84
- - `commitTransaction(connId)` / `rollbackTransaction(connId): Promise<void>` — 커밋/롤백.
85
- - `executeParametrized(connId, query, params?): Promise<unknown[][]>` — 파라미터 바인딩 SQL 실행.
86
- - `executeDefs(connId, defs, options?): Promise<unknown[][]>` 쿼리 정의 배열 실행. `options` 가 모두 null 이면 묶음 실행 후 빈 결과, 아니면 정의별 결과셋을 `options[i]`(ResultMeta) 로 파싱.
87
- - `bulkInsert(connId, tableName, columnDefs, records): Promise<void>` — 대량 삽입. `columnDefs` = 컬럼별 메타, `records` = 행 객체 배열.
88
- - `type OrmServiceType = ServiceMethods<typeof OrmService>` — 클라이언트 타입 공유용.
82
+ ```ts
83
+ export const OrderService = defineService("Order", (ctx) => ({
84
+ ship: async (orderId: number) => {
85
+ // ... 처리 ...
86
+ await ctx.server.emitEvent(
87
+ OrderStatusChangedEvent,
88
+ (info) => info.warehouseId === 7,
89
+ { orderId, status: "shipped" },
90
+ );
91
+ },
92
+ }));
93
+ ```
89
94
 
90
- `AutoUpdateService` (이름 `"AutoUpdate"`, `"SdAutoUpdateService"`) — 인증 없음.
95
+ ## 내장 서비스
91
96
 
92
- - `getLastVersion(platform): Promise<{ version; downloadPath } | undefined>` — `www/<client>/<platform>/updates` 에서 최신 semver 산출물(`platform === "android"` `.apk`, `.exe`)을 찾아 버전·다운로드 경로 반환. 디렉토리/산출물 없으면 undefined. clientPath 없으면 throw.
93
- - `type AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>` — 클라이언트 타입 공유용.
97
+ 서버 옵션의 `services` 배열에 그대로 추가해 사용하는 미리 정의된 서비스. 클라이언트 타입 공유용 `*Type` 함께 export 된다.
98
+
99
+ - `OrmService` / `OrmServiceType` — `["Orm", "SdOrmService"]` 두 이름으로 노출되는 ORM 서비스. **WebSocket 전송 전용**(소켓 단위로 DB 커넥션을 풀링하므로 HTTP 호출 시 throw). 모든 메서드가 로그인 필요(`auth(...)` 래핑). DB 접속 정보는 `getConfig("orm")[configName]` 으로 `rootPath/.config.json` 의 `orm` 섹션에서 읽는다. 메서드: `getInfo`(dialect/database/schema 조회), `connect`(커넥션 풀에 연결 추가 후 connId 반환), `close`/`beginTransaction`/`commitTransaction`/`rollbackTransaction`(connId 대상 트랜잭션 제어), `executeParametrized`(파라미터 쿼리 실행), `executeDefs`(QueryDef 배열을 dialect 에 맞춰 빌드·실행·파싱), `bulkInsert`(대량 삽입). 소켓 종료 시 해당 소켓의 모든 커넥션 자동 정리.
100
+ - `AutoUpdateService` / `AutoUpdateServiceType` — `["AutoUpdate", "SdAutoUpdateService"]` 두 이름으로 노출되는 자동업데이트 서비스. 메서드 `getLastVersion(platform: string)` 은 `rootPath/www/<clientName>/<platform>/updates` 에서 `android` 면 `.apk`, 그 외면 `.exe` 중 semver 최대 버전을 찾아 `{ version, downloadPath }` 반환(없으면 `undefined`). 인증 불필요.
94
101
 
95
102
  ```ts
96
- services: [OrmService, AutoUpdateService]
97
- // 클라이언트: client.getService<OrmServiceType>("Orm")
103
+ services: [OrmService, AutoUpdateService, ...앱서비스들]
98
104
  ```
99
105
 
100
- ## 설정 읽기 (getConfig)
101
-
102
- `getConfig<TConfig>(filePath): Promise<TConfig | undefined>` — JSON 설정 파일 로드. 1시간 만료 캐시 + FsWatcher 로 파일 변경 시 자동 리로드, 삭제 시 캐시 무효화. 파일 없으면 undefined. 보통 직접 호출하지 않고 `ctx.getConfig(section)`(루트 + 클라이언트 설정 merge 후 섹션 추출)으로 사용.
106
+ 주의: 내장 서비스는 클라이언트가 `getService("Orm")` / `getService("AutoUpdate")` 의 짧은 이름 또는 `SdOrmService`/`SdAutoUpdateService` 레거시 이름 어느 쪽으로도 호출할 수 있다.
@@ -1,74 +1,74 @@
1
- # @simplysm/service-server — service-authoring
1
+ # @simplysm/service-server — 서비스 작성
2
2
 
3
- RPC 서비스(클라이언트가 원격 호출할 메서드 묶음)를 정의하고 인증을 거는 묶음. `defineService`·`auth`·`ServiceContext`·`ServiceMethods` 서비스 작성 항상 함께 읽힌다. `defineService` 산출물을 `ServiceServerOptions.services` 등록한다.
3
+ RPC 로 노출할 서비스 메서드를 정의하고, 컨텍스트로 인증 정보·클라이언트 정보·설정에 접근하며, 인증·권한을 붙일 같이 읽는 묶음. 서버 옵션 `services` 등록할 `ServiceDefinition` 만드는 것이 목표.
4
4
 
5
5
  ## defineService
6
6
 
7
- `defineService<TMethods>(name: string | string[], factory: (ctx: ServiceContext) => TMethods): ServiceDefinition<TMethods>` — 이름과 팩토리로 서비스를 정의.
7
+ ```ts
8
+ function defineService<TMethods extends Record<string, (...args: any[]) => any>>(
9
+ name: string | string[],
10
+ factory: (ctx: ServiceContext) => TMethods,
11
+ ): ServiceDefinition<TMethods>
12
+ ```
8
13
 
9
- - `name: string | string[]` — 서비스 이름. 배열이면 다중 별칭(첫 요소가 primary `name`, 전체가 호출 매칭용 `names`). 클라이언트는 `names` 아무 이름으로나 호출 가능. 빈 배열이면 "서비스 이름은 하나 이상 필요합니다." throw.
10
- - `factory: (ctx) => TMethods` — 컨텍스트를 받아 메서드 객체 반환. 메서드 호출마다 ctx 1회 실행됨. `auth(...)` 감싸면 서비스 전체에 인증 부여.
14
+ - `name: string | string[]` — 서비스 이름. 배열이면 여러 이름(별칭)으로 동시 노출하며 첫 요소가 대표 이름(`definition.name`). 배열이면 throw. 클라이언트는 이 이름으로 `getService("<name>")` 호출. 신/구 이름을 같이 받으려면 `["New", "Old"]`.
15
+ - `factory: (ctx) => TMethods` — **요청마다 호출**되어 메서드 객체를 반환하는 팩토리. `ctx` 로 그 요청의 인증·클라이언트 정보가 들어오므로 메서드 안에서 `ctx.*` 자유롭게 참조. 요청 간 공유 상태(커넥션 풀 등) 팩토리 바깥 모듈 스코프에 것.
16
+ - 반환 `TMethods` 의 각 값은 클라이언트가 호출할 메서드. 동기·비동기 모두 가능하며 반환값이 그대로 응답으로 직렬화된다.
11
17
 
12
18
  ```ts
13
- export const HealthService = defineService("Health", (ctx) => ({
14
- check: () => ({ status: "ok" }),
19
+ export const UserService = defineService("User", (ctx) => ({
20
+ getProfile: auth(() => ctx.authInfo),
21
+ echo: (msg: string) => `Echo: ${msg}`,
15
22
  }));
23
+ export type UserServiceMethods = ServiceMethods<typeof UserService>;
16
24
  ```
17
25
 
18
- ## ServiceDefinition
19
-
20
- `interface ServiceDefinition<TMethods>` — `defineService` 산출물.
21
-
22
- - `name: string` — 대표 이름(`names[0]`).
23
- - `names: string[]` — 호출 매칭에 쓰이는 전체 이름 목록(별칭 포함).
24
- - `factory: (ctx: ServiceContext) => TMethods` — 메서드 생성 팩토리.
25
- - `authPermissions?: string[]` — 서비스 수준 인증 권한. 팩토리를 `auth` 로 감쌌을 때만 존재(빈 배열 = 로그인만 요구, undefined = 인증 없음).
26
-
27
26
  ## auth
28
27
 
29
- 인증 래퍼. 서비스 팩토리 또는 개별 메서드를 감싼다. 호출 동작은 보존하고 권한 메타데이터만 심볼로 부착하며, 메서드 수준 권한이 서비스 수준보다 우선한다.
28
+ 서비스 팩토리 또는 개별 메서드를 인증 래퍼로 감싸 로그인·권한을 요구한다. 권한 메타데이터를 함수에 심볼로 부착하되 호출 동작은 그대로 보존하는 래퍼를 만든다.
29
+
30
+ ```ts
31
+ function auth<TFn extends (...args: any[]) => any>(fn: TFn): TFn;
32
+ function auth<TFn extends (...args: any[]) => any>(permissions: string[], fn: TFn): TFn;
33
+ ```
30
34
 
31
- - `auth<TFn>(fn): TFn` — 로그인만 요구(권한 무관). 토큰 없으면 "로그인이 필요합니다." throw.
32
- - `auth<TFn>(permissions: string[], fn): TFn` — `roles` `permissions` 중 하나라도 있어야 통과. 배열이면 로그인만 요구. 권한 부족 시 "권한이 부족합니다." throw.
33
- - 적용 위치: 팩토리 전체를 감싸면(서비스 수준) 모든 메서드에, 메서드 1개를 감싸면(메서드 수준) 메서드에만 적용.
34
- - 특수 상황: 서버 옵션 `auth === false` 면 검사 스킵(인증 의도적 비활성), `auth` 미설정(null)인데 auth 메서드 호출 "auth 설정이 필요합니다." 설정오류 throw.
35
+ - `auth(fn)` — 로그인만 요구(권한 역할 무관). 토큰이 없으면 "로그인이 필요합니다." throw.
36
+ - `auth(permissions, fn)` — `permissions: string[]` 역할 중 하나라도 토큰 `roles` 있어야 통과. 없으면 "권한이 부족합니다." throw. 빈 배열은 `auth(fn)` 과 동일(로그인만).
37
+ - **서비스 수준**: `defineService("User", auth((ctx) => ({ ... })))` — 그 서비스의 모든 메서드에 적용. `defineService` `authPermissions` 추출.
38
+ - **메서드 수준**: 반환 객체 개별 메서드를 `auth(...)` 감쌈. 메서드 수준 권한이 서비스 수준보다 우선.
39
+ - 적용 우선순위: 메서드 래핑 권한 → 없으면 서비스 권한. 서버 옵션 `auth` 가 `undefined` 면 권한 요구 메서드 호출 시 설정 오류 throw, `false` 면 인증 검사 자체를 스킵.
35
40
 
36
41
  ```ts
37
- export const UserService = defineService("User", auth((ctx) => ({
38
- getProfile: () => ctx.authInfo,
39
- removeAll: auth(["admin"], () => repo.removeAll()),
42
+ export const AdminService = defineService("Admin", auth((ctx) => ({
43
+ list: () => fetchUsers(), // 로그인만
44
+ remove: auth(["admin"], (id: string) => deleteUser(id)), // admin 역할 필요
40
45
  })));
41
46
  ```
42
47
 
43
48
  ## ServiceContext
44
49
 
45
- `interface ServiceContext<TAuthInfo>` 팩토리·메서드 안에서 호출 맥락에 접근.
46
-
47
- - `server: ServiceServer<TAuthInfo>` — 현재 서버 인스턴스(이벤트 emit·옵션 접근 등).
48
- - `socket?: ServiceSocket` — WebSocket 호출일 때의 소켓. HTTP/레거시 호출이면 undefined(소켓 의존 서비스는 이걸로 분기).
49
- - `http?: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> }` — HTTP 호출 컨텍스트.
50
- - `legacy?: { clientName?: string }` — V1 레거시 호출 컨텍스트.
51
- - `authInfo: TAuthInfo | undefined` (getter) — 인증 페이로드의 `data`(socket 우선, 없으면 http). 미인증이면 undefined. 결측을 `?? ""` 등으로 치환하지 말고 그대로 전파.
52
- - `clientName: string | undefined` (getter) — 클라이언트 이름(socket→http→legacy 순). 빈문자·`..`·`/`·`\` 포함 시 "유효하지 않은 클라이언트 이름" throw(경로탐색 가드).
53
- - `clientPath: string | undefined` (getter) — `<rootPath>/www/<clientName>` 절대경로. clientName 없으면 undefined.
54
- - `getConfig<T>(section): Promise<T>` — 루트 `.config.json` 과 클라이언트별 `.config.json` 을 merge 후 `section` 값 반환. 섹션 없으면 "설정 섹션을 찾을 수 없습니다" throw.
50
+ `factory` 요청마다 받는 컨텍스트. 메서드 안에서 인증·클라이언트·설정·서버에 접근하는 통로.
55
51
 
56
- ## ServiceMethods
57
-
58
- `type ServiceMethods<TDefinition> = TDefinition extends ServiceDefinition<infer M> ? M : never` — `ServiceDefinition` 에서 메서드 시그니처 타입만 추출. 클라이언트 측 타입 공유에 사용.
52
+ - `server: ServiceServer<TAuthInfo>` — 서버 인스턴스. `ctx.server.emitEvent(...)` 로 이벤트 발생, `ctx.server.signAuthToken(...)` 로 토큰 발급.
53
+ - `socket?: ServiceSocket` — WebSocket 요청일 때만 존재하는 소켓. HTTP 요청이면 `undefined`(소켓 필요한 기능은 존재 검사 필수).
54
+ - `http?: { clientName: string; authTokenPayload? }` — HTTP 요청일 때만 존재.
55
+ - `legacy?: { clientName? }` — V1 레거시 연결 컨텍스트(자동업데이트 전용).
56
+ - `get authInfo: TAuthInfo | undefined` — 검증된 토큰의 `data` 페이로드. 비로그인 요청이면 `undefined`(결측을 그대로 노출하므로 받는 쪽도 옵셔널로 다룰 것).
57
+ - `get clientName: string | undefined` — 요청 클라이언트 이름(소켓→HTTP→레거시 순 우선). 빈 문자열·`..`·슬래시(`/`,`\`) 포함 등 경로 탈출 위험 값이면 throw.
58
+ - `get clientPath: string | undefined` — `rootPath/www/<clientName>` 절대경로. clientName 없으면 `undefined`.
59
+ - `getConfig<T>(section: string): Promise<T>` — `rootPath/.config.json` 루트 설정에 클라이언트별 `www/<clientName>/.config.json` 을 머지한 뒤 `section` 키 값을 반환. 섹션이 없으면 throw. 설정 파일은 변경 시 자동 리로드(파일 워처 + 캐시).
59
60
 
60
61
  ```ts
61
- export type UserServiceType = ServiceMethods<typeof UserService>;
62
- // 클라이언트: client.getService<UserServiceType>("User");
62
+ export const ReportService = defineService("Report", auth((ctx) => ({
63
+ mine: () => loadReports(ctx.authInfo!.userId),
64
+ dbConfig: () => ctx.getConfig<DbConnConfig>("orm"),
65
+ })));
63
66
  ```
64
67
 
65
- ## 저수준 (직접 서버 조립·디스패치 시에만)
66
-
67
- - `getServiceAuthPermissions(fn): string[] | undefined` — 함수가 `auth()` 래핑됐으면 권한배열, 아니면 undefined.
68
- - `createServiceContext(server, socket?, http?, legacy?): ServiceContext` — 컨텍스트 수동 생성(전송 핸들러용). 인자별로 socket/http/legacy 출처를 지정.
69
- - `executeServiceMethod(server, { serviceName, methodName, params, socket?, http? }): Promise<unknown>` — 서비스 검색→clientName 가드→컨텍스트 생성→팩토리 호출→메서드 검색→인증 검사→실행. 전송 계층이 RPC 를 실제 디스패치하는 진입점. 서비스/메서드 미존재 시 throw.
68
+ ## ServiceDefinition / ServiceMethods / getServiceAuthPermissions
70
69
 
71
- ## 주의사항
70
+ - `ServiceDefinition<TMethods>` — `defineService` 반환 타입. `{ name: string; names: string[]; factory: (ctx) => TMethods; authPermissions?: string[] }`. `names` 는 별칭 전체, `authPermissions` 는 서비스 수준 `auth` 권한(없으면 `undefined`).
71
+ - `type ServiceMethods<TDefinition>` — `ServiceDefinition<M>` 에서 메서드 시그니처 `M` 만 추출하는 타입 유틸. 클라이언트와 서비스 타입을 공유하려고 common 패키지에 `export type XxxServiceMethods = ServiceMethods<typeof XxxService>` 로 재노출하고, 클라이언트는 `client.getService<XxxServiceMethods>("Xxx")` 로 사용.
72
+ - `getServiceAuthPermissions(fn: Function): string[] | undefined` — `auth(...)` 로 래핑된 함수에서 권한 배열을 읽음. 래핑 안 됐으면 `undefined`. 내부 실행기·커스텀 전송에서만 필요(일반 작성에서는 불필요).
72
73
 
73
- - `factory` 호출마다 실행되므로 호출 공유 상태(예: 소켓별 DB 연결) 팩토리 외부 `WeakMap` 등에 둔다(OrmService 참고).
74
- - 결측(authInfo/clientName)은 undefined 로 끝까지 전파. `?? ""` 등으로 치환 금지.
74
+ 주의: 클라이언트가 쓰는 서비스 이름 문자열과 `ServiceMethods` 타입은 단일 소스(`defineService` 이름 / `typeof XxxService`) 따른다. 호출부에서 이름·제네릭을 중복 정의하지 말 것.
@@ -1,51 +1,88 @@
1
- # @simplysm/service-server — transport-internals
1
+ # @simplysm/service-server — 전송 계층 내부
2
2
 
3
- `ServiceServer.listen()` 이 내부적으로 등록하는 저수준 전송·프로토콜 핸들러. 보통 직접 호출하지 않으며, 커스텀 서버를 손수 조립하거나 동작을 디버깅할 때만 참조한다.
3
+ `ServiceServer.listen()` 이 내부적으로 구성하는 저수준 전송·프로토콜·실행기. 일반 작성에서는 `createServiceServer` 가 알아서 엮으므로 직접 일이 없고, 커스텀 전송을 손수 조립하거나 동작을 테스트·디버깅·확장할 때만 참조한다.
4
4
 
5
- ## WebSocketHandler
5
+ ## executeServiceMethod
6
6
 
7
- `createWebSocketHandler(runMethod, jwtSecret): WebSocketHandler` — 다중 WebSocket 연결을 `clientId` 단위로 관리하고 메시지를 `runMethod`(보통 `executeServiceMethod` 바인딩)로 라우팅·이벤트 브로드캐스트.
7
+ ```ts
8
+ function executeServiceMethod(
9
+ server: ServiceServer,
10
+ def: {
11
+ serviceName: string;
12
+ methodName: string;
13
+ params: unknown[];
14
+ socket?: ServiceSocket;
15
+ http?: { clientName: string; authTokenPayload?: AuthTokenPayload };
16
+ },
17
+ ): Promise<unknown>
18
+ ```
8
19
 
9
- - `runMethod(def): Promise<unknown>` `{ serviceName, methodName, params, socket? }` 받아 RPC 결과 반환.
10
- - `jwtSecret: string | undefined` — `"auth"` 메시지로 들어온 토큰 검증용. 없으면 auth 메시지에서 "JWT Secret이 정의되지 않았습니다." throw.
20
+ 요청 1건을 실제 서비스 메서드로 라우팅·실행하는 핵심 게이트키퍼. WebSocket/HTTP 핸들러가 모두 함수로 수렴한다.
11
21
 
12
- `WebSocketHandler`:
22
+ - `serviceName`/`methodName` — `server.options.services` 에서 `names` 매칭으로 서비스를, 그 팩토리 산출 객체에서 메서드를 찾음. 없으면 throw.
23
+ - `params: unknown[]` — 메서드 인자 배열. 스프레드되어 메서드에 전달.
24
+ - `socket?` / `http?` — 둘 중 하나로 요청 출처 전달. `clientName` 에 `..`·슬래시가 있으면 보안 차단 throw.
25
+ - 동작: 컨텍스트 생성 → 팩토리 호출 → 메서드 조회 → 인증/권한 검사(`auth` 래핑 권한 기준) → 실행 후 반환값 반환. `auth: false` 면 인증 스킵, `auth` 미설정인데 권한 요구 메서드면 설정 오류 throw.
13
26
 
14
- - `addSocket(socket, clientId, clientName, connReq): void` — 연결 등록. 같은 `clientId` 의 기존 연결은 닫고 교체(동일 clientId 다중 동시 연결 불가).
15
- - `closeAll(): void` — 전 연결 종료.
16
- - `emit<TEventDef>(eventName, infoSelector, data): Promise<void>` — `infoSelector(info) === true` 인 리스너에 `evt:on` 푸시. `infoSelector` = 리스너 info 로 수신 대상 선별.
27
+ ## createServiceContext
17
28
 
18
- 처리 메시지: `"<Service>.<method>"`(RPC), `evt:add`/`evt:remove`/`evt:gets`/`evt:emit`(이벤트 리스너 등록·해제·조회·브로드캐스트), `auth`(토큰 등록). 미지원 메시지는 code `BAD_MESSAGE`, 처리 중 예외는 `INTERNAL_ERROR` 응답(`DEV` env 시 stack 포함).
29
+ ```ts
30
+ function createServiceContext<TAuthInfo>(
31
+ server, socket?, http?, legacy?,
32
+ ): ServiceContext<TAuthInfo>
33
+ ```
19
34
 
20
- ## ServiceSocket
35
+ `ServiceContext`(인증·클라이언트·설정 접근자) 인스턴스를 만든다. 인자 `socket`/`http`/`legacy` 중 들어온 것으로 `authInfo`·`clientName` 출처가 결정된다. 컨텍스트 필드 의미는 [service-authoring.md](./service-authoring.md) 의 ServiceContext 절 참조. 테스트에서 컨텍스트를 직접 만들어 서비스 메서드를 단위 호출할 때 유용.
21
36
 
22
- `createServiceSocket(socket, clientId, clientName, connReq): ServiceSocket` — 단일 WebSocket 연결 래퍼. 프로토콜 인코딩/디코딩, 5초 ping/pong keep-alive(무응답 시 terminate), 이벤트 리스너 추적.
37
+ ## ServiceSocket / createServiceSocket
23
38
 
24
- - `connectedAtDateTime: DateTime` / `clientName: string` / `connReq: FastifyRequest` 연결 메타(readonly).
25
- - `authTokenPayload?: AuthTokenPayload` — `auth` 메시지로 세팅되는 인증 페이로드. 읽기/쓰기 가능.
26
- - `close(): void` — 연결 terminate.
27
- - `send(uuid, msg): Promise<number>` — 메시지 인코딩 후 전송, 전송 바이트수 반환(소켓 미개방 시 0).
28
- - `addListener(key, eventName, info): void` — key/이벤트명/info 로 리스너 등록.
29
- - `removeListener(key): void` — key 로 리스너 제거.
30
- - `getEventListeners(eventName): Array<{ key; info }>` — 해당 이벤트명 리스너 전체 조회.
31
- - `filterEventTargetKeys(targetKeys): string[]` — 이 소켓에 존재하는 대상 key 만 필터.
32
- - `on("error"|"close"|"message", handler): void` — 핸들러 등록. `error` → `(err)`, `close` → `(code)`, `message` → `({ uuid, msg })`.
39
+ 단일 WebSocket 연결을 감싸 프로토콜 인코딩·ping/pong 연결 유지·이벤트 리스너 추적을 담당하는 인터페이스. `createServiceSocket(socket, clientId, clientName, connReq)` 로 생성.
33
40
 
34
- ## HTTP 핸들러
41
+ - `connectedAtDateTime: DateTime` / `clientName: string` / `connReq: FastifyRequest` — 연결 시각·클라이언트 이름·원본 요청.
42
+ - `authTokenPayload?: AuthTokenPayload` — 소켓 `auth` 메시지로 검증된 토큰. 이후 그 소켓 요청의 `ctx.authInfo` 출처.
43
+ - `close()` — 소켓 즉시 종료(terminate).
44
+ - `send(uuid, msg): Promise<number>` — 서버 메시지를 프로토콜로 인코딩해 전송, 보낸 바이트 수 반환.
45
+ - `addListener(key, eventName, info)` / `removeListener(key)` — 이벤트 구독 등록·해제. `key` 는 구독 식별자, `info` 는 selector 매칭용 메타.
46
+ - `getEventListeners(eventName)` — 해당 이벤트의 구독 `{ key, info }[]` 조회.
47
+ - `filterEventTargetKeys(targetKeys)` — 주어진 키 중 이 소켓에 존재하는 것만 반환.
48
+ - `on("error" | "close" | "message", handler)` — 소켓 이벤트 후킹. 5초 주기 ping, pong 미수신 시 자동 terminate.
35
49
 
36
- - `handleHttpRequest(req, reply, jwtSecret, runMethod): Promise<void>` — `/api/:service/:method` 처리. `x-sd-client-name` 헤더 필수(없으면 throw). `Authorization: Bearer <t>` 있으면 검증(실패 시 401). GET 은 `?json=` 쿼리, POST 는 배열 본문에서 params 추출(POST 비배열 400, 그 외 메서드 405).
37
- - `handleUpload(req, reply, rootPath, jwtSecret): Promise<void>` — `/upload` multipart 처리. 인증 필수(토큰 없음·검증 실패 시 401). 파일을 `www/uploads/<uuid><ext>` 로 저장하고 `ServiceUploadResult[]`(path/filename/size) 반환. 멀티파트 아니면 400. 도중 실패 시 이미 저장한 파일·불완전 파일 전부 삭제 후 500(원자성).
38
- - `handleStaticFile(req, reply, rootPath, urlPath): Promise<void>` — `www` 하위 정적 파일 서빙. `www` 밖 경로는 "접근이 거부되었습니다" throw(경로탐색 가드). 디렉토리는 슬래시 리다이렉트 후 `index.html`. `.` 시작 파일은 403. 없으면 404, 그 외 500(HTML 에러 페이지).
50
+ ## WebSocketHandler / createWebSocketHandler
39
51
 
40
- ## ServerProtocolWrapper
52
+ ```ts
53
+ function createWebSocketHandler(
54
+ runMethod: (def) => Promise<unknown>,
55
+ jwtSecret: string | undefined,
56
+ ): WebSocketHandler
57
+ ```
41
58
 
42
- `createServerProtocolWrapper(): ServerProtocolWrapper` 무거운 인코딩/디코딩을 공유 worker 스레드(지연 싱글턴) 자동 위임, 가벼운 건 메인 스레드.
59
+ 여러 `ServiceSocket` `clientId` 관리하고, 클라이언트 메시지를 `runMethod`(보통 `executeServiceMethod` 바인딩) 라우팅하며, 이벤트를 브로드캐스트한다.
43
60
 
44
- - `encode(uuid, message): Promise<{ chunks; totalSize }>` body `Uint8Array`(또는 Uint8Array 포함 배열)면 worker, 아니면 메인.
45
- - `decode(bytes): Promise<ServiceMessageDecodeResult>` 청크 재조립(stateful)은 항상 메인 단일 누적기, 재조립 완료 후 30KB 초과 JSON 파싱만 worker(청크 분산 재조립 버그 #35 방지).
46
- - `dispose(): void`프로토콜 리소스 해제.
61
+ - `addSocket(socket, clientId, clientName, connReq)` 연결 등록. 같은 `clientId` 기존 연결은 닫고 교체.
62
+ - `closeAll()`모든 연결 종료(서버 `close()` 호출).
63
+ - `emit<TEventDef>(eventName, infoSelector, data): Promise<void>` 소켓의 해당 이벤트 구독 중 `infoSelector(info)` 가 `true` 인 키에게만 `evt:on` 메시지 전송. `ServiceServer.emitEvent` 의 실제 구현.
64
+ - 처리하는 클라이언트 메시지 종류: `"<service>.<method>"`(RPC 호출), `evt:add`/`evt:remove`/`evt:gets`/`evt:emit`(이벤트 구독·조회·발생), `auth`(소켓 토큰 검증). 그 외는 `BAD_MESSAGE` 에러 응답. `DEV` 환경에서만 에러 스택 포함.
47
65
 
48
- ## 주의사항
66
+ ## HTTP / 정적 / 업로드 핸들러
49
67
 
50
- - `decode` 청크 재조립을 worker 분기하면 메시지의 청크가 서로 다른 누적기로 흩어져 재조립이 완성되지 못함 — 메인 스레드 누적기 유지가 필수.
51
- - 같은 `clientId` 재연결 시 이전 소켓이 강제 종료되므로 동일 `clientId` 다중 동시 연결은 불가.
68
+ `fastify` 라우트에 직접 물리는 저수준 함수들. 커스텀 라우트를 때만 직접 사용.
69
+
70
+ - `handleHttpRequest(req, reply, jwtSecret, runMethod)` — `/api/:service/:method` 처리. `x-sd-client-name` 헤더 필수, `Authorization: Bearer <token>` 검증(실패 시 401), GET 은 `?json=` 쿼리, POST 는 배열 본문에서 파라미터를 받아 `runMethod` 실행. 그 외 메서드는 405.
71
+ - `handleUpload(req, reply, rootPath, jwtSecret)` — `/upload` multipart 처리. 인증 토큰 필수(없거나 무효면 401). 파일을 `rootPath/www/uploads/<uuid><ext>` 로 저장하고 `ServiceUploadResult[]`(`{ path, filename, size }`) 반환. 도중 실패 시 그 요청에서 저장한 파일을 모두 롤백 삭제 후 500.
72
+ - `handleStaticFile(req, reply, rootPath, urlPath)` — `rootPath/www` 하위 정적 파일 전송. `www` 밖 경로는 차단(throw), 디렉터리면 슬래시 리다이렉트 후 `index.html`, `.` 으로 시작하는 숨김 파일은 403, 미존재는 404 HTML 응답.
73
+
74
+ ## ServerProtocolWrapper / createServerProtocolWrapper
75
+
76
+ 메시지 인코딩/디코딩을 크기·내용에 따라 worker 스레드와 메인 스레드로 자동 분배하는 래퍼. `createServerProtocolWrapper()` 로 생성(worker 는 지연 싱글턴).
77
+
78
+ - `encode(uuid, message): Promise<{ chunks: Bytes[]; totalSize: number }>` — `body` 에 `Uint8Array` 가 있으면 worker, 아니면 메인 스레드에서 인코딩.
79
+ - `decode(bytes): Promise<ServiceMessageDecodeResult>` — 청크 재조립(stateful)은 항상 메인 스레드 단일 누적기에서, 재조립 완료 후 30KB 초과 JSON 파싱(stateless)만 worker 위임. 진행 중이면 `{ type: "progress" }`, 완료면 `{ type: "complete", uuid, message }`.
80
+ - `dispose()` — 프로토콜 리소스 해제.
81
+
82
+ ## getConfig
83
+
84
+ ```ts
85
+ function getConfig<TConfig>(filePath: string): Promise<TConfig | undefined>
86
+ ```
87
+
88
+ `filePath` JSON 설정을 읽어 캐시·파일워치한다. `ServiceContext.getConfig` 의 내부 구현. 캐시 히트 시 즉시 반환(접근 시 만료 시간 갱신), 파일 변경 시 자동 리로드, 1시간 무접근 시 캐시·워처 GC. 파일이 없으면 `undefined`.
@@ -1,50 +1,49 @@
1
- # @simplysm/service-server — v1-legacy
1
+ # @simplysm/service-server — V1 레거시 지원
2
2
 
3
- ver2(구버전) WebSocket 클라이언트를 받기 위한 레거시 핸들러. 주로 구버전 앱의 자동업데이트(`SdAutoUpdateService.getLastVersion`) 요청을 처리한다. `ServiceServer` 는 ver=2 가 아닌 연결을 자동으로 이 핸들러로 넘긴다(`AutoUpdate` 서비스나 `legacyV1Handlers` 가 있을 때만, 둘 다 없으면 연결 거부).
3
+ `ver !== "2"`(구버전) WebSocket 클라이언트를 받기 위한 레거시 핸들러. 주로 구버전 앱의 자동업데이트(`SdAutoUpdateService.getLastVersion`) 요청을 처리한다. `ServiceServer` 는 ver=2 가 아닌 연결을 자동으로 이 핸들러로 넘긴다(`AutoUpdate` 서비스나 `legacyV1Handlers` 가 있을 때만, 둘 다 없으면 연결 거부). `ServiceServerOptions.legacyV1Handlers` 로 커스텀 핸들러를 끼울 때만 직접 다룬다.
4
4
 
5
5
  ## handleV1Connection
6
6
 
7
- - `handleV1Connection(socket, autoUpdateMethods: V1AutoUpdateMethods, clientNameSetter?): void`
8
- - `handleV1Connection(socket, options: V1ConnectionOptions): void`
7
+ ```ts
8
+ function handleV1Connection(socket, autoUpdateMethods: V1AutoUpdateMethods, clientNameSetter?): void;
9
+ function handleV1Connection(socket, options: V1ConnectionOptions): void;
10
+ ```
9
11
 
10
- 연결 즉시 `{ name: "connected" }` 전송 후, JSON 메시지(`V1Request`)를 받아 처리:
12
+ V1 WebSocket 연결 1건을 받아 연결 알림(`{ name: "connected" }`) 전송 메시지를 처리한다. 처리 순서: 커스텀 핸들러들 → (미처리 시) `SdAutoUpdateService.getLastVersion` fallback → 그래도 미처리면 `UPGRADE_REQUIRED` 에러 응답. 메시지 파싱·처리 중 예외는 잡아 warn 로그만 남기고 응답하지 않음.
11
13
 
12
- 1. 등록된 `handlers` 순서대로 실행, `handled: true` 결과로 응답.
13
- 2. 미처리이고 command `"SdAutoUpdateService.getLastVersion"` 이면 자동업데이트 fallback 실행.
14
- 3. 그 외엔 `{ message: "앱 업그레이드가 필요합니다.", code: "UPGRADE_REQUIRED" }` 에러 응답.
14
+ - `socket: WebSocket` `ws` 원시 소켓.
15
+ - 2번째 인자: `V1AutoUpdateMethods` 객체(자동업데이트만 응대)이거나 `V1ConnectionOptions`(핸들러·팩토리 포함). `"getLastVersion" in arg` 분기.
16
+ - `clientNameSetter?` 시그니처에서만. 요청의 `clientName` 외부에 통지하는 콜백.
15
17
 
16
- ## 타입
18
+ ## V1ConnectionOptions
17
19
 
18
- - `V1Request` — `{ uuid: string; command: string; params: unknown[]; clientName?: string }`. 클라이언트 요청.
19
- - `V1Response` — `{ name: "response"; reqUuid: string; state: "success" | "error"; body: unknown }`. 서버 응답 형식. `state` = 처리 결과("success" = 정상, "error" = 실패).
20
- - `V1AutoUpdateMethods` — `{ getLastVersion(platform): Promise<unknown> | unknown }`. 자동업데이트 fallback 구현.
21
- - `V1RequestHandlerResult` — `{ handled: true; state?: "success"|"error"; body }`(처리함, state 기본 success) 또는 `{ handled: false }`(다음 핸들러로 넘김).
22
- - `V1RequestHandlerContext` — `{ request: V1Request; serviceContext: ServiceContext }`. 핸들러 인자.
23
- - `V1RequestHandler` — `(ctx: V1RequestHandlerContext) => V1RequestHandlerResult | Promise<V1RequestHandlerResult>`. 커스텀 처리 함수. `ServiceServerOptions.legacyV1Handlers` 등록.
24
- - `V1ConnectionOptions`:
25
- - `serviceContext?: ServiceContext` — 핸들러에 넘길 고정 컨텍스트.
26
- - `serviceContextFactory?: (request) => ServiceContext` — 요청별 컨텍스트 생성(고정 대신 요청마다 만들 때).
27
- - `handlers?: V1RequestHandler[]` — 커스텀 핸들러 체인.
28
- - `autoUpdateMethods?: V1AutoUpdateMethods` — 자동업데이트 fallback 고정 구현.
29
- - `autoUpdateMethodsFactory?: (ctx) => V1AutoUpdateMethods` — 요청별 fallback 생성.
30
- - `clientNameSetter?: (clientName) => void` — 메시지마다 clientName 통지 콜백.
20
+ - `serviceContext?: ServiceContext` — 모든 요청에서 공유할 고정 컨텍스트.
21
+ - `serviceContextFactory?: (request: V1Request) => ServiceContext` 요청마다 컨텍스트를 새로 만들 때. `serviceContext` 보다 우선.
22
+ - `handlers?: V1RequestHandler[]` — 커스텀 요청 핸들러 목록. 앞에서부터 호출되며 첫 `handled: true` 에서 멈춤. 핸들러가 있는데 컨텍스트가 없으면 throw.
23
+ - `autoUpdateMethods?: V1AutoUpdateMethods` 자동업데이트 fallback 구현(고정).
24
+ - `autoUpdateMethodsFactory?: (ctx: V1RequestHandlerContext) => V1AutoUpdateMethods` — 요청마다 fallback 구현 생성. 지정 `autoUpdateMethods` 대신 사용.
25
+ - `clientNameSetter?: (clientName: string | undefined) => void` 요청 `clientName` 통지 콜백.
31
26
 
32
- ## 사용
27
+ ## V1RequestHandler 와 관련 타입
28
+
29
+ - `V1Request` — `{ uuid: string; command: string; params: unknown[]; clientName?: string }`. 구버전 클라이언트가 보내는 요청 형태.
30
+ - `V1Response` — `{ name: "response"; reqUuid: string; state: "success" | "error"; body: unknown }`. 서버가 돌려보내는 응답 형태(`state` 로 성공/에러 구분).
31
+ - `V1RequestHandlerContext` — `{ request: V1Request; serviceContext: ServiceContext }`. 핸들러가 받는 인자.
32
+ - `V1RequestHandlerResult` — `{ handled: true; state?: "success"|"error"; body: unknown } | { handled: false }`. `handled: false` 면 다음 핸들러·fallback 으로 넘어감, `true` 면 그 `state`(기본 `"success"`)·`body` 로 즉시 응답.
33
+ - `V1RequestHandler` — `(ctx: V1RequestHandlerContext) => V1RequestHandlerResult | Promise<V1RequestHandlerResult>`. 동기·비동기 모두 가능.
34
+ - `V1AutoUpdateMethods` — `{ getLastVersion: (platform: string) => Promise<unknown> | unknown }`. `SdAutoUpdateService.getLastVersion` 명령의 fallback 인터페이스.
33
35
 
34
36
  ```ts
35
- createServiceServer({
36
- // ...
37
- services: [AutoUpdateService], // ver2 연결의 getLastVersion fallback 자동 연결
37
+ const server = createServiceServer({
38
+ rootPath, port,
39
+ services: [AutoUpdateService], // ver!=2 연결 getLastVersion fallback 자동 연결
38
40
  legacyV1Handlers: [
39
- (ctx) =>
40
- ctx.request.command === "Legacy.ping"
41
+ ({ request, serviceContext }) =>
42
+ request.command === "Legacy.ping"
41
43
  ? { handled: true, body: "pong" }
42
44
  : { handled: false },
43
45
  ],
44
46
  });
45
47
  ```
46
48
 
47
- ## 주의사항
48
-
49
- - `handlers` 가 있는데 `serviceContext`(또는 factory)가 없으면 핸들러 실행 시 "serviceContext가 필요합니다." throw — 핸들러를 쓰려면 컨텍스트를 함께 제공.
50
- - 메시지 처리 중 예외는 warn 로그만 남기고 응답하지 않음(레거시 한정 동작).
49
+ 주의: `legacyV1Handlers` 도 없고 `AutoUpdate`(`SdAutoUpdateService`) 서비스도 등록 안 됐으면 ver=2 가 아닌 연결은 코드 1008 로 즉시 거부된다.