@simplysm/sd-claude 14.0.88 → 14.0.89

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 (122) hide show
  1. package/claude/references/sd-simplysm14/README.md +17 -17
  2. package/claude/references/sd-simplysm14/apis/angular/README.md +27 -53
  3. package/claude/references/sd-simplysm14/apis/angular/controls.md +37 -105
  4. package/claude/references/sd-simplysm14/apis/angular/crud.md +46 -43
  5. package/claude/references/sd-simplysm14/apis/angular/directives.md +22 -32
  6. package/claude/references/sd-simplysm14/apis/angular/features.md +40 -55
  7. package/claude/references/sd-simplysm14/apis/angular/infra.md +40 -40
  8. package/claude/references/sd-simplysm14/apis/angular/layout.md +25 -53
  9. package/claude/references/sd-simplysm14/apis/angular/overlay.md +70 -82
  10. package/claude/references/sd-simplysm14/apis/angular/routing-appstructure.md +44 -39
  11. package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +21 -36
  12. package/claude/references/sd-simplysm14/apis/angular/shared-data.md +52 -65
  13. package/claude/references/sd-simplysm14/apis/angular/sheet.md +65 -70
  14. package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +33 -35
  15. package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +7 -7
  16. package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +29 -29
  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 +13 -12
  21. package/claude/references/sd-simplysm14/apis/core-common/README.md +222 -98
  22. package/claude/references/sd-simplysm14/apis/core-common/array-ext.md +102 -53
  23. package/claude/references/sd-simplysm14/apis/core-common/async-runtime.md +128 -0
  24. package/claude/references/sd-simplysm14/apis/core-common/datetime.md +98 -64
  25. package/claude/references/sd-simplysm14/apis/core-common/errors.md +91 -0
  26. package/claude/references/sd-simplysm14/apis/core-common/json-transfer.md +34 -28
  27. package/claude/references/sd-simplysm14/apis/core-common/obj.md +104 -40
  28. package/claude/references/sd-simplysm14/apis/core-node/README.md +11 -8
  29. package/claude/references/sd-simplysm14/apis/core-node/consola.md +23 -31
  30. package/claude/references/sd-simplysm14/apis/core-node/cpx.md +33 -22
  31. package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +28 -25
  32. package/claude/references/sd-simplysm14/apis/core-node/fsx.md +39 -53
  33. package/claude/references/sd-simplysm14/apis/core-node/pathx.md +26 -29
  34. package/claude/references/sd-simplysm14/apis/core-node/worker.md +27 -29
  35. package/claude/references/sd-simplysm14/apis/excel/README.md +14 -14
  36. package/claude/references/sd-simplysm14/apis/lint/README.md +27 -21
  37. package/claude/references/sd-simplysm14/apis/lint/rules.md +89 -49
  38. package/claude/references/sd-simplysm14/apis/orm-common/README.md +5 -59
  39. package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +98 -67
  40. package/claude/references/sd-simplysm14/apis/orm-common/expr.md +107 -92
  41. package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +99 -65
  42. package/claude/references/sd-simplysm14/apis/orm-common/schema.md +83 -98
  43. package/claude/references/sd-simplysm14/apis/orm-common/types.md +62 -52
  44. package/claude/references/sd-simplysm14/apis/orm-node/README.md +62 -25
  45. package/claude/references/sd-simplysm14/apis/orm-node/db-conn.md +27 -27
  46. package/claude/references/sd-simplysm14/apis/sd-cli/README.md +12 -15
  47. package/claude/references/sd-simplysm14/apis/sd-cli/SdTsCompiler.md +92 -45
  48. package/claude/references/sd-simplysm14/apis/sd-cli/sd-config-types.md +226 -108
  49. package/claude/references/sd-simplysm14/apis/service-client/README.md +84 -86
  50. package/claude/references/sd-simplysm14/apis/service-client/orm.md +14 -11
  51. package/claude/references/sd-simplysm14/apis/service-client/transport.md +33 -10
  52. package/claude/references/sd-simplysm14/apis/service-common/README.md +37 -23
  53. package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +9 -9
  54. package/claude/references/sd-simplysm14/apis/service-common/protocol.md +13 -13
  55. package/claude/references/sd-simplysm14/apis/service-server/README.md +81 -65
  56. package/claude/references/sd-simplysm14/apis/service-server/service-authoring.md +32 -35
  57. package/claude/references/sd-simplysm14/apis/service-server/transport-internals.md +44 -33
  58. package/claude/references/sd-simplysm14/apis/service-server/v1-legacy.md +34 -45
  59. package/claude/references/sd-simplysm14/apis/storage/README.md +24 -18
  60. package/claude/skills/sd-demo/SKILL.md +6 -0
  61. package/claude/skills/sd-impl/SKILL.md +4 -7
  62. package/claude/skills/sd-spec/SKILL.md +31 -858
  63. package/claude/skills/sd-spec/references/spec-authoring.md +519 -0
  64. package/claude/workflows/sd-docs.js +84 -0
  65. package/package.json +1 -1
  66. package/claude/references/sd-simplysm14/apis/orm-common/query-builder.md +0 -29
  67. package/claude/skills/sd-demo/evals/fixtures/inventory-list/.specs/inventory/spec.md +0 -99
  68. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/package.json +0 -12
  69. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/index.ts +0 -3
  70. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inbound/inbound.list.ts +0 -150
  71. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inventory/inventory-master.list.ts +0 -143
  72. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/outbound/outbound.list.ts +0 -150
  73. package/claude/skills/sd-demo/evals/fixtures/inventory-list/pnpm-workspace.yaml +0 -2
  74. package/claude/skills/sd-demo/evals/fixtures/inventory-list/sd.config.ts +0 -12
  75. package/claude/skills/sd-demo/evals/golden.jsonl +0 -1
  76. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/package.json +0 -8
  77. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/src/.gitkeep +0 -0
  78. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tests/.gitkeep +0 -0
  79. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tsconfig.json +0 -10
  80. package/claude/skills/sd-dev/evals/golden.jsonl +0 -1
  81. package/claude/skills/sd-docs/SKILL.md +0 -46
  82. package/claude/skills/sd-docs/evals/fixtures/new-write/.claude/references/sd-simplysm14/README.md +0 -7
  83. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/bar/package.json +0 -5
  84. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/bar/src/index.ts +0 -3
  85. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/baz/package.json +0 -6
  86. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/baz/src/index.ts +0 -1
  87. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/foo/package.json +0 -5
  88. package/claude/skills/sd-docs/evals/fixtures/new-write/packages/foo/src/index.ts +0 -8
  89. package/claude/skills/sd-docs/evals/fixtures/update-mixed/.claude/references/sd-simplysm14/README.md +0 -7
  90. package/claude/skills/sd-docs/evals/fixtures/update-mixed/.claude/references/sd-simplysm14/apis/foo/README.md +0 -3
  91. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/bar/package.json +0 -5
  92. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/bar/src/index.ts +0 -3
  93. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/baz/package.json +0 -6
  94. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/baz/src/index.ts +0 -1
  95. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/foo/package.json +0 -5
  96. package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/foo/src/index.ts +0 -8
  97. package/claude/skills/sd-docs/evals/golden.jsonl +0 -2
  98. package/claude/skills/sd-impl/evals/fixtures/case-a-new-screen/.specs/260513120000_warehouse/spec.md +0 -101
  99. package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/.specs/260513120000_warehouse/spec.md +0 -101
  100. package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/packages/app/src/screens/box-register/box-register.view.ts +0 -46
  101. package/claude/skills/sd-impl/evals/fixtures/case-c-new-cross/.specs/260513120000_warehouse/spec.md +0 -89
  102. package/claude/skills/sd-impl/evals/fixtures/case-d-spec-modify/.specs/260513120000_warehouse/spec.md +0 -101
  103. package/claude/skills/sd-impl/evals/golden.jsonl +0 -4
  104. package/claude/skills/sd-manual/evals/fixtures/new-manual/src/notification.ts +0 -25
  105. package/claude/skills/sd-manual/evals/fixtures/update-manual/.claude/references/sd-simplysm14/manuals/notification.md +0 -14
  106. package/claude/skills/sd-manual/evals/fixtures/update-manual/src/notification.ts +0 -37
  107. package/claude/skills/sd-manual/evals/golden.jsonl +0 -2
  108. package/claude/skills/sd-review/evals/fixtures/code-review/src/foo.ts +0 -7
  109. package/claude/skills/sd-review/evals/fixtures/doc-review/docs/foo.md +0 -4
  110. package/claude/skills/sd-review/evals/golden.jsonl +0 -2
  111. package/claude/skills/sd-skill/evals/fixtures/existing-skill/.claude/skills/todo-format/SKILL.md +0 -14
  112. package/claude/skills/sd-skill/evals/fixtures/new-skill/.gitkeep +0 -0
  113. package/claude/skills/sd-skill/evals/golden.jsonl +0 -2
  114. package/claude/skills/sd-spec/evals/fixtures/case-a-split//355/232/214/354/235/230/353/241/235.md +0 -20
  115. package/claude/skills/sd-spec/evals/fixtures/case-b-detail/.specs/260513120000_warehouse/spec.md +0 -95
  116. package/claude/skills/sd-spec/evals/golden.jsonl +0 -2
  117. package/claude/skills/sd-unpack/evals/fixtures/eml-with-text-attachment/meeting.eml +0 -21
  118. package/claude/skills/sd-unpack/evals/fixtures/simple-eml/meeting.eml +0 -10
  119. package/claude/skills/sd-unpack/evals/golden.jsonl +0 -2
  120. package/claude/skills/sd-use/evals/fixtures/empty/.gitkeep +0 -0
  121. package/claude/skills/sd-use/evals/golden.jsonl +0 -6
  122. /package/claude/{skills/sd-docs/references/doc-rules.md → workflows/sd-docs.rules.md} +0 -0
