@simplysm/sd-claude 14.0.89 → 14.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/claude/references/sd-simplysm14/README.md +16 -17
  2. package/claude/references/sd-simplysm14/apis/angular/README.md +52 -30
  3. package/claude/references/sd-simplysm14/apis/angular/controls.md +200 -38
  4. package/claude/references/sd-simplysm14/apis/angular/crud.md +41 -53
  5. package/claude/references/sd-simplysm14/apis/angular/directives.md +66 -22
  6. package/claude/references/sd-simplysm14/apis/angular/features.md +127 -40
  7. package/claude/references/sd-simplysm14/apis/angular/infra.md +60 -43
  8. package/claude/references/sd-simplysm14/apis/angular/layout.md +56 -20
  9. package/claude/references/sd-simplysm14/apis/angular/overlay.md +74 -74
  10. package/claude/references/sd-simplysm14/apis/angular/routing-appstructure.md +50 -40
  11. package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +55 -15
  12. package/claude/references/sd-simplysm14/apis/angular/shared-data.md +59 -42
  13. package/claude/references/sd-simplysm14/apis/angular/sheet.md +77 -62
  14. package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +8 -7
  15. package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +71 -43
  16. package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +22 -14
  17. package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +19 -19
  18. package/claude/references/sd-simplysm14/apis/core-browser/README.md +17 -17
  19. package/claude/references/sd-simplysm14/apis/core-browser/dom-element.md +28 -28
  20. package/claude/references/sd-simplysm14/apis/core-browser/indexed-db.md +37 -37
  21. package/claude/references/sd-simplysm14/apis/core-common/README.md +87 -219
  22. package/claude/references/sd-simplysm14/apis/core-common/array-ext.md +54 -98
  23. package/claude/references/sd-simplysm14/apis/core-common/async-runtime.md +57 -99
  24. package/claude/references/sd-simplysm14/apis/core-common/datetime.md +60 -103
  25. package/claude/references/sd-simplysm14/apis/core-common/errors.md +42 -47
  26. package/claude/references/sd-simplysm14/apis/core-common/obj.md +42 -88
  27. package/claude/references/sd-simplysm14/apis/core-common/serialization.md +55 -0
  28. package/claude/references/sd-simplysm14/apis/core-node/README.md +6 -7
  29. package/claude/references/sd-simplysm14/apis/core-node/consola.md +17 -12
  30. package/claude/references/sd-simplysm14/apis/core-node/cpx.md +14 -13
  31. package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +9 -8
  32. package/claude/references/sd-simplysm14/apis/core-node/fsx.md +14 -13
  33. package/claude/references/sd-simplysm14/apis/core-node/pathx.md +4 -8
  34. package/claude/references/sd-simplysm14/apis/core-node/worker.md +14 -12
  35. package/claude/references/sd-simplysm14/apis/excel/README.md +22 -22
  36. package/claude/references/sd-simplysm14/apis/excel/cell.md +37 -29
  37. package/claude/references/sd-simplysm14/apis/excel/conditional-format.md +29 -15
  38. package/claude/references/sd-simplysm14/apis/excel/style.md +33 -27
  39. package/claude/references/sd-simplysm14/apis/excel/utils.md +29 -19
  40. package/claude/references/sd-simplysm14/apis/excel/workbook-worksheet.md +78 -55
  41. package/claude/references/sd-simplysm14/apis/excel/wrapper.md +42 -45
  42. package/claude/references/sd-simplysm14/apis/orm-common/README.md +6 -8
  43. package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +118 -67
  44. package/claude/references/sd-simplysm14/apis/orm-common/expr.md +83 -86
  45. package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +102 -93
  46. package/claude/references/sd-simplysm14/apis/orm-common/schema.md +138 -81
  47. package/claude/references/sd-simplysm14/apis/orm-common/types.md +49 -44
  48. package/claude/references/sd-simplysm14/apis/orm-node/README.md +42 -42
  49. package/claude/references/sd-simplysm14/apis/orm-node/db-conn.md +44 -33
  50. package/claude/references/sd-simplysm14/apis/sd-cli/README.md +11 -10
  51. package/claude/references/sd-simplysm14/apis/service-client/README.md +56 -52
  52. package/claude/references/sd-simplysm14/apis/service-client/orm.md +33 -28
  53. package/claude/references/sd-simplysm14/apis/service-client/transport.md +23 -21
  54. package/claude/references/sd-simplysm14/apis/service-common/README.md +83 -48
  55. package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +126 -34
  56. package/claude/references/sd-simplysm14/apis/service-common/protocol.md +109 -54
  57. package/claude/references/sd-simplysm14/apis/service-server/README.md +69 -81
  58. package/claude/references/sd-simplysm14/apis/service-server/service-authoring.md +46 -43
  59. package/claude/references/sd-simplysm14/apis/service-server/transport-internals.md +63 -37
  60. package/claude/references/sd-simplysm14/apis/service-server/v1-legacy.md +40 -30
  61. package/claude/references/sd-simplysm14/apis/storage/README.md +17 -17
  62. package/claude/references/sd-simplysm14/manuals/client-app-structure.md +135 -140
  63. package/claude/references/sd-simplysm14/manuals/client-orm.md +1 -1
  64. package/claude/references/sd-simplysm14/manuals/client-service.md +19 -7
  65. package/claude/references/sd-simplysm14/manuals/client-shared-data.md +2 -2
  66. package/claude/references/sd-simplysm14/manuals/client-system-log.md +16 -4
  67. package/claude/references/sd-simplysm14/manuals/data-log.md +0 -1
  68. package/claude/references/sd-simplysm14/manuals/orm.md +16 -0
  69. package/claude/rules/sd-design-rules.md +10 -0
  70. package/claude/skills/sd-demo/SKILL.md +0 -6
  71. package/claude/skills/sd-docs/SKILL.md +60 -0
  72. package/claude/{workflows/sd-docs.rules.md → skills/sd-docs/references/subagent-prompt.md} +118 -103
  73. package/claude/skills/sd-impl/SKILL.md +7 -4
  74. package/claude/skills/sd-spec/SKILL.md +842 -15
  75. package/claude/skills/sd-spec/references/example-spec.md +26 -36
  76. package/package.json +1 -1
  77. package/claude/references/sd-simplysm14/apis/core-common/json-transfer.md +0 -53
  78. package/claude/skills/sd-spec/references/spec-authoring.md +0 -519
  79. package/claude/workflows/sd-docs.js +0 -84
@@ -1,118 +1,106 @@
1
1
  # @simplysm/service-server
2
2
 
3
- Fastify 기반 서비스 서버. WebSocket/HTTP 두 전송 계층으로 RPC 스타일 서비스 메서드를 노출하고, JWT 인증·정적 파일·업로드·이벤트 브로드캐스팅·내장 ORM/자동업데이트 서비스를 제공한다.
3
+ Fastify 기반 RPC 서비스 서버. WebSocket/HTTP 두 전송 계층으로 서비스 메서드를 노출하고, JWT 인증·정적 파일·파일 업로드·서버측 이벤트 브로드캐스팅·내장 ORM/자동업데이트 서비스를 한 프로세스에서 제공한다.
4
4
 
5
5
  ## 사용 트리거 인덱스
6
6
 
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)
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
- ## 서버 인스턴스
15
+ ## 서버 부트스트랩
16
16
 
17
- ```ts
18
- class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{ ready: void; close: void }>
19
- function createServiceServer<TAuthInfo = unknown>(options: ServiceServerOptions): ServiceServer<TAuthInfo>
20
- ```
17
+ 서버 앱 진입점에서 `createServiceServer(options)` 로 인스턴스를 만들고 `await server.listen()` 으로 기동한다.
21
18
 
22
- `createServiceServer` `new ServiceServer` 의 얇은 래퍼. `TAuthInfo` 는 인증 토큰 `data` 필드의 타입(`ctx.authInfo` 와 토큰 발급/검증에 전파됨).
19
+ ### createServiceServer / ServiceServer
23
20
 
24
- `ServiceServerOptions`:
21
+ `createServiceServer<TAuthInfo = unknown>(options: ServiceServerOptions): ServiceServer<TAuthInfo>` — 옵션을 받아 서버 인스턴스 생성(아직 리슨 안 함). `TAuthInfo` 는 인증 토큰의 `data` 페이로드 타입으로, `server.signAuthToken`·`ctx.authInfo` 에 그대로 흐른다. `new ServiceServer(options)` 직접 생성과 동일.
25
22
 
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).
23
+ `ServiceServerOptions` 필드:
32
24
 
33
- 메서드:
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).
34
31
 
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` — 생성 전달한 옵션(읽기 전용 참조).
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` 상속. 기동·종료 시점 후킹.
40
43
 
41
44
  ```ts
42
- const server = createServiceServer<MyAuthInfo>({
43
- rootPath: import.meta.dirname,
45
+ const server = createServiceServer<AuthInfo>({
46
+ rootPath: process.cwd(),
44
47
  port: 50080,
45
- auth: { jwtSecret: "secret" },
46
- services: [MyService, OrmService, AutoUpdateService],
48
+ auth: { jwtSecret: env("JWT_SECRET")! },
49
+ services: [UserService, OrmService, AutoUpdateService],
47
50
  });
48
51
  await server.listen();
49
52
  ```
50
53
 
51
- ## 이벤트 브로드캐스트
52
-
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
- ```
60
-
61
- `ServiceEventDef` 는 `@simplysm/service-common` 의 이벤트 정의 타입(`eventName`/`$info`/`$data` 보유). 클라이언트는 이벤트 리스너 등록 시 `info` 를 같이 보내고, 서버는 등록된 모든 소켓의 리스너 중 `infoSelector` 가 true 인 대상에게만 `data` 를 푸시한다.
54
+ 주의: `auth` 를 미지정한 채 권한 요구(`auth(...)` 래핑) 서비스를 등록하면 `listen()` 이 즉시 throw 한다. 인증을 끄려면 `auth: false` 를 명시할 것.
62
55
 
63
- - `infoSelector: (item) => boolean` — 수신 대상 필터. 등록된 각 리스너의 `info` 를 받아 전송 여부를 결정. 특정 조건(예: 같은 화면을 보는 클라이언트)에만 보낼 때 사용.
64
- - `getEvent` 는 `emit` 만 노출하는 프록시를 반환(내부적으로 `emitEvent` 호출) — 같은 eventDef 로 여러 번 emit 할 때 편함.
56
+ ## JWT 인증 토큰
65
57
 
66
- ```ts
67
- const evt = server.getEvent(MyDataChangedEvent);
68
- await evt.emit((info) => info.boardId === 3, { updatedAt: new Date() });
69
- ```
58
+ 로그인 서비스 메서드에서 자격 확인 후 토큰을 발급하고, 다른 메서드에서 토큰을 검증할 때. 보통은 `server.signAuthToken`/`server.verifyAuthToken`(시크릿 자동 사용)을 쓰고, 시크릿을 직접 다룰 때만 아래 함수를 호출.
70
59
 
71
- ## JWT 인증
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>` — 서명 검증 없이 페이로드만 디코드. 검증이 끝난 토큰의 내용만 다시 읽을 때(만료·위변조 판정에는 쓰지 말 것).
72
64
 
73
65
  ```ts
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>
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
+ }));
84
72
  ```
85
73
 
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
- ## 내장 서비스
74
+ ## 서버측 이벤트 발생
94
75
 
95
- `defineService` 결과 상수. `services` 목록에 그대로 추가해 사용. 이름 별칭(`["Orm","SdOrmService"]`, `["AutoUpdate","SdAutoUpdateService"]`) 가져 신·구 클라이언트 모두 호출 가능.
76
+ 서비스 메서드 처리 결과를 구독 중인 클라이언트에 브로드캐스트할 때. 이벤트 정의 객체(`@simplysm/service-common` `defineEvent`) 클라이언트·서버가 공유한다.
96
77
 
97
- ### OrmService / OrmServiceType
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> }` 형태.
98
81
 
99
82
  ```ts
100
- export const OrmService: ServiceDefinition
101
- export type OrmServiceType = ServiceMethods<typeof OrmService>
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
+ }));
102
93
  ```
103
94
 
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"` 로 정규화해 응답.
105
-
106
- ### AutoUpdateService / AutoUpdateServiceType
95
+ ## 내장 서비스
107
96
 
108
- ```ts
109
- export const AutoUpdateService: ServiceDefinition
110
- export type AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>
111
- ```
97
+ 서버 옵션의 `services` 배열에 그대로 추가해 사용하는 미리 정의된 서비스. 클라이언트 타입 공유용 `*Type` 도 함께 export 된다.
112
98
 
113
- 인증 불필요. `getLastVersion(platform: string)` — `<clientPath>/<platform>/updates/` 에서 최신 버전 파일을 semver 골라 `{ version, downloadPath }` 반환, 없으면 undefined. `platform === "android"` `.apk`, 외엔 `.exe` 파일만 후보(파일명이 버전 숫자 패턴 `^[0-9.]*$` 여야 ). `downloadPath` `/` 시작하는 POSIX 경로.
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`). 인증 불필요.
114
101
 
115
102
  ```ts
116
- client.getService<OrmServiceType>("Orm");
117
- client.getService<AutoUpdateServiceType>("AutoUpdate");
103
+ services: [OrmService, AutoUpdateService, ...앱서비스들]
118
104
  ```
105
+
106
+ 주의: 두 내장 서비스는 클라이언트가 `getService("Orm")` / `getService("AutoUpdate")` 의 짧은 이름 또는 `SdOrmService`/`SdAutoUpdateService` 레거시 이름 어느 쪽으로도 호출할 수 있다.
@@ -1,71 +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` — 서비스 식별 이름. 문자열 1개 또는 배열(별칭 다중 등록,원소가 primary). 빈 배열이면 throw. 클라이언트는 `"<name>.<method>"` 형태로 호출.
10
- - `factory` 호출마다 `ctx`(요청 컨텍스트)를 받아 메서드 객체를 반환하는 함수. 요청별로 매번 호출되므로 요청 스코프 상태를 여기 둔다. 인스턴스 간 공유 상태는 팩토리 외부에 것(예: `OrmService` 의 `WeakMap`).
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
- 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
- 팩토리 전체를 `auth(...)` 로 감싸면 정의의 `authPermissions` 가 채워져 서비스 전 메서드에 인증이 강제된다(`getServiceAuthPermissions` 로 추출).
19
-
20
26
  ## auth
21
27
 
22
- 메서드 또는 팩토리를 감싸 인증·권한을 부여하는 래퍼. 호출 동작은 그대로 유지하고 권한 메타데이터만 부착한다.
28
+ 서비스 팩토리 또는 개별 메서드를 인증 래퍼로 감싸 로그인·권한을 요구한다. 권한 메타데이터를 함수에 심볼로 부착하되 호출 동작은 그대로 보존하는 래퍼를 만든다.
23
29
 
24
- - `auth(fn)` — 권한 배열 없이 감쌈. 로그인만 필요(역할 무관).
25
- - `auth(permissions: string[], fn)` 지정 역할 중 하나라도 토큰 `roles` 에 있어야 통과. 빈 배열은 로그인만 요구하는 것과 동일.
26
-
27
- 적용 수준 두 가지(둘 다 같은 함수):
28
-
29
- - 서비스 수준: `auth((ctx) => ({ ... }))` 또는 `auth(["admin"], (ctx) => ({ ... }))` — 모든 메서드에 적용.
30
- - 메서드 수준: 객체 안에서 `someMethod: auth(() => result)` 또는 `auth(["admin"], () => result)` — 그 메서드만.
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
+ ```
31
34
 
32
- 권한 해석 우선순위(`executeServiceMethod`): 메서드 수준 권한이 있으면 그것을, 없으면 서비스 수준 권한을 사용. 권한이 있는데 서버 `auth` `undefined` 설정 오류로 throw, `false` 면 검사 스킵, 객체면 토큰 검증(미인증 시 `"로그인이 필요합니다."`, 권한 부족 시 `"권한이 부족합니다."` 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` 면 인증 검사 자체를 스킵.
33
40
 
34
41
  ```ts
35
- const UserService = defineService("User", auth((ctx) => ({
36
- getProfile: () => ctx.authInfo,
37
- adminOnly: auth(["admin"], () => "admin"),
42
+ export const AdminService = defineService("Admin", auth((ctx) => ({
43
+ list: () => fetchUsers(), // 로그인만
44
+ remove: auth(["admin"], (id: string) => deleteUser(id)), // admin 역할 필요
38
45
  })));
39
46
  ```
40
47
 
41
- `getServiceAuthPermissions(fn: Function): string[] | undefined` — `auth()` 로 감싼 함수에서 권한 배열을 읽음. 감싸지 않았으면 undefined. 보통 내부에서만 사용.
42
-
43
48
  ## ServiceContext
44
49
 
45
- 팩토리가 받는 요청 컨텍스트. `ServiceContext<TAuthInfo>` 멤버:
46
-
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.
50
+ `factory` 가 요청마다 받는 컨텍스트. 메서드 안에서 인증·클라이언트·설정·서버에 접근하는 통로.
55
51
 
56
- ## ServiceMethods
57
-
58
- `ServiceMethods<TDefinition>` `ServiceDefinition<M>` 에서 메서드 시그니처 `M` 추출하는 타입 유틸. 서버 정의를 클라이언트와 공유해 호출 타입을 맞출 때.
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
- ## ServiceDefinition
66
-
67
- `defineService` 의 반환 타입. `{ name: string; names: string[]; factory: (ctx) => TMethods; authPermissions?: string[] }`. `name` 은 primary 이름, `names` 는 모든 별칭, `authPermissions` 는 팩토리가 `auth()` 로 감싸졌을 때만 채워짐. 보통 직접 만들지 않고 `defineService` 결과를 그대로 `services` 에 넣는다.
68
+ ## ServiceDefinition / ServiceMethods / getServiceAuthPermissions
68
69
 
69
- ## createServiceContext
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`. 내부 실행기·커스텀 전송에서만 필요(일반 작성에서는 불필요).
70
73
 
71
- `createServiceContext<TAuthInfo>(server, socket?, http?, legacy?): ServiceContext<TAuthInfo>` 컨텍스트 객체를 직접 생성. 서버 내부(요청 처리·V1 레거시 fallback)에서 사용하며, 커스텀 호출 경로를 손수 만들 때만 직접 호출.
74
+ 주의: 클라이언트가 쓰는 서비스 이름 문자열과 `ServiceMethods` 타입은 단일 소스(`defineService` 이름 / `typeof XxxService`) 따른다. 호출부에서 이름·제네릭을 중복 정의하지 것.
@@ -1,62 +1,88 @@
1
- # @simplysm/service-server — transport-internals
1
+ # @simplysm/service-server — 전송 계층 내부
2
2
 
3
- `ServiceServer.listen()` 이 내부적으로 등록하는 저수준 전송·프로토콜 핸들러와 서비스 실행기. 보통 직접 호출하지 않으며, 커스텀 서버를 손수 조립하거나 동작을 디버깅·확장할 때만 참조한다.
3
+ `ServiceServer.listen()` 이 내부적으로 구성하는 저수준 전송·프로토콜·실행기. 일반 작성에서는 `createServiceServer` 가 알아서 엮으므로 직접 일이 없고, 커스텀 전송을 손수 조립하거나 동작을 테스트·디버깅·확장할 때만 참조한다.
4
4
 
5
5
  ## executeServiceMethod
6
6
 
7
- `executeServiceMethod(server, def): Promise<unknown>` — 서비스 이름·메서드 이름·params 로 실제 메서드를 찾아 인증 검사 후 실행하는 핵심 디스패처. WebSocket/HTTP 핸들러가 공통으로 이걸 호출한다.
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
- - `def.serviceName` / `def.methodName` — `services` 에서 매칭할 이름. 서비스 없으면 `"서비스 [..]를 찾을 없습니다."`, 메서드 없으면 `"메서드 [..]를 찾을 수 없습니다."` throw.
10
- - `def.params: unknown[]` — 메서드 인자.
11
- - `def.socket?` / `def.http?` — 요청 출처(둘 중 하나). clientName 에 `..`·`/`·`\` 포함 시 보안 throw.
20
+ 요청 1건을 실제 서비스 메서드로 라우팅·실행하는 핵심 게이트키퍼. WebSocket/HTTP 핸들러가 모두 함수로 수렴한다.
12
21
 
13
- 인증 검사는 메서드/서비스 권한 + 서버 `auth` 설정 조합으로 수행(service-authoring.md `auth` 항목 참조).
22
+ - `serviceName`/`methodName` `server.options.services` 에서 `names` 매칭으로 서비스를, 팩토리 산출 객체에서 메서드를 찾음. 없으면 throw.
23
+ - `params: unknown[]` — 메서드 인자 배열. 스프레드되어 메서드에 전달.
24
+ - `socket?` / `http?` — 둘 중 하나로 요청 출처 전달. `clientName` 에 `..`·슬래시가 있으면 보안 차단 throw.
25
+ - 동작: 컨텍스트 생성 → 팩토리 호출 → 메서드 조회 → 인증/권한 검사(`auth` 래핑 권한 기준) → 실행 후 반환값 반환. `auth: false` 면 인증 스킵, `auth` 미설정인데 권한 요구 메서드면 설정 오류 throw.
14
26
 
15
- ## createWebSocketHandler / WebSocketHandler
27
+ ## createServiceContext
16
28
 
17
- `createWebSocketHandler(runMethod, jwtSecret?): WebSocketHandler` — 여러 WebSocket 연결을 `clientId` 키로 관리하고 메시지를 라우팅·이벤트 브로드캐스트한다. `runMethod` 는 보통 `executeServiceMethod` 바인딩.
29
+ ```ts
30
+ function createServiceContext<TAuthInfo>(
31
+ server, socket?, http?, legacy?,
32
+ ): ServiceContext<TAuthInfo>
33
+ ```
18
34
 
19
- `WebSocketHandler` 멤버:
35
+ `ServiceContext`(인증·클라이언트·설정 접근자) 인스턴스를 만든다. 인자 `socket`/`http`/`legacy` 중 들어온 것으로 `authInfo`·`clientName` 출처가 결정된다. 컨텍스트 필드 의미는 [service-authoring.md](./service-authoring.md) 의 ServiceContext 절 참조. 테스트에서 컨텍스트를 직접 만들어 서비스 메서드를 단위 호출할 때 유용.
20
36
 
21
- - `addSocket(socket, clientId, clientName, connReq)` — 새 연결 등록. 같은 `clientId` 기존 연결은 닫고 교체. 연결 처리 중 에러 시 소켓 terminate.
22
- - `closeAll()` — 모든 연결 종료(서버 close 시).
23
- - `emit<TEventDef>(eventName, infoSelector, data): Promise<void>` — 등록 리스너 중 `infoSelector(info)` true 인 키에만 `evt:on` 전송.
37
+ ## ServiceSocket / createServiceSocket
24
38
 
25
- 처리하는 클라이언트 메시지 `name`: `"<service>.<method>"`(RPC 실행), `evt:add`/`evt:remove`/`evt:gets`/`evt:emit`(이벤트 리스너 등록·해제·조회·발신), `auth`(토큰 검증 소켓에 페이로드 저장; jwtSecret 없으면 throw). 그 외엔 `BAD_MESSAGE`, 실행 중 예외는 `INTERNAL_ERROR` 코드로 에러 응답(`DEV` env 시 stack 포함).
39
+ 단일 WebSocket 연결을 감싸 프로토콜 인코딩·ping/pong 연결 유지·이벤트 리스너 추적을 담당하는 인터페이스. `createServiceSocket(socket, clientId, clientName, connReq)` 생성.
26
40
 
27
- ## createServiceSocket / ServiceSocket
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.
28
49
 
29
- `createServiceSocket(socket: WebSocket, clientId, clientName, connReq): ServiceSocket` — 단일 WebSocket 연결을 감싸 프로토콜 인코딩/디코딩, 5초 주기 ping/pong keep-alive(무응답 시 terminate), 이벤트 리스너 추적을 담당.
50
+ ## WebSocketHandler / createWebSocketHandler
30
51
 
31
- `ServiceSocket` 멤버:
52
+ ```ts
53
+ function createWebSocketHandler(
54
+ runMethod: (def) => Promise<unknown>,
55
+ jwtSecret: string | undefined,
56
+ ): WebSocketHandler
57
+ ```
32
58
 
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 }) 핸들러 등록.
59
+ 여러 `ServiceSocket` `clientId` 관리하고, 클라이언트 메시지를 `runMethod`(보통 `executeServiceMethod` 바인딩)로 라우팅하며, 이벤트를 브로드캐스트한다.
41
60
 
42
- ## handleHttpRequest
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` 환경에서만 에러 스택 포함.
43
65
 
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. 결과를 그대로 응답.
66
+ ## HTTP / 정적 / 업로드 핸들러
45
67
 
46
- ## handleUpload
68
+ `fastify` 라우트에 직접 물리는 저수준 함수들. 커스텀 라우트를 짤 때만 직접 사용.
47
69
 
48
- `handleUpload(req, reply, rootPath, jwtSecret?): Promise<void>` — `/upload` multipart 업로드 처리. multipart 아니면 400, 인증 토큰 누락·검증 실패 시 401. 각 파일을 `<rootPath>/www/uploads/<uuid><ext>` 로 저장하고 `ServiceUploadResult[]`(`{ path, filename, size }`) 반환. 크기 제한 초과나 도중 에러 이미 저장된 파일을 모두 삭제(원자적 정리)하고 500.
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 응답.
49
73
 
50
- ## handleStaticFile
74
+ ## ServerProtocolWrapper / createServerProtocolWrapper
51
75
 
52
- `handleStaticFile(req, reply, rootPath, urlPath): Promise<void>` `<rootPath>/www/` 하위 정적 파일 제공. `www` 경로 탐색 시도는 throw. 디렉토리는 끝에 `/` 붙여 리다이렉트 후 `index.html` 제공. `.` 으로 시작하는 숨김 파일은 403, 없는 파일은 404, 그 외 전송 에러는 500(각각 HTML 에러 페이지).
76
+ 메시지 인코딩/디코딩을 크기·내용에 따라 worker 스레드와 메인 스레드로 자동 분배하는 래퍼. `createServerProtocolWrapper()` 생성(worker 지연 싱글턴).
53
77
 
54
- ## createServerProtocolWrapper / ServerProtocolWrapper
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()` — 프로토콜 리소스 해제.
55
81
 
56
- `createServerProtocolWrapper(): ServerProtocolWrapper` — 메시지 인코딩/디코딩 래퍼. 무거운 작업(Uint8Array 본문, 30KB 초과 JSON 파싱)은 공유 worker 스레드에 위임하고 가벼운 작업은 메인에서 처리. 청크 재조립(stateful)은 항상 메인 단일 누적기에서 수행한다(분산 시 재조립 불가 회피, #35).
82
+ ## getConfig
57
83
 
58
- `ServerProtocolWrapper` 멤버:
84
+ ```ts
85
+ function getConfig<TConfig>(filePath: string): Promise<TConfig | undefined>
86
+ ```
59
87
 
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()` — 프로토콜 리소스 해제(소켓 종료 시).
88
+ `filePath` JSON 설정을 읽어 캐시·파일워치한다. `ServiceContext.getConfig` 의 내부 구현. 캐시 히트 시 즉시 반환(접근 만료 시간 갱신), 파일 변경 자동 리로드, 1시간 무접근 캐시·워처 GC. 파일이 없으면 `undefined`.
@@ -1,39 +1,49 @@
1
- # @simplysm/service-server — v1-legacy
1
+ # @simplysm/service-server — V1 레거시 지원
2
2
 
3
- `ver !== "2"`(구버전) WebSocket 클라이언트를 받기 위한 레거시 핸들러. 주로 구버전 앱의 자동 업데이트(`SdAutoUpdateService.getLastVersion`) 요청을 처리한다. `ServiceServer` 는 ver=2 가 아닌 연결을 자동으로 이 핸들러로 넘긴다(`AutoUpdate` 서비스나 `legacyV1Handlers` 가 있을 때만, 둘 다 없으면 연결 거부). `ServiceServerOptions.legacyV1Handlers` 로 커스텀 핸들러를 끼울 때만 직접 다룬다.
3
+ `ver !== "2"`(구버전) WebSocket 클라이언트를 받기 위한 레거시 핸들러. 주로 구버전 앱의 자동업데이트(`SdAutoUpdateService.getLastVersion`) 요청을 처리한다. `ServiceServer` 는 ver=2 가 아닌 연결을 자동으로 이 핸들러로 넘긴다(`AutoUpdate` 서비스나 `legacyV1Handlers` 가 있을 때만, 둘 다 없으면 연결 거부). `ServiceServerOptions.legacyV1Handlers` 로 커스텀 핸들러를 끼울 때만 직접 다룬다.
4
4
 
5
5
  ## handleV1Connection
6
6
 
7
- V1 소켓 연결을 처리한다. 두 가지 시그니처:
7
+ ```ts
8
+ function handleV1Connection(socket, autoUpdateMethods: V1AutoUpdateMethods, clientNameSetter?): void;
9
+ function handleV1Connection(socket, options: V1ConnectionOptions): void;
10
+ ```
8
11
 
9
- - `handleV1Connection(socket, autoUpdateMethods: V1AutoUpdateMethods, clientNameSetter?)` 자동 업데이트 메서드만 넘기는 단축형.
10
- - `handleV1Connection(socket, options: V1ConnectionOptions)` — 전체 옵션형.
12
+ V1 WebSocket 연결 1건을 받아 연결 알림(`{ name: "connected" }`) 전송 후 메시지를 처리한다. 처리 순서: 커스텀 핸들러들 → (미처리 시) `SdAutoUpdateService.getLastVersion` fallback 그래도 미처리면 `UPGRADE_REQUIRED` 에러 응답. 메시지 파싱·처리 중 예외는 잡아 warn 로그만 남기고 응답하지 않음.
11
13
 
12
- 연결 즉시 `{ name: "connected" }` 전송. 메시지 수신 시: ① `clientNameSetter` 호출 ② 사용자 `handlers` 순회(처리되면 그 응답) → ③ 미처리이고 command 가 `"SdAutoUpdateService.getLastVersion"` 이면 자동 업데이트 메서드 실행 → ④ 그래도 미처리면 `{ message: "앱 업그레이드가 필요합니다.", code: "UPGRADE_REQUIRED" }` 에러 응답. 메시지 파싱 에러는 warn 로그.
14
+ - `socket: WebSocket` `ws` 원시 소켓.
15
+ - 2번째 인자: `V1AutoUpdateMethods` 객체(자동업데이트만 응대)이거나 `V1ConnectionOptions`(핸들러·팩토리 포함). `"getLastVersion" in arg` 로 분기.
16
+ - `clientNameSetter?` — 첫 시그니처에서만. 요청의 `clientName` 을 외부에 통지하는 콜백.
13
17
 
14
18
  ## V1ConnectionOptions
15
19
 
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` 을 여기에 어댑트해 넘긴다.
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` 통지 콜백.
26
+
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 인터페이스.
35
+
36
+ ```ts
37
+ const server = createServiceServer({
38
+ rootPath, port,
39
+ services: [AutoUpdateService], // ver!=2 연결 getLastVersion fallback 자동 연결
40
+ legacyV1Handlers: [
41
+ ({ request, serviceContext }) =>
42
+ request.command === "Legacy.ping"
43
+ ? { handled: true, body: "pong" }
44
+ : { handled: false },
45
+ ],
46
+ });
47
+ ```
48
+
49
+ 주의: `legacyV1Handlers` 도 없고 `AutoUpdate`(`SdAutoUpdateService`) 서비스도 등록 안 됐으면 ver=2 가 아닌 연결은 코드 1008 로 즉시 거부된다.