@simplysm/service-server 13.0.100 → 14.0.4

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 (88) hide show
  1. package/README.md +79 -133
  2. package/dist/auth/auth-token-payload.js +2 -1
  3. package/dist/auth/auth-token-payload.js.map +1 -6
  4. package/dist/auth/jwt-manager.js +21 -21
  5. package/dist/auth/jwt-manager.js.map +1 -6
  6. package/dist/core/define-service.d.ts +12 -12
  7. package/dist/core/define-service.d.ts.map +1 -1
  8. package/dist/core/define-service.js +77 -63
  9. package/dist/core/define-service.js.map +1 -6
  10. package/dist/core/service-executor.d.ts.map +1 -1
  11. package/dist/core/service-executor.js +42 -32
  12. package/dist/core/service-executor.js.map +1 -6
  13. package/dist/index.d.ts +0 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +11 -2
  16. package/dist/index.js.map +1 -6
  17. package/dist/legacy/v1-auto-update-handler.d.ts +2 -2
  18. package/dist/legacy/v1-auto-update-handler.js +42 -35
  19. package/dist/legacy/v1-auto-update-handler.js.map +1 -6
  20. package/dist/protocol/protocol-wrapper.d.ts +9 -9
  21. package/dist/protocol/protocol-wrapper.js +64 -46
  22. package/dist/protocol/protocol-wrapper.js.map +1 -6
  23. package/dist/service-server.d.ts +2 -1
  24. package/dist/service-server.d.ts.map +1 -1
  25. package/dist/service-server.js +183 -165
  26. package/dist/service-server.js.map +1 -6
  27. package/dist/services/auto-update-service.js +35 -34
  28. package/dist/services/auto-update-service.js.map +1 -6
  29. package/dist/services/orm-service.js +114 -120
  30. package/dist/services/orm-service.js.map +1 -6
  31. package/dist/transport/http/http-request-handler.d.ts.map +1 -1
  32. package/dist/transport/http/http-request-handler.js +58 -46
  33. package/dist/transport/http/http-request-handler.js.map +1 -6
  34. package/dist/transport/http/static-file-handler.js +42 -39
  35. package/dist/transport/http/static-file-handler.js.map +1 -6
  36. package/dist/transport/http/upload-handler.d.ts.map +1 -1
  37. package/dist/transport/http/upload-handler.js +60 -55
  38. package/dist/transport/http/upload-handler.js.map +1 -6
  39. package/dist/transport/socket/service-socket.d.ts +13 -13
  40. package/dist/transport/socket/service-socket.js +132 -108
  41. package/dist/transport/socket/service-socket.js.map +1 -6
  42. package/dist/transport/socket/websocket-handler.d.ts +9 -13
  43. package/dist/transport/socket/websocket-handler.d.ts.map +1 -1
  44. package/dist/transport/socket/websocket-handler.js +148 -139
  45. package/dist/transport/socket/websocket-handler.js.map +1 -6
  46. package/dist/types/server-options.d.ts +1 -1
  47. package/dist/types/server-options.d.ts.map +1 -1
  48. package/dist/types/server-options.js +2 -1
  49. package/dist/types/server-options.js.map +1 -6
  50. package/dist/utils/config-manager.js +48 -46
  51. package/dist/utils/config-manager.js.map +1 -6
  52. package/dist/workers/service-protocol.worker.js +8 -11
  53. package/dist/workers/service-protocol.worker.js.map +1 -6
  54. package/docs/auth.md +28 -16
  55. package/docs/core.md +113 -54
  56. package/docs/legacy.md +21 -0
  57. package/docs/main.md +81 -0
  58. package/docs/protocol.md +31 -0
  59. package/docs/services.md +49 -44
  60. package/docs/transport.md +81 -76
  61. package/docs/types.md +31 -0
  62. package/docs/utilities.md +27 -0
  63. package/package.json +12 -13
  64. package/src/auth/jwt-manager.ts +2 -2
  65. package/src/core/define-service.ts +19 -19
  66. package/src/core/service-executor.ts +23 -17
  67. package/src/index.ts +10 -12
  68. package/src/legacy/v1-auto-update-handler.ts +10 -10
  69. package/src/protocol/protocol-wrapper.ts +16 -16
  70. package/src/service-server.ts +51 -43
  71. package/src/services/auto-update-service.ts +1 -1
  72. package/src/services/orm-service.ts +7 -7
  73. package/src/transport/http/http-request-handler.ts +16 -10
  74. package/src/transport/http/static-file-handler.ts +8 -8
  75. package/src/transport/http/upload-handler.ts +16 -9
  76. package/src/transport/socket/service-socket.ts +22 -22
  77. package/src/transport/socket/websocket-handler.ts +50 -70
  78. package/src/types/server-options.ts +1 -1
  79. package/src/utils/config-manager.ts +11 -11
  80. package/dist/services/smtp-client-service.d.ts +0 -8
  81. package/dist/services/smtp-client-service.d.ts.map +0 -1
  82. package/dist/services/smtp-client-service.js +0 -46
  83. package/dist/services/smtp-client-service.js.map +0 -6
  84. package/docs/server.md +0 -126
  85. package/src/services/smtp-client-service.ts +0 -59
  86. package/tests/define-service.spec.ts +0 -66
  87. package/tests/orm-service.spec.ts +0 -83
  88. package/tests/service-executor.spec.ts +0 -114
@@ -5,29 +5,29 @@ import { createServiceProtocol } from "@simplysm/service-common";
5
5
  import type * as ServiceProtocolWorkerModule from "../workers/service-protocol.worker";
6
6
 
7
7
  /**
8
- * Protocol wrapper interface
8
+ * 프로토콜 래퍼 인터페이스
9
9
  *
10
- * Automatically offloads heavy message encoding/decoding to a worker thread
11
- * while using main thread for lightweight operations.
10
+ * 무거운 메시지 인코딩/디코딩을 worker 스레드에 자동으로 위임하고,
11
+ * 가벼운 작업은 메인 스레드에서 처리한다.
12
12
  */
13
13
  export interface ServerProtocolWrapper {
14
14
  /**
15
- * Encode message (auto worker delegation)
15
+ * 메시지를 인코딩한다 (worker 자동 위임)
16
16
  */
17
17
  encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
18
18
 
19
19
  /**
20
- * Decode message (auto worker delegation)
20
+ * 메시지를 디코딩한다 (worker 자동 위임)
21
21
  */
22
22
  decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
23
23
 
24
24
  /**
25
- * Dispose protocol resources
25
+ * 프로토콜 리소스를 해제한다
26
26
  */
27
27
  dispose(): void;
28
28
  }
29
29
 
30
- // Shared worker instance (lazy singleton)
30
+ // 공유 worker 인스턴스 (지연 싱글턴)
31
31
  let sharedWorker: WorkerProxy<typeof ServiceProtocolWorkerModule> | undefined;
32
32
 
33
33
  function getWorker(): WorkerProxy<typeof ServiceProtocolWorkerModule> {
@@ -43,37 +43,37 @@ function getWorker(): WorkerProxy<typeof ServiceProtocolWorkerModule> {
43
43
  }
44
44
 
45
45
  /**
46
- * Create a protocol wrapper instance
46
+ * 프로토콜 래퍼 인스턴스를 생성한다
47
47
  *
48
- * Automatically offloads heavy message encoding/decoding to a worker thread
49
- * while using main thread for lightweight operations.
48
+ * 무거운 메시지 인코딩/디코딩을 worker 스레드에 자동으로 위임하고,
49
+ * 가벼운 작업은 메인 스레드에서 처리한다.
50
50
  */
51
51
  export function createServerProtocolWrapper(): ServerProtocolWrapper {
52
52
  // -------------------------------------------------------------------
53
- // State
53
+ // 상태
54
54
  // -------------------------------------------------------------------
55
55
 
56
56
  const protocol = createServiceProtocol();
57
57
  const SIZE_THRESHOLD = 30 * 1024; // 30KB
58
58
 
59
59
  // -------------------------------------------------------------------
60
- // Helpers
60
+ // 헬퍼
61
61
  // -------------------------------------------------------------------
62
62
 
63
63
  /**
64
- * Check if message should use worker for encoding
64
+ * 메시지 인코딩에 worker를 사용해야 하는지 확인한다
65
65
  */
66
66
  function shouldUseWorkerForEncode(msg: ServiceMessage): boolean {
67
67
  if (!("body" in msg)) return false;
68
68
 
69
69
  const body = msg.body;
70
70
 
71
- // Uint8Array: always use worker
71
+ // Uint8Array: 항상 worker 사용
72
72
  if (body instanceof Uint8Array) {
73
73
  return true;
74
74
  }
75
75
 
76
- // Array: check for Uint8Array elements (ORM results, etc.)
76
+ // 배열: Uint8Array 요소 존재 여부 확인 (ORM 결과 )
77
77
  if (Array.isArray(body)) {
78
78
  return body.length > 0 && body.some((item) => item instanceof Uint8Array);
79
79
  }
@@ -82,7 +82,7 @@ export function createServerProtocolWrapper(): ServerProtocolWrapper {
82
82
  }
83
83
 
84
84
  // -------------------------------------------------------------------
85
- // Public API
85
+ // 공개 API
86
86
  // -------------------------------------------------------------------
87
87
 
88
88
  return {
@@ -31,14 +31,18 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
31
31
  isOpen = false;
32
32
 
33
33
  private readonly _wsHandler: ReturnType<typeof createWebSocketHandler>;
34
+ private readonly _jwtSecret: string | undefined;
35
+ private _shutdownRegistered = false;
34
36
 
35
37
  readonly fastify: FastifyInstance;
36
38
 
37
39
  constructor(readonly options: ServiceServerOptions) {
38
40
  super();
39
41
 
40
- // SSL configuration (synchronous)
41
- // Note: Fastify HTTPS requires Buffer type (Uint8Array not directly usable)
42
+ this._jwtSecret = options.auth != null && options.auth !== false ? options.auth.jwtSecret : undefined;
43
+
44
+ // SSL 설정 (동기)
45
+ // 참고: Fastify HTTPS는 Buffer 타입이 필요함 (Uint8Array를 직접 사용할 수 없음)
42
46
  const httpsConf = options.ssl
43
47
  ? { pfx: Buffer.from(options.ssl.pfxBytes), passphrase: options.ssl.passphrase }
44
48
  : null;
@@ -47,17 +51,27 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
47
51
 
48
52
  this._wsHandler = createWebSocketHandler(
49
53
  (def) => executeServiceMethod(this, def),
50
- options.auth?.jwtSecret,
54
+ this._jwtSecret,
51
55
  );
52
56
  }
53
57
 
54
58
  async listen(): Promise<void> {
55
- logger.info(`Server starting... ${env.VER ?? ""}`);
59
+ logger.info(`서버 시작 중... ${env.VER ?? ""}`);
60
+
61
+ // auth 설정 검증: auth 미설정(undefined)인데 auth 요구 서비스가 있으면 에러
62
+ if (this.options.auth === undefined) {
63
+ const authRequiredService = this.options.services.find((s) => s.authPermissions != null);
64
+ if (authRequiredService != null) {
65
+ throw new Error(
66
+ `auth 설정이 필요합니다: 서비스 [${authRequiredService.name}]에 auth가 설정되어 있습니다.`,
67
+ );
68
+ }
69
+ }
56
70
 
57
- // WebSocket plugin
71
+ // WebSocket 플러그인
58
72
  await this.fastify.register(fastifyWebsocket);
59
73
 
60
- // Security plugin
74
+ // 보안 플러그인
61
75
  await this.fastify.register(fastifyHelmet, {
62
76
  global: true,
63
77
  contentSecurityPolicy: {
@@ -78,17 +92,17 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
78
92
  originAgentCluster: false,
79
93
  });
80
94
 
81
- // Upload plugin
95
+ // 업로드 플러그인
82
96
  await this.fastify.register(fastifyMultipart);
83
97
 
84
- // Register @fastify/static
98
+ // @fastify/static 등록
85
99
  await this.fastify.register(fastifyStatic, {
86
100
  root: path.resolve(this.options.rootPath, "www"),
87
101
  wildcard: false,
88
102
  serve: false,
89
103
  });
90
104
 
91
- // CORS configuration
105
+ // CORS 설정
92
106
  await this.fastify.register(fastifyCors, {
93
107
  origin: (_origin, cb) => {
94
108
  cb(null, true);
@@ -97,7 +111,7 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
97
111
  exposedHeaders: ["Content-Disposition", "Content-Length"],
98
112
  });
99
113
 
100
- // JSON parser
114
+ // JSON 파서
101
115
  this.fastify.addContentTypeParser(
102
116
  "application/json",
103
117
  { parseAs: "string" },
@@ -113,22 +127,22 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
113
127
  },
114
128
  );
115
129
 
116
- // JSON serializer
130
+ // JSON 직렬화기
117
131
  this.fastify.setSerializerCompiler(() => (data) => json.stringify(data));
118
132
 
119
- // API routes
133
+ // API 라우트
120
134
  this.fastify.all("/api/:service/:method", async (req, reply) => {
121
- await handleHttpRequest(req, reply, this.options.auth?.jwtSecret, (def) =>
135
+ await handleHttpRequest(req, reply, this._jwtSecret, (def) =>
122
136
  executeServiceMethod(this, def),
123
137
  );
124
138
  });
125
139
 
126
- // Upload route
140
+ // 업로드 라우트
127
141
  this.fastify.all("/upload", async (req, reply) => {
128
- await handleUpload(req, reply, this.options.rootPath, this.options.auth?.jwtSecret);
142
+ await handleUpload(req, reply, this.options.rootPath, this._jwtSecret);
129
143
  });
130
144
 
131
- // WebSocket route
145
+ // WebSocket 라우트
132
146
  const onWebSocketConnected = (socket: WebSocket, req: FastifyRequest) => {
133
147
  const { ver, clientId, clientName } = req.query as {
134
148
  ver: string | undefined;
@@ -138,15 +152,15 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
138
152
 
139
153
  if (ver === "2") {
140
154
  if (clientId == null || clientName == null) {
141
- socket.close(1008, "Missing Client ID/NAME");
155
+ socket.close(1008, "클라이언트 ID/NAME이 누락되었습니다");
142
156
  return;
143
157
  }
144
158
  this._wsHandler.addSocket(socket, clientId, clientName, req);
145
159
  } else {
146
- // V1 legacy support (auto-update only)
160
+ // V1 레거시 지원 (자동 업데이트 전용)
147
161
  const autoUpdateDef = this.options.services.find((s) => s.name === "AutoUpdate");
148
162
  if (autoUpdateDef == null) {
149
- socket.close(1008, "AutoUpdate service not configured");
163
+ socket.close(1008, "AutoUpdate 서비스가 설정되지 않았습니다");
150
164
  return;
151
165
  }
152
166
 
@@ -163,7 +177,7 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
163
177
  this.fastify.get("/", { websocket: true }, onWebSocketConnected.bind(this));
164
178
  this.fastify.get("/ws", { websocket: true }, onWebSocketConnected.bind(this));
165
179
 
166
- // Static file wildcard handler
180
+ // 정적 파일 와일드카드 핸들러
167
181
  this.fastify.route({
168
182
  method: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"],
169
183
  url: "/*",
@@ -175,19 +189,19 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
175
189
  },
176
190
  });
177
191
 
178
- // HTTP server-level error handling
192
+ // HTTP 서버 수준 에러 핸들링
179
193
  this.fastify.server.on("error", (err) => {
180
- logger.error("HTTP server error", err);
194
+ logger.error("HTTP 서버 에러", err);
181
195
  });
182
196
 
183
- // Listen
197
+ // 리슨
184
198
  await this.fastify.listen({ port: this.options.port, host: "0.0.0.0" });
185
199
 
186
- // Register graceful shutdown handler
200
+ // 정상 종료 핸들러 등록
187
201
  this._registerGracefulShutdown();
188
202
 
189
203
  this.isOpen = true;
190
- logger.info(`Server started (port: ${this.options.port})`);
204
+ logger.info(`서버 시작됨 (포트: ${this.options.port})`);
191
205
  this.emit("ready");
192
206
  }
193
207
 
@@ -196,15 +210,10 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
196
210
  await this.fastify.close();
197
211
 
198
212
  this.isOpen = false;
199
- logger.debug("Server closed");
213
+ logger.debug("서버 종료됨");
200
214
  this.emit("close");
201
215
  }
202
216
 
203
- async broadcastReload(clientName: string | undefined, changedFileSet: Set<string>) {
204
- logger.debug("Broadcasting RELOAD to all server clients");
205
- await this._wsHandler.broadcastReload(clientName, changedFileSet);
206
- }
207
-
208
217
  async emitEvent<TInfo, TData>(
209
218
  eventDef: ServiceEventDef<TInfo, TData>,
210
219
  infoSelector: (item: TInfo) => boolean,
@@ -214,25 +223,24 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
214
223
  }
215
224
 
216
225
  async signAuthToken(payload: AuthTokenPayload<TAuthInfo>) {
217
- const jwtSecret = this.options.auth?.jwtSecret;
218
- if (jwtSecret == null) throw new Error("JWT Secret is not defined.");
219
-
220
- return signJwt(jwtSecret, payload);
226
+ if (this._jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
227
+ return signJwt(this._jwtSecret, payload);
221
228
  }
222
229
 
223
230
  async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>> {
224
- const jwtSecret = this.options.auth?.jwtSecret;
225
- if (jwtSecret == null) throw new Error("JWT Secret is not defined.");
226
-
227
- return verifyJwt(jwtSecret, token);
231
+ if (this._jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
232
+ return verifyJwt(this._jwtSecret, token);
228
233
  }
229
234
 
230
235
  private _registerGracefulShutdown() {
236
+ if (this._shutdownRegistered) return;
237
+ this._shutdownRegistered = true;
238
+
231
239
  const shutdownHandler = async (signal: string) => {
232
- logger.info(`${signal} signal received. Starting server shutdown...`);
240
+ logger.info(`${signal} 시그널 수신됨. 서버 종료를 시작합니다...`);
233
241
 
234
242
  const forceExitTimer = setTimeout(() => {
235
- logger.error("Server shutdown timed out (10s). Forcing exit.");
243
+ logger.error("서버 종료 시간 초과 (10초). 강제 종료합니다.");
236
244
  process.exit(1);
237
245
  }, 10000);
238
246
 
@@ -240,11 +248,11 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
240
248
  if (this.isOpen) {
241
249
  await this.close();
242
250
  }
243
- logger.info("Server shut down gracefully.");
251
+ logger.info("서버가 정상적으로 종료되었습니다.");
244
252
  clearTimeout(forceExitTimer);
245
253
  process.exit(0);
246
254
  } catch (err) {
247
- logger.error("Error during server shutdown", err);
255
+ logger.error("서버 종료 에러 발생", err);
248
256
  process.exit(1);
249
257
  }
250
258
  };
@@ -12,7 +12,7 @@ export const AutoUpdateService = defineService("AutoUpdate", (ctx) => ({
12
12
  | undefined
13
13
  > {
14
14
  const clientPath = ctx.clientPath;
15
- if (clientPath == null) throw new Error("Client path not found.");
15
+ if (clientPath == null) throw new Error("클라이언트 경로를 찾을 수 없습니다.");
16
16
 
17
17
  if (!(await fsx.exists(path.resolve(clientPath, platform, "updates")))) return undefined;
18
18
 
@@ -15,7 +15,7 @@ import consola from "consola";
15
15
 
16
16
  const logger = consola.withTag("service-server:OrmService");
17
17
 
18
- // Static state needs to live outside the factory (shared across calls)
18
+ // 정적 상태는 팩토리 외부에 존재해야 (호출 공유)
19
19
  const socketConns = new WeakMap<ServiceSocket, Map<number, DbConn>>();
20
20
 
21
21
  export const OrmService = defineService(
@@ -24,7 +24,7 @@ export const OrmService = defineService(
24
24
  const sock = (): ServiceSocket => {
25
25
  const socket = ctx.socket;
26
26
  if (socket == null) {
27
- throw new Error("WebSocket connection is required. ORM service cannot be used over HTTP.");
27
+ throw new Error("WebSocket 연결이 필요합니다. ORM 서비스는 HTTP로 사용할 없습니다.");
28
28
  }
29
29
  return socket;
30
30
  };
@@ -34,7 +34,7 @@ export const OrmService = defineService(
34
34
  opt.configName
35
35
  ];
36
36
  if (config == null) {
37
- throw new Error(`ORM configuration not found: ${opt.configName}`);
37
+ throw new Error(`ORM 설정을 찾을 없습니다: ${opt.configName}`);
38
38
  }
39
39
  return { ...config, ...opt.config } as DbConnConfig;
40
40
  };
@@ -43,7 +43,7 @@ export const OrmService = defineService(
43
43
  const myConns = socketConns.get(sock());
44
44
  const conn = myConns?.get(connId);
45
45
  if (conn == null) {
46
- throw new Error("Not connected to database. (Invalid Connection ID)");
46
+ throw new Error("데이터베이스에 연결되지 않았습니다. (유효하지 않은 연결 ID)");
47
47
  }
48
48
  return conn;
49
49
  };
@@ -71,7 +71,7 @@ export const OrmService = defineService(
71
71
  sock().on("close", async () => {
72
72
  if (myConns == null) return;
73
73
 
74
- logger.debug("Socket close detected: cleaning up all open DB connections.");
74
+ logger.debug("소켓 종료 감지: 열린 모든 DB 연결을 정리합니다.");
75
75
  const conns = Array.from(myConns.values());
76
76
 
77
77
  await Promise.all(
@@ -81,7 +81,7 @@ export const OrmService = defineService(
81
81
  await conn.close();
82
82
  }
83
83
  } catch (err) {
84
- logger.warn("Error ignored during forced DB connection close", err);
84
+ logger.warn("강제 DB 연결 종료 에러 무시됨", err);
85
85
  }
86
86
  }),
87
87
  );
@@ -110,7 +110,7 @@ export const OrmService = defineService(
110
110
  const conn = getConn(connId);
111
111
  await conn.close();
112
112
  } catch (err) {
113
- logger.warn("Error ignored during DB connection close", err);
113
+ logger.warn("DB 연결 종료 에러 무시됨", err);
114
114
  }
115
115
  },
116
116
 
@@ -16,49 +16,55 @@ export async function handleHttpRequest<TAuthInfo = unknown>(
16
16
  ): Promise<void> {
17
17
  const { service, method } = req.params as { service: string; method: string };
18
18
 
19
- // ClientName header
19
+ // ClientName 헤더
20
20
  const clientName = req.headers["x-sd-client-name"] as string | undefined;
21
- if (clientName == null) throw new Error("ClientName header is required");
21
+ if (clientName == null) throw new Error("ClientName 헤더가 필요합니다");
22
22
 
23
- // Parse and verify Authorization header
23
+ // Authorization 헤더 파싱 검증
24
24
  let authTokenPayload: AuthTokenPayload<TAuthInfo> | undefined;
25
25
  try {
26
26
  const authHeader = req.headers.authorization;
27
27
  if (authHeader != null) {
28
- if (jwtSecret == null) throw new Error("JWT Secret is not defined.");
28
+ if (jwtSecret == null) throw new Error("JWT Secret 정의되지 않았습니다.");
29
29
 
30
30
  const token = authHeader.split(" ")[1]; // "Bearer <token>"
31
31
  authTokenPayload = await verifyJwt<TAuthInfo>(jwtSecret, token);
32
32
  }
33
33
  } catch (err) {
34
34
  reply.status(401).send({
35
- error: "Unauthorized",
35
+ error: "인증 실패",
36
36
  message: err instanceof Error ? err.message : String(err),
37
37
  });
38
38
  return;
39
39
  }
40
40
 
41
- // Parse parameters
41
+ // 매개변수 파싱
42
42
  let params: unknown[] | undefined;
43
43
  if (req.method === "GET") {
44
44
  const query = req.query as { json?: string };
45
45
  if (typeof query.json !== "string") {
46
- throw new Error("JSON query parameter required");
46
+ throw new Error("JSON 쿼리 파라미터가 필요합니다");
47
47
  }
48
48
  params = json.parse(query.json);
49
49
  } else if (req.method === "POST") {
50
50
  if (!Array.isArray(req.body)) {
51
51
  reply.status(400).send({
52
- error: "Bad Request",
53
- message: "Request body must be an array.",
52
+ error: "잘못된 요청",
53
+ message: "요청 본문은 배열이어야 합니다.",
54
54
  });
55
55
  return;
56
56
  }
57
57
 
58
58
  params = req.body as unknown[];
59
+ } else {
60
+ reply.status(405).send({
61
+ error: "Method Not Allowed",
62
+ message: `${req.method} 메서드는 지원하지 않습니다.`,
63
+ });
64
+ return;
59
65
  }
60
66
 
61
- // Execute service and send response
67
+ // 서비스 실행 응답 전송
62
68
  if (params != null) {
63
69
  const serviceResult = await runMethod({
64
70
  serviceName: service,
@@ -14,12 +14,12 @@ export async function handleStaticFile(
14
14
  let targetFilePath = path.resolve(rootPath, "www", urlPath);
15
15
  const allowedRootPath = path.resolve(rootPath, "www");
16
16
 
17
- // Security guard for targetPath (prevent path traversal)
17
+ // targetPath 보안 가드 (경로 탐색 공격 방지)
18
18
  if (targetFilePath !== allowedRootPath && !pathx.isChildPath(targetFilePath, allowedRootPath)) {
19
- throw new Error("Access denied");
19
+ throw new Error("접근이 거부되었습니다");
20
20
  }
21
21
 
22
- // Redirect with trailing slash for directories (standard web server behavior)
22
+ // 디렉토리에 대해 슬래시를 추가하여 리다이렉트 (표준 서버 동작)
23
23
  if ((await fsx.exists(targetFilePath)) && (await fsx.stat(targetFilePath)).isDirectory()) {
24
24
  if (!urlPath.endsWith("/")) {
25
25
  const urlObj = new URL(req.raw.url!, "http://localhost");
@@ -29,15 +29,15 @@ export async function handleStaticFile(
29
29
  targetFilePath = path.resolve(targetFilePath, "index.html");
30
30
  }
31
31
 
32
- // Permission check (hidden files, etc.)
32
+ // 권한 확인 (숨김 파일 )
33
33
  if (path.basename(targetFilePath).startsWith(".")) {
34
- const errorMessage = "You do not have permission to access this file.";
34
+ const errorMessage = " 파일에 접근할 권한이 없습니다.";
35
35
  responseErrorHtml(reply, 403, errorMessage);
36
36
  logger.warn(`[403] ${errorMessage} (${targetFilePath})`);
37
37
  return;
38
38
  }
39
39
 
40
- // Send file
40
+ // 파일 전송
41
41
  const filename = path.basename(targetFilePath);
42
42
  const directory = path.dirname(targetFilePath);
43
43
 
@@ -46,11 +46,11 @@ export async function handleStaticFile(
46
46
  } catch (err: unknown) {
47
47
  const error = err as { code?: string };
48
48
  if (error.code === "ENOENT") {
49
- const errorMessage = "File not found.";
49
+ const errorMessage = "파일을 찾을 수 없습니다.";
50
50
  responseErrorHtml(reply, 404, errorMessage);
51
51
  logger.warn(`[404] ${errorMessage} (${targetFilePath})`);
52
52
  } else {
53
- const errorMessage = "An error occurred while sending the file.";
53
+ const errorMessage = "파일 전송 에러가 발생했습니다.";
54
54
  responseErrorHtml(reply, 500, errorMessage);
55
55
  logger.error(`[500] ${errorMessage}`, err);
56
56
  }
@@ -17,24 +17,24 @@ export async function handleUpload(
17
17
  jwtSecret: string | undefined,
18
18
  ): Promise<void> {
19
19
  if (!req.isMultipart()) {
20
- reply.status(400).send("Multipart request expected");
20
+ reply.status(400).send("Multipart 요청이 필요합니다");
21
21
  return;
22
22
  }
23
23
 
24
- // Auth check
24
+ // 인증 확인
25
25
  try {
26
26
  const authHeader = req.headers.authorization;
27
27
  if (authHeader == null) {
28
- throw new Error("Authentication token is missing.");
28
+ throw new Error("인증 토큰이 누락되었습니다.");
29
29
  }
30
30
  if (jwtSecret == null) {
31
- throw new Error("JWT Secret is not defined.");
31
+ throw new Error("JWT Secret 정의되지 않았습니다.");
32
32
  }
33
33
  const token = authHeader.split(" ")[1];
34
34
  await verifyJwt(jwtSecret, token);
35
35
  } catch (err) {
36
36
  reply.status(401).send({
37
- error: "Unauthorized",
37
+ error: "인증 실패",
38
38
  message: err instanceof Error ? err.message : String(err),
39
39
  });
40
40
  return;
@@ -58,7 +58,7 @@ export async function handleUpload(
58
58
  await pipeline(part.file, createWriteStream(currentSavePath));
59
59
 
60
60
  if (part.file.truncated) {
61
- throw new Error(`File limit exceeded: ${originalFilename}`);
61
+ throw new Error(`파일 크기 제한 초과: ${originalFilename}`);
62
62
  }
63
63
 
64
64
  const stats = await fsx.stat(currentSavePath);
@@ -75,13 +75,20 @@ export async function handleUpload(
75
75
 
76
76
  reply.send(result);
77
77
  } catch (err) {
78
- logger.error("Upload Error", err);
78
+ logger.error("업로드 에러", err);
79
79
 
80
80
  if (currentSavePath != null) {
81
81
  await fsx.rm(currentSavePath).catch(() => {});
82
- logger.warn(`Incomplete file deleted: ${currentSavePath}`);
82
+ logger.warn(`불완전한 파일 삭제됨: ${currentSavePath}`);
83
83
  }
84
84
 
85
- reply.code(500).send("Upload Failed");
85
+ // 이미 저장된 파일 정리
86
+ for (const savedFile of result) {
87
+ const savedPath = path.resolve(rootPath, "www", savedFile.path);
88
+ await fsx.rm(savedPath).catch(() => {});
89
+ logger.warn(`이미 저장된 파일 삭제됨: ${savedPath}`);
90
+ }
91
+
92
+ reply.code(500).send("업로드 실패");
86
93
  }
87
94
  }