@@ -1,17 +1,17 @@
1
1
  # @simplysm/service-common — protocol
2
2
 
3
- 서버·클라이언트 간 서비스 메시지의 바이너리 인코딩/디코딩과 청크 재조립을 담당하는 프로토콜(V2). 헤더 28바이트(UUID 16 + TotalSize 8 + Index 4) + JSON 본문, 3MB 초과 시 300KB 청크로 자동 분할, 최대 100MB.
3
+ 서버·클라이언트 간 서비스 메시지의 바이너리 인코딩/디코딩과 청크 재조립을 담당하는 프로토콜(V2). 헤더 28바이트(UUID 16 + TotalSize 8 + Index 4) + JSON 본문 구조이며, 3MB 초과 시 300KB 청크로 자동 분할, 단일 메시지 최대 100MB.
4
4
 
5
5
  ## createServiceProtocol / ServiceProtocol
6
6
 
7
- `createServiceProtocol(): ServiceProtocol` — stateful 청크 누적기를 내장한 프로토콜 인스턴스 생성. 누적기는 GC 타이머를 가지므로 사용 종료 시 `dispose()` 필수.
7
+ `createServiceProtocol(): ServiceProtocol` — stateful 청크 누적기(`LazyGcMap`)를 내장한 프로토콜 인스턴스 생성. 누적기는 GC 타이머를 가지므로 사용 종료 시 `dispose()` 필수.
8
8
 
9
9
  `ServiceProtocol` 메서드:
10
10
 
11
- - `encode(uuid, message): { chunks: Bytes[]; totalSize: number }` — 메시지를 `[name, body]` JSON→바이트로 직렬화 후 헤더 부착. `SPLIT_MESSAGE_SIZE`(3MB) 이하면 단일 청크, 초과면 `CHUNK_SIZE`(300KB) 단위 분할. `MAX_TOTAL_SIZE`(100MB) 초과 시 `ArgumentError` throw. `uuid` 는 메시지 묶음 식별자(재조립 키).
12
- - `accumulate(bytes): ServiceAccumulateResult` — 수신 청크 1개를 uuid별 누적기에 모음(stateful, 재조립 전용). 같은 index 중복 패킷은 무시. JSON 파싱은 함. 미완성이면 `progress`, 전 청크 도착 시 raw 바이트 담은 `complete` 반환. 헤더 미만(<28B)/크기 초과/무결성 위반(completedSize > totalSize) 시 throw.
13
- - `parseMessage(resultBytes): ServiceMessage` — 재조립된 raw 바이트를 메시지 객체로 파싱(stateless). 누적 상태 비의존이라 worker 등 다른 컨텍스트에 위임 가능. 파싱 실패 시 `ArgumentError` throw.
14
- - `decode<T>(bytes): ServiceMessageDecodeResult<T>` — `accumulate` 후 완료 시 `parseMessage` 까지 수행하는 통합 동작. 가장 일반적인 수신 처리 경로.
11
+ - `encode(uuid: string, message: ServiceMessage): { chunks: Bytes[]; totalSize: number }` — 메시지를 `[name, body]` JSON→바이트로 직렬화 후 28바이트 헤더 부착. `SPLIT_MESSAGE_SIZE`(3MB) 이하면 단일 청크, 초과면 `CHUNK_SIZE`(300KB) 단위로 분할해 여러 청크. `MAX_TOTAL_SIZE`(100MB) 초과 시 `ArgumentError` throw. `uuid`=메시지 묶음 식별자(재조립 키).
12
+ - `accumulate(bytes: Bytes): ServiceAccumulateResult` — 수신 청크 1개를 uuid별 누적기에 모음(stateful, 재조립 전용). 같은 index 중복 패킷은 무시. JSON 파싱은 하지 않음. 미완성이면 `progress`, 전 청크 도착 시 raw 바이트 담은 `complete` 반환. 헤더 미만(<28B)·크기 초과·무결성 위반(completedSize > totalSize) 시 `ArgumentError` throw.
13
+ - `parseMessage(resultBytes: Bytes): ServiceMessage` — 재조립된 raw 바이트를 메시지 객체로 파싱(stateless). 누적 상태에 비의존이라 worker 등 다른 실행 컨텍스트에 위임 가능. 파싱 실패 시 `ArgumentError` throw.
14
+ - `decode<T extends ServiceMessage>(bytes: Bytes): ServiceMessageDecodeResult<T>` — `accumulate` 후 완료 시 `parseMessage` 까지 수행하는 통합 동작. 가장 일반적인 수신 처리 경로.
15
15
  - `dispose(): void` — 내부 누적기 GC 타이머 해제·메모리 반환. 인스턴스 폐기 전 반드시 호출.
16
16
 
17
17
  ```ts
@@ -24,8 +24,8 @@ try {
24
24
  } finally { proto.dispose(); }
25
25
  ```
26
26
 
27
- `ServiceMessageDecodeResult<T>` (유니언, `type` 판별):
28
- - `{ type: "complete"; uuid; message: T }` — 전 청크 수신, 메시지 재조립·파싱 완료.
27
+ `ServiceMessageDecodeResult<TMessage>` (유니언, `type` 판별):
28
+ - `{ type: "complete"; uuid; message: TMessage }` — 전 청크 수신, 메시지 재조립·파싱 완료.
29
29
  - `{ type: "progress"; uuid; totalSize; completedSize }` — 일부 청크만 도착. 진행률 표시용.
30
30
 
31
31
  `ServiceAccumulateResult` (유니언, `type` 판별):
@@ -43,9 +43,9 @@ try {
43
43
 
44
44
  - `MAX_TOTAL_SIZE: 100MB` — 단일 메시지 허용 최대 크기. 초과 시 `encode`/`accumulate` throw.
45
45
  - `SPLIT_MESSAGE_SIZE: 3MB` — 이 값 초과 시 청킹 시작(이하면 단일 청크).
46
- - `CHUNK_SIZE: 300KB` — 분할 청크 1 본문 크기.
46
+ - `CHUNK_SIZE: 300KB` — 분할 청크 1개의 본문 크기.
47
47
  - `GC_INTERVAL: 10초` — 미완성 누적기 정리 주기.
48
- - `EXPIRE_TIME: 60초` — 미완성 메시지 만료 시간.
48
+ - `EXPIRE_TIME: 60초` — 미완성 메시지 만료 시간(이후 GC 대상).
49
49
 
50
50
  ## 메시지 타입
51
51
 
@@ -61,9 +61,9 @@ try {
61
61
  - `ServiceProgressMessage` `"progress"` — 서버가 청크 수신 진행 알림. `body: { totalSize, completedSize }`(바이트).
62
62
  - `ServiceErrorMessage` `"error"` — 서버 에러 알림. `body: { name, message, code, stack?, detail?, cause? }`.
63
63
  - `ServiceAuthMessage` `"auth"` — 클라이언트 인증. `body: string`(토큰).
64
- - `ServiceRequestMessage` `` `${string}.${string}` `` — 클라이언트 서비스 메서드 호출(`service.method`). `body: unknown[]`(매개변수).
65
- - `ServiceResponseMessage` `"response"` — 서버 응답. `body?: unknown`(결과).
66
- - `ServiceAddEventListenerMessage` `"evt:add"` — 리스너 등록. `body: { key, name, info }` — `key`=리스너 키(uuid, 제거에 필요), `name`=이벤트 이름, `info`=발생 필터링용 정보.
64
+ - `ServiceRequestMessage` `` `${string}.${string}` `` — 클라이언트 서비스 메서드 호출(`service.method`). `body: unknown[]`(매개변수 배열).
65
+ - `ServiceResponseMessage` `"response"` — 서버 응답. `body?: unknown`(결과, 없을 수 있어 optional).
66
+ - `ServiceAddEventListenerMessage` `"evt:add"` — 리스너 등록. `body: { key, name, info }` — `key`=리스너 키(uuid, 제거에 필요), `name`=이벤트 이름, `info`=발생 필터링용 정보.
67
67
  - `ServiceRemoveEventListenerMessage` `"evt:remove"` — 리스너 제거. `body: { key }`(리스너 키).
68
68
  - `ServiceGetEventListenerInfosMessage` `"evt:gets"` — 특정 이벤트 리스너 info 목록 요청. `body: { name }`(이벤트 이름).
69
69
  - `ServiceEmitEventMessage` `"evt:emit"` — 클라이언트가 이벤트 발생 요청. `body: { keys, data }` — `keys`=대상 리스너 키 목록, `data`=데이터.
@@ -1,102 +1,118 @@
1
1
  # @simplysm/service-server
2
2
 
3
- Fastify 기반 서비스 서버. WebSocket(v2)/HTTP RPC, JWT 인증, 정적 파일·업로드, 서버→클라이언트 이벤트 푸시를 서버로 제공한다. 클라이언트는 `defineService` 정의한 서비스의 메서드를 원격 호출한다.
3
+ Fastify 기반 서비스 서버. WebSocket/HTTP 전송 계층으로 RPC 스타일 서비스 메서드를 노출하고, 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
+ - **ServiceServer / createServiceServer / ServiceServerOptions** — 서버 인스턴스를 만들고 listen/close 때, 포트·SSL·auth·서비스 목록을 설정할 때. (아래 "서버 인스턴스" 인라인)
8
+ - **이벤트 브로드캐스트 (getEvent / emitEvent / ServerEventProxy)** — 서버에서 WebSocket 클라이언트들에게 이벤트를 푸시할 때. (아래 "이벤트 브로드캐스트" 인라인)
9
+ - **JWT 인증 (signAuthToken/verifyAuthToken, signJwt/verifyJwt/decodeJwt, AuthTokenPayload)** — 로그인 토큰을 발급·검증할 때. (아래 "JWT 인증" 인라인)
10
+ - **서비스 정의 (defineService / auth / ServiceContext / ServiceDefinition / ServiceMethods)** — 서버에 노출할 RPC 서비스를 작성하고 인증·권한을 거는 작업 컨텍스트. 자세히: [service-authoring.md](./service-authoring.md)
11
+ - **내장 서비스 (OrmService / AutoUpdateService)** — DB 원격 실행·앱 자동업데이트를 services 목록에 바로 꽂을 때. (아래 "내장 서비스" 인라인)
12
+ - **전송 계층 내부 (WebSocketHandler / ServiceSocket / HTTP·업로드·정적 핸들러 / 프로토콜 래퍼 / ConfigManager)** — 서버 내부 동작을 이해하거나 커스텀 통합할 때. 자세히: [transport-internals.md](./transport-internals.md)
13
+ - **V1 레거시 자동업데이트 (handleV1Connection 등)** — 구버전(ver≠2) 클라이언트를 지원해야 때. 자세히: [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
+ ```ts
18
+ class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{ ready: void; close: void }>
19
+ function createServiceServer<TAuthInfo = unknown>(options: ServiceServerOptions): ServiceServer<TAuthInfo>
20
+ ```
18
21
 
19
- `class ServiceServer<TAuthInfo> extends EventEmitter<{ ready: void; close: void }>`:
22
+ `createServiceServer` 는 `new ServiceServer` 의 얇은 래퍼. `TAuthInfo` 인증 토큰 `data` 필드의 타입(`ctx.authInfo` 와 토큰 발급/검증에 전파됨).
20
23
 
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.
24
+ `ServiceServerOptions`:
30
25
 
31
- `ServerEventProxy<TEventDef>`:
26
+ - `rootPath: string` — 서버 루트 디렉토리. 정적 파일은 `<rootPath>/www`, 업로드는 `<rootPath>/www/uploads`, 설정은 `<rootPath>/.config.json` 및 `<rootPath>/www/<clientName>/.config.json` 에서 읽음.
27
+ - `port: number` — 리슨 포트. host 는 항상 `0.0.0.0`. `0` 이면 OS 가 임의 포트 배정(테스트용).
28
+ - `ssl?: { pfxBytes: Uint8Array; passphrase: string }` — HTTPS 인증서. 지정 시 HTTPS 구동 + HSTS·crossOriginOpenerPolicy 활성, 미지정 시 HTTP 구동 + `upgrade-insecure-requests` CSP 해제. PFX 형식 인증서만 지원.
29
+ - `auth?: { jwtSecret: string } | false` — 인증 모드. `{ jwtSecret }` = JWT 검증 활성, `false` = auth 요구 서비스가 있어도 인증 검사 스킵(의도적 비활성화), 미지정(undefined) = auth 요구 서비스가 하나라도 있으면 `listen()` 시 throw.
30
+ - `services: ServiceDefinition[]` — 노출할 서비스 목록. `defineService` 결과를 나열.
31
+ - `legacyV1Handlers?: V1RequestHandler[]` — V1 레거시 클라이언트용 커스텀 요청 핸들러. 자세히: [v1-legacy.md](./v1-legacy.md).
32
32
 
33
- - `emit(infoSelector: (info: TEventDef["$info"]) => boolean, data: TEventDef["$data"]): Promise<void>` — `getEvent` 가 반환하는 emit 헬퍼. `emitEvent` 와 동작 동일하되 eventDef 가 이미 바인딩됨.
33
+ 메서드:
34
+
35
+ - `listen(): Promise<void>` — Fastify 플러그인(websocket/helmet/multipart/static/cors) 등록 후 리슨 시작. auth 미설정인데 auth 요구 서비스가 있으면 throw. SIGINT/SIGTERM graceful shutdown 핸들러 등록(10초 내 미종료 시 강제 종료). 완료 시 `isOpen=true` + `ready` 이벤트 발생.
36
+ - `close(): Promise<void>` — 모든 WebSocket 연결 종료 + Fastify 종료. `isOpen=false` + `close` 이벤트 발생.
37
+ - `isOpen: boolean` — 현재 리슨 중 여부.
38
+ - `fastify: FastifyInstance` — 내부 Fastify 인스턴스(예: `fastify.server.address()` 로 실제 포트 조회).
39
+ - `options: ServiceServerOptions` — 생성 시 전달한 옵션(읽기 전용 참조).
34
40
 
35
41
  ```ts
36
- const server = createServiceServer<MyAuth>({
37
- rootPath: process.cwd(),
42
+ const server = createServiceServer<MyAuthInfo>({
43
+ rootPath: import.meta.dirname,
38
44
  port: 50080,
39
- auth: { jwtSecret: env("JWT_SECRET")! },
40
- services: [OrmService, AutoUpdateService, MyService],
45
+ auth: { jwtSecret: "secret" },
46
+ services: [MyService, OrmService, AutoUpdateService],
41
47
  });
42
48
  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
49
  ```
46
50
 
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 구버전 클라이언트용 커스텀 핸들러. 미지정 시 빈 배열.
51
+ ## 이벤트 브로드캐스트
55
52
 
56
- ## 인증 토큰 (AuthTokenPayload / signJwt / verifyJwt / decodeJwt)
53
+ ```ts
54
+ interface ServerEventProxy<TEventDef extends ServiceEventDef> {
55
+ emit(infoSelector: (item: TEventDef["$info"]) => boolean, data: TEventDef["$data"]): Promise<void>;
56
+ }
57
+ server.getEvent<TEventDef>(eventDef: TEventDef): ServerEventProxy<TEventDef>
58
+ server.emitEvent<TEventDef>(eventDef, infoSelector, data): Promise<void>
59
+ ```
57
60
 
58
- `ServiceServer.signAuthToken`/`verifyAuthToken` 의 저수준 구현. 서버 밖에서 토큰을 직접 다룰 사용. HS256, 발급시각 자동, 만료 12시간 고정.
61
+ `ServiceEventDef` 는 `@simplysm/service-common` 이벤트 정의 타입(`eventName`/`$info`/`$data` 보유). 클라이언트는 이벤트 리스너 등록 `info` 같이 보내고, 서버는 등록된 모든 소켓의 리스너 중 `infoSelector` 가 true 인 대상에게만 `data` 를 푸시한다.
59
62
 
60
- `interface AuthTokenPayload<TAuthInfo> extends jose.JWTPayload`:
63
+ - `infoSelector: (item) => boolean` — 수신 대상 필터. 등록된 각 리스너의 `info` 를 받아 전송 여부를 결정. 특정 조건(예: 같은 화면을 보는 클라이언트)에만 보낼 때 사용.
64
+ - `getEvent` 는 `emit` 만 노출하는 프록시를 반환(내부적으로 `emitEvent` 호출) — 같은 eventDef 로 여러 번 emit 할 때 편함.
61
65
 
62
- - `roles: string[]` — 권한 역할 목록. `auth(["admin"], ...)` 의 권한 검사 대상.
63
- - `data: TAuthInfo` — 애플리케이션 인증 정보. `ctx.authInfo` 로 노출됨.
66
+ ```ts
67
+ const evt = server.getEvent(MyDataChangedEvent);
68
+ await evt.emit((info) => info.boardId === 3, { updatedAt: new Date() });
69
+ ```
64
70
 
65
- - `signJwt<TAuthInfo>(jwtSecret, payload): Promise<string>` — HS256·발급시각 자동·만료 12h 로 서명.
66
- - `verifyJwt<TAuthInfo>(jwtSecret, token): Promise<AuthTokenPayload<TAuthInfo>>` — 검증·디코드. 만료 시 "토큰이 만료되었습니다.", 그 외 실패 시 "유효하지 않은 토큰입니다." throw.
67
- - `decodeJwt<TAuthInfo>(token): AuthTokenPayload<TAuthInfo>` — 서명 검증 없이 페이로드만 디코드.
71
+ ## JWT 인증
68
72
 
69
73
  ```ts
70
- const token = await signJwt(secret, { roles: ["admin"], data: { userId: "u1" } });
71
- const payload = await verifyJwt<MyAuth>(secret, token); // payload.data, payload.roles
74
+ interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
75
+ roles: string[];
76
+ data: TAuthInfo;
77
+ }
78
+ server.signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>
79
+ server.verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>
80
+
81
+ function signJwt<T>(jwtSecret: string, payload: AuthTokenPayload<T>): Promise<string>
82
+ function verifyJwt<T>(jwtSecret: string, token: string): Promise<AuthTokenPayload<T>>
83
+ function decodeJwt<T>(token: string): AuthTokenPayload<T>
72
84
  ```
73
85
 
74
- ## 빌트인 서비스 (OrmService / AutoUpdateService)
86
+ - `AuthTokenPayload.roles: string[]` — 보유 역할 목록. `auth(["admin"], ...)` 권한 검사 시 이 배열에 해당 권한이 포함되는지 확인.
87
+ - `AuthTokenPayload.data: TAuthInfo` — 임의 사용자 정보. 서비스 메서드에서 `ctx.authInfo` 로 읽힘.
88
+ - `signAuthToken`/`verifyAuthToken` — 서버 옵션의 `jwtSecret` 을 자동 사용하는 인스턴스 메서드. jwtSecret 미설정 시 throw.
89
+ - `signJwt` — HS256, 발급시각 자동 설정, **만료 12시간 고정**. secret 은 UTF-8 로 인코딩됨.
90
+ - `verifyJwt` — 검증 실패 시 만료면 `"토큰이 만료되었습니다."`, 그 외엔 `"유효하지 않은 토큰입니다."` throw(jose 에러 코드 `ERR_JWT_EXPIRED` 로 만료 여부 구분).
91
+ - `decodeJwt` — **서명 검증 없이** 페이로드만 디코드. 신뢰할 수 없는 토큰 검증 용도로는 쓰지 말 것.
92
+
93
+ ## 내장 서비스
75
94
 
76
- `services` 배열에 넣어 등록. 서비스는 별칭 2개로 노출된다.
95
+ `defineService` 결과 상수. `services` 목록에 그대로 추가해 사용. 다 이름 별칭(`["Orm","SdOrmService"]`, `["AutoUpdate","SdAutoUpdateService"]`)을 가져 신·구 클라이언트 모두 호출 가능.
77
96
 
78
- `OrmService` (이름 `"Orm"`, `"SdOrmService"`) — `auth()` 래핑(로그인 필요). WebSocket 전용(HTTP 호출 시 throw). 소켓별 DB 연결을 `connId` 로 관리하며 소켓 종료 시 자동 정리. 연결 설정은 `ctx.getConfig("orm")[configName]` 에서 읽음. 메서드:
97
+ ### OrmService / OrmServiceType
79
98
 
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>` — 클라이언트 타입 공유용.
99
+ ```ts
100
+ export const OrmService: ServiceDefinition
101
+ export type OrmServiceType = ServiceMethods<typeof OrmService>
102
+ ```
89
103
 
90
- `AutoUpdateService` (이름 `"AutoUpdate"`, `"SdAutoUpdateService"`) 인증 없음.
104
+ `auth()` 로 래핑됨(로그인 필요). **WebSocket 전용** — HTTP 호출 시 throw(연결 ID 상태를 소켓에 묶어 관리하기 때문). DB 설정은 `ctx.getConfig("orm")` 의 `<configName>` 키에서 읽음. 소켓 종료 시 해당 소켓의 모든 열린 DB 연결을 자동 정리. 메서드: `getInfo`/`connect`(연결 ID 반환)/`close`/`beginTransaction`(`isolationLevel?`)/`commitTransaction`/`rollbackTransaction`/`executeParametrized`/`executeDefs`/`bulkInsert`. `dialect` 가 `"mssql-azure"` `"mssql"` 로 정규화해 응답.
91
105
 
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>` — 클라이언트 타입 공유용.
106
+ ### AutoUpdateService / AutoUpdateServiceType
94
107
 
95
108
  ```ts
96
- services: [OrmService, AutoUpdateService]
97
- // 클라이언트: client.getService<OrmServiceType>("Orm")
109
+ export const AutoUpdateService: ServiceDefinition
110
+ export type AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>
98
111
  ```
99
112
 
100
- ## 설정 읽기 (getConfig)
113
+ 인증 불필요. `getLastVersion(platform: string)` — `<clientPath>/<platform>/updates/` 에서 최신 버전 파일을 semver 로 골라 `{ version, downloadPath }` 반환, 없으면 undefined. `platform === "android"` 면 `.apk`, 그 외엔 `.exe` 파일만 후보(파일명이 버전 숫자 패턴 `^[0-9.]*$` 여야 함). `downloadPath` 는 `/` 로 시작하는 POSIX 경로.
101
114
 
102
- `getConfig<TConfig>(filePath): Promise<TConfig | undefined>` — JSON 설정 파일 로드. 1시간 만료 캐시 + FsWatcher 로 파일 변경 시 자동 리로드, 삭제 시 캐시 무효화. 파일 없으면 undefined. 보통 직접 호출하지 않고 `ctx.getConfig(section)`(루트 + 클라이언트 설정 merge 후 섹션 추출)으로 사용.
115
+ ```ts
116
+ client.getService<OrmServiceType>("Orm");
117
+ client.getService<AutoUpdateServiceType>("AutoUpdate");
118
+ ```
@@ -4,71 +4,68 @@ RPC 서비스(클라이언트가 원격 호출할 메서드 묶음)를 정의하
4
4
 
5
5
  ## defineService
6
6
 
7
- `defineService<TMethods>(name: string | string[], factory: (ctx: ServiceContext) => TMethods): ServiceDefinition<TMethods>` — 이름과 팩토리로 서비스를 정의.
7
+ `defineService<TMethods>(name: string | string[], factory: (ctx: ServiceContext) => TMethods): ServiceDefinition<TMethods>` — 서비스 정의 생성.
8
8
 
9
- - `name: string | string[]` — 서비스 이름. 배열이면 다중 별칭(첫 요소가 primary `name`, 전체가 호출 매칭용 `names`). 클라이언트는 `names` 중 아무 이름으로나 호출 가능. 빈 배열이면 "서비스 이름은 하나 이상 필요합니다." throw.
10
- - `factory: (ctx) => TMethods` 컨텍스트를 받아 메서드 객체 반환. 메서드 호출마다 ctx 1회 실행됨. `auth(...)` 감싸면 서비스 전체에 인증 부여.
9
+ - `name` — 서비스 식별 이름. 문자열 1개 또는 배열(별칭 다중 등록, 원소가 primary). 빈 배열이면 throw. 클라이언트는 `"<name>.<method>"` 형태로 호출.
10
+ - `factory` 호출마다 `ctx`(요청 컨텍스트)를 받아 메서드 객체를 반환하는 함수. 요청별로 매번 호출되므로 요청 스코프 상태를 여기 둔다. 인스턴스 공유 상태는 팩토리 외부에 둘 것(예: `OrmService` 의 `WeakMap`).
11
11
 
12
12
  ```ts
13
- export const HealthService = defineService("Health", (ctx) => ({
13
+ const HealthService = defineService("Health", (ctx) => ({
14
14
  check: () => ({ status: "ok" }),
15
15
  }));
16
16
  ```
17
17
 
18
- ## ServiceDefinition
18
+ 팩토리 전체를 `auth(...)` 로 감싸면 정의의 `authPermissions` 가 채워져 서비스 전 메서드에 인증이 강제된다(`getServiceAuthPermissions` 로 추출).
19
19
 
20
- `interface ServiceDefinition<TMethods>` — `defineService` 산출물.
20
+ ## auth
21
21
 
22
- - `name: string` 대표 이름(`names[0]`).
23
- - `names: string[]` — 호출 매칭에 쓰이는 전체 이름 목록(별칭 포함).
24
- - `factory: (ctx: ServiceContext) => TMethods` — 메서드 생성 팩토리.
25
- - `authPermissions?: string[]` — 서비스 수준 인증 권한. 팩토리를 `auth` 로 감쌌을 때만 존재(빈 배열 = 로그인만 요구, undefined = 인증 없음).
22
+ 메서드 또는 팩토리를 감싸 인증·권한을 부여하는 래퍼. 호출 동작은 그대로 유지하고 권한 메타데이터만 부착한다.
26
23
 
27
- ## auth
24
+ - `auth(fn)` — 권한 배열 없이 감쌈. 로그인만 필요(역할 무관).
25
+ - `auth(permissions: string[], fn)` — 지정 역할 중 하나라도 토큰 `roles` 에 있어야 통과. 빈 배열은 로그인만 요구하는 것과 동일.
28
26
 
29
- 인증 래퍼. 서비스 팩토리 또는 개별 메서드를 감싼다. 호출 동작은 보존하고 권한 메타데이터만 심볼로 부착하며, 메서드 수준 권한이 서비스 수준보다 우선한다.
27
+ 적용 수준 가지(둘 같은 함수):
30
28
 
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.
29
+ - 서비스 수준: `auth((ctx) => ({ ... }))` 또는 `auth(["admin"], (ctx) => ({ ... }))` 모든 메서드에 적용.
30
+ - 메서드 수준: 객체 안에서 `someMethod: auth(() => result)` 또는 `auth(["admin"], () => result)` 메서드만.
31
+
32
+ 권한 해석 우선순위(`executeServiceMethod`): 메서드 수준 권한이 있으면 그것을, 없으면 서비스 수준 권한을 사용. 권한이 있는데 서버 `auth` `undefined`설정 오류로 throw, `false` 검사 스킵, 객체면 토큰 검증(미인증 `"로그인이 필요합니다."`, 권한 부족 시 `"권한이 부족합니다."` throw).
35
33
 
36
34
  ```ts
37
- export const UserService = defineService("User", auth((ctx) => ({
35
+ const UserService = defineService("User", auth((ctx) => ({
38
36
  getProfile: () => ctx.authInfo,
39
- removeAll: auth(["admin"], () => repo.removeAll()),
37
+ adminOnly: auth(["admin"], () => "admin"),
40
38
  })));
41
39
  ```
42
40
 
41
+ `getServiceAuthPermissions(fn: Function): string[] | undefined` — `auth()` 로 감싼 함수에서 권한 배열을 읽음. 감싸지 않았으면 undefined. 보통 내부에서만 사용.
42
+
43
43
  ## ServiceContext
44
44
 
45
- `interface ServiceContext<TAuthInfo>` — 팩토리·메서드 안에서 호출 맥락에 접근.
45
+ 팩토리가 받는 요청 컨텍스트. `ServiceContext<TAuthInfo>` 멤버:
46
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.
47
+ - `server: ServiceServer<TAuthInfo>` — 서버 인스턴스. `server.options` 접근 등.
48
+ - `socket?: ServiceSocket` — WebSocket 요청이면 해당 소켓(HTTP/레거시 요청이면 undefined).
49
+ - `http?: { clientName: string; authTokenPayload? }` — HTTP 요청 메타(WebSocket 요청이면 undefined).
50
+ - `legacy?: { clientName? }` — V1 레거시 요청 메타.
51
+ - `authInfo` (getter) — `TAuthInfo | undefined`. 소켓/HTTP 토큰 페이로드의 `data`. 미인증이면 undefined.
52
+ - `clientName` (getter) — `string | undefined`. 소켓→HTTP→레거시 순으로 클라이언트 이름. `..`·`/`·`\`·빈 문자열 포함 시 보안상 throw.
53
+ - `clientPath` (getter) — `string | undefined`. `<rootPath>/www/<clientName>` 절대경로. clientName 없으면 undefined.
54
+ - `getConfig<T>(section: string): Promise<T>` — 루트 `.config.json` + 클라이언트별 `.config.json` 을 병합(클라이언트가 루트를 덮어씀)한 뒤 `section` 키를 반환. 해당 섹션 없으면 throw.
55
55
 
56
56
  ## ServiceMethods
57
57
 
58
- `type ServiceMethods<TDefinition> = TDefinition extends ServiceDefinition<infer M> ? M : never` `ServiceDefinition` 에서 메서드 시그니처 타입만 추출. 클라이언트 타입 공유에 사용.
58
+ `ServiceMethods<TDefinition>` `ServiceDefinition<M>` 에서 메서드 시그니처 `M` 추출하는 타입 유틸. 서버 정의를 클라이언트와 공유해 호출 타입을 맞출 때.
59
59
 
60
60
  ```ts
61
61
  export type UserServiceType = ServiceMethods<typeof UserService>;
62
62
  // 클라이언트: client.getService<UserServiceType>("User");
63
63
  ```
64
64
 
65
- ## 저수준 (직접 서버 조립·디스패치 시에만)
65
+ ## ServiceDefinition
66
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.
67
+ `defineService` 의 반환 타입. `{ name: string; names: string[]; factory: (ctx) => TMethods; authPermissions?: string[] }`. `name` primary 이름, `names` 는 모든 별칭, `authPermissions` 는 팩토리가 `auth()` 감싸졌을 때만 채워짐. 보통 직접 만들지 않고 `defineService` 결과를 그대로 `services` 에 넣는다.
70
68
 
71
- ## 주의사항
69
+ ## createServiceContext
72
70
 
73
- - `factory` 호출마다 실행되므로 호출 공유 상태(예: 소켓별 DB 연결) 팩토리 외부 `WeakMap` 등에 둔다(OrmService 참고).
74
- - 결측(authInfo/clientName)은 undefined 로 끝까지 전파. `?? ""` 등으로 치환 금지.
71
+ `createServiceContext<TAuthInfo>(server, socket?, http?, legacy?): ServiceContext<TAuthInfo>` 컨텍스트 객체를 직접 생성. 서버 내부(요청 처리·V1 레거시 fallback)에서 사용하며, 커스텀 호출 경로를 손수 만들 때만 직접 호출.
@@ -1,51 +1,62 @@
1
1
  # @simplysm/service-server — transport-internals
2
2
 
3
- `ServiceServer.listen()` 이 내부적으로 등록하는 저수준 전송·프로토콜 핸들러. 보통 직접 호출하지 않으며, 커스텀 서버를 손수 조립하거나 동작을 디버깅할 때만 참조한다.
3
+ `ServiceServer.listen()` 이 내부적으로 등록하는 저수준 전송·프로토콜 핸들러와 서비스 실행기. 보통 직접 호출하지 않으며, 커스텀 서버를 손수 조립하거나 동작을 디버깅·확장할 때만 참조한다.
4
4
 
5
- ## WebSocketHandler
5
+ ## executeServiceMethod
6
6
 
7
- `createWebSocketHandler(runMethod, jwtSecret): WebSocketHandler`다중 WebSocket 연결을 `clientId` 단위로 관리하고 메시지를 `runMethod`(보통 `executeServiceMethod` 바인딩)로 라우팅·이벤트 브로드캐스트.
7
+ `executeServiceMethod(server, def): Promise<unknown>`서비스 이름·메서드 이름·params 실제 메서드를 찾아 인증 검사 실행하는 핵심 디스패처. WebSocket/HTTP 핸들러가 공통으로 이걸 호출한다.
8
8
 
9
- - `runMethod(def): Promise<unknown>` — `{ serviceName, methodName, params, socket? }` 받아 RPC 결과 반환.
10
- - `jwtSecret: string | undefined` — `"auth"` 메시지로 들어온 토큰 검증용. 없으면 auth 메시지에서 "JWT Secret이 정의되지 않았습니다." throw.
9
+ - `def.serviceName` / `def.methodName` — `services` 에서 매칭할 이름. 서비스 없으면 `"서비스 [..]를 찾을 없습니다."`, 메서드 없으면 `"메서드 [..]를 찾을 수 없습니다."` throw.
10
+ - `def.params: unknown[]` — 메서드 인자.
11
+ - `def.socket?` / `def.http?` — 요청 출처(둘 중 하나). clientName 에 `..`·`/`·`\` 포함 시 보안 throw.
11
12
 
12
- `WebSocketHandler`:
13
+ 인증 검사는 메서드/서비스 권한 + 서버 `auth` 설정 조합으로 수행(service-authoring.md 의 `auth` 항목 참조).
13
14
 
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 로 수신 대상 선별.
15
+ ## createWebSocketHandler / WebSocketHandler
17
16
 
18
- 처리 메시지: `"<Service>.<method>"`(RPC), `evt:add`/`evt:remove`/`evt:gets`/`evt:emit`(이벤트 리스너 등록·해제·조회·브로드캐스트), `auth`(토큰 등록). 미지원 메시지는 code `BAD_MESSAGE`, 처리 예외는 `INTERNAL_ERROR` 응답(`DEV` env stack 포함).
17
+ `createWebSocketHandler(runMethod, jwtSecret?): WebSocketHandler` 여러 WebSocket 연결을 `clientId` 키로 관리하고 메시지를 라우팅·이벤트 브로드캐스트한다. `runMethod` 보통 `executeServiceMethod` 바인딩.
19
18
 
20
- ## ServiceSocket
19
+ `WebSocketHandler` 멤버:
21
20
 
22
- `createServiceSocket(socket, clientId, clientName, connReq): ServiceSocket` — 단일 WebSocket 연결 래퍼. 프로토콜 인코딩/디코딩, 5초 ping/pong keep-alive(무응답 terminate), 이벤트 리스너 추적.
21
+ - `addSocket(socket, clientId, clientName, connReq)` — 연결 등록. 같은 `clientId` 기존 연결은 닫고 교체. 연결 처리 에러 시 소켓 terminate.
22
+ - `closeAll()` — 모든 연결 종료(서버 close 시).
23
+ - `emit<TEventDef>(eventName, infoSelector, data): Promise<void>` — 등록 리스너 중 `infoSelector(info)` true 인 키에만 `evt:on` 전송.
23
24
 
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 })`.
25
+ 처리하는 클라이언트 메시지 `name`: `"<service>.<method>"`(RPC 실행), `evt:add`/`evt:remove`/`evt:gets`/`evt:emit`(이벤트 리스너 등록·해제·조회·발신), `auth`(토큰 검증 후 소켓에 페이로드 저장; jwtSecret 없으면 throw). 그 외엔 `BAD_MESSAGE`, 실행 중 예외는 `INTERNAL_ERROR` 코드로 에러 응답(`DEV` env 시 stack 포함).
33
26
 
34
- ## HTTP 핸들러
27
+ ## createServiceSocket / ServiceSocket
35
28
 
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 에러 페이지).
29
+ `createServiceSocket(socket: WebSocket, clientId, clientName, connReq): ServiceSocket`단일 WebSocket 연결을 감싸 프로토콜 인코딩/디코딩, 5초 주기 ping/pong keep-alive(무응답terminate), 이벤트 리스너 추적을 담당.
39
30
 
40
- ## ServerProtocolWrapper
31
+ `ServiceSocket` 멤버:
41
32
 
42
- `createServerProtocolWrapper(): ServerProtocolWrapper` 무거운 인코딩/디코딩을 공유 worker 스레드(지연 싱글턴)에 자동 위임, 가벼운 건 메인 스레드.
33
+ - `connectedAtDateTime: DateTime` / `clientName: string` / `connReq: FastifyRequest` 연결 메타(읽기 전용).
34
+ - `authTokenPayload?: AuthTokenPayload` — `auth` 메시지 검증 후 저장되는 인증 페이로드(get/set).
35
+ - `close()` — 연결 terminate.
36
+ - `send(uuid, msg): Promise<number>` — 메시지 인코딩 후 전송, 전송 바이트 수 반환(소켓 닫혀 있으면 0).
37
+ - `addListener(key, eventName, info)` / `removeListener(key)` — 이벤트 리스너 등록·제거.
38
+ - `getEventListeners(eventName): Array<{ key, info }>` — 해당 이벤트의 리스너 목록.
39
+ - `filterEventTargetKeys(targetKeys): string[]` — 이 소켓에 실제 등록된 키만 필터.
40
+ - `on(event, handler)` — `"error"`(Error) / `"close"`(code: number) / `"message"`({ uuid, msg }) 핸들러 등록.
43
41
 
44
- - `encode(uuid, message): Promise<{ chunks; totalSize }>` — body 가 `Uint8Array`(또는 Uint8Array 포함 배열)면 worker, 아니면 메인.
45
- - `decode(bytes): Promise<ServiceMessageDecodeResult>` — 청크 재조립(stateful)은 항상 메인 단일 누적기, 재조립 완료 후 30KB 초과 JSON 파싱만 worker(청크 분산 재조립 버그 #35 방지).
46
- - `dispose(): void` — 프로토콜 리소스 해제.
42
+ ## handleHttpRequest
47
43
 
48
- ## 주의사항
44
+ `handleHttpRequest<TAuthInfo>(req, reply, jwtSecret?, runMethod): Promise<void>` — `/api/:service/:method` 라우트 처리. `x-sd-client-name` 헤더 필수(없으면 throw), `Authorization: Bearer <token>` 있으면 검증(실패 시 401). GET 은 `?json=` 쿼리에서 params 파싱, POST 는 본문 배열(아니면 400), 그 외 메서드는 405. 결과를 그대로 응답.
49
45
 
50
- - `decode` 의 청크 재조립을 worker 로 분기하면 한 메시지의 청크가 서로 다른 누적기로 흩어져 재조립이 완성되지 못함 — 메인 스레드 누적기 유지가 필수.
51
- - 같은 `clientId` 재연결 시 이전 소켓이 강제 종료되므로 동일 `clientId` 다중 동시 연결은 불가.
46
+ ## handleUpload
47
+
48
+ `handleUpload(req, reply, rootPath, jwtSecret?): Promise<void>` — `/upload` multipart 업로드 처리. multipart 아니면 400, 인증 토큰 누락·검증 실패 시 401. 각 파일을 `<rootPath>/www/uploads/<uuid><ext>` 로 저장하고 `ServiceUploadResult[]`(`{ path, filename, size }`) 반환. 크기 제한 초과나 도중 에러 시 이미 저장된 파일을 모두 삭제(원자적 정리)하고 500.
49
+
50
+ ## handleStaticFile
51
+
52
+ `handleStaticFile(req, reply, rootPath, urlPath): Promise<void>` — `<rootPath>/www/` 하위 정적 파일 제공. `www` 밖 경로 탐색 시도는 throw. 디렉토리는 끝에 `/` 붙여 리다이렉트 후 `index.html` 제공. `.` 으로 시작하는 숨김 파일은 403, 없는 파일은 404, 그 외 전송 에러는 500(각각 HTML 에러 페이지).
53
+
54
+ ## createServerProtocolWrapper / ServerProtocolWrapper
55
+
56
+ `createServerProtocolWrapper(): ServerProtocolWrapper` — 메시지 인코딩/디코딩 래퍼. 무거운 작업(Uint8Array 본문, 30KB 초과 JSON 파싱)은 공유 worker 스레드에 위임하고 가벼운 작업은 메인에서 처리. 청크 재조립(stateful)은 항상 메인 단일 누적기에서 수행한다(분산 시 재조립 불가 회피, #35).
57
+
58
+ `ServerProtocolWrapper` 멤버:
59
+
60
+ - `encode(uuid, message): Promise<{ chunks: Bytes[]; totalSize: number }>` — 인코딩. 본문이 Uint8Array 거나 Uint8Array 요소를 포함한 배열이면 worker 사용.
61
+ - `decode(bytes): Promise<ServiceMessageDecodeResult>` — 누적·디코딩. 진행 중이면 `{ type: "progress", ... }`, 완료 시 `{ type: "complete", uuid, message }`(30KB 초과 시 worker 파싱).
62
+ - `dispose()` — 프로토콜 리소스 해제(소켓 종료 시).
@@ -1,50 +1,39 @@
1
1
  # @simplysm/service-server — v1-legacy
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`
9
-
10
- 연결 즉시 `{ name: "connected" }` 전송 후, JSON 메시지(`V1Request`)를 받아 처리:
11
-
12
- 1. 등록된 `handlers` 순서대로 실행, `handled: true` 결과로 응답.
13
- 2. 미처리이고 command 가 `"SdAutoUpdateService.getLastVersion"` 이면 자동업데이트 fallback 실행.
14
- 3. 그 외엔 `{ message: "앱 업그레이드가 필요합니다.", code: "UPGRADE_REQUIRED" }` 에러 응답.
15
-
16
- ## 타입
17
-
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 통지 콜백.
31
-
32
- ## 사용 예
33
-
34
- ```ts
35
- createServiceServer({
36
- // ...
37
- services: [AutoUpdateService], // ver≠2 연결의 getLastVersion fallback 자동 연결
38
- legacyV1Handlers: [
39
- (ctx) =>
40
- ctx.request.command === "Legacy.ping"
41
- ? { handled: true, body: "pong" }
42
- : { handled: false },
43
- ],
44
- });
45
- ```
46
-
47
- ## 주의사항
48
-
49
- - `handlers` 가 있는데 `serviceContext`(또는 factory)가 없으면 핸들러 실행 시 "serviceContext가 필요합니다." throw — 핸들러를 쓰려면 컨텍스트를 함께 제공.
50
- - 메시지 처리 중 예외는 warn 로그만 남기고 응답하지 않음(레거시 한정 동작).
7
+ V1 소켓 연결을 처리한다. 가지 시그니처:
8
+
9
+ - `handleV1Connection(socket, autoUpdateMethods: V1AutoUpdateMethods, clientNameSetter?)` — 자동 업데이트 메서드만 넘기는 단축형.
10
+ - `handleV1Connection(socket, options: V1ConnectionOptions)` 전체 옵션형.
11
+
12
+ 연결 즉시 `{ name: "connected" }` 전송. 메시지 수신 시: ① `clientNameSetter` 호출 → ② 사용자 `handlers` 순회(처리되면 응답) → ③ 미처리이고 command 가 `"SdAutoUpdateService.getLastVersion"` 이면 자동 업데이트 메서드 실행 → ④ 그래도 미처리면 `{ message: "앱 업그레이드가 필요합니다.", code: "UPGRADE_REQUIRED" }` 에러 응답. 메시지 파싱 에러는 warn 로그.
13
+
14
+ ## V1ConnectionOptions
15
+
16
+ - `serviceContext?: ServiceContext` — 핸들러에 넘길 고정 컨텍스트.
17
+ - `serviceContextFactory?: (request: V1Request) => ServiceContext` — 요청별 컨텍스트 생성(고정 컨텍스트보다 우선 적용). `ServiceServer` 는 이걸로 clientName 만 담은 컨텍스트를 만든다.
18
+ - `handlers?: V1RequestHandler[]` — 사용자 정의 처리기 목록. 하나라도 `handled: true` 응답으로 종료. 핸들러가 있는데 컨텍스트가 없으면 throw.
19
+ - `autoUpdateMethods?: V1AutoUpdateMethods` — getLastVersion fallback 고정 구현.
20
+ - `autoUpdateMethodsFactory?: (ctx: V1RequestHandlerContext) => V1AutoUpdateMethods` — 요청별 fallback 생성(있으면 고정 구현보다 우선). 컨텍스트 없으면 throw.
21
+ - `clientNameSetter?: (clientName: string | undefined) => void` 메시지의 `clientName` 외부로 전달하는 콜백.
22
+
23
+ ## V1RequestHandler
24
+
25
+ `V1RequestHandler` — `(ctx: V1RequestHandlerContext) => V1RequestHandlerResult | Promise<...>`. 동기/비동기 모두 허용.
26
+
27
+ `V1RequestHandlerContext` — `{ request: V1Request; serviceContext: ServiceContext }`.
28
+
29
+ `V1RequestHandlerResult` `{ handled: true; state?: "success" | "error"; body: unknown }`(이 핸들러가 처리; `state` 미지정 시 `"success"`) 또는 `{ handled: false }`(다음 핸들러/fallback 으로 위임).
30
+
31
+ ## V1Request / V1Response
32
+
33
+ `V1Request` — 클라이언트 요청. `{ uuid: string; command: string; params: unknown[]; clientName?: string }`. `command` 는 `"<service>.<method>"` 형태.
34
+
35
+ `V1Response` — 서버 응답. `{ name: "response"; reqUuid: string; state: "success" | "error"; body: unknown }`. `state` 가 `"success"` 면 정상 결과, `"error"` 면 오류 본문.
36
+
37
+ ## V1AutoUpdateMethods
38
+
39
+ `V1AutoUpdateMethods` — `{ getLastVersion: (platform: string) => Promise<unknown> | unknown }`. V1 자동 업데이트 fallback 의 최소 인터페이스. `ServiceServer` 는 등록된 `AutoUpdate` 서비스의 `getLastVersion` 을 여기에 어댑트해 넘긴다.