dicoshot-nest 0.2.2 → 0.3.0

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 (33) hide show
  1. package/README.ko.md +415 -0
  2. package/README.md +387 -69
  3. package/dist/app.controller.js.map +1 -1
  4. package/dist/app.module.js.map +1 -1
  5. package/dist/decorators/dicoshot-notify.decorator.d.ts +8 -0
  6. package/dist/decorators/dicoshot-notify.decorator.js +8 -0
  7. package/dist/decorators/dicoshot-notify.decorator.js.map +1 -0
  8. package/dist/dicoshot.constants.d.ts +1 -0
  9. package/dist/dicoshot.constants.js +2 -1
  10. package/dist/dicoshot.constants.js.map +1 -1
  11. package/dist/dicoshot.listener.js.map +1 -1
  12. package/dist/dicoshot.module.js +3 -0
  13. package/dist/dicoshot.module.js.map +1 -1
  14. package/dist/dicoshot.service.d.ts +6 -3
  15. package/dist/dicoshot.service.js +17 -8
  16. package/dist/dicoshot.service.js.map +1 -1
  17. package/dist/filters/dicoshot-exception.filter.js +16 -8
  18. package/dist/filters/dicoshot-exception.filter.js.map +1 -1
  19. package/dist/index.d.ts +7 -4
  20. package/dist/index.js +9 -5
  21. package/dist/index.js.map +1 -1
  22. package/dist/interceptors/dicoshot-notify.interceptor.d.ts +10 -0
  23. package/dist/interceptors/dicoshot-notify.interceptor.js +45 -0
  24. package/dist/interceptors/dicoshot-notify.interceptor.js.map +1 -0
  25. package/dist/interceptors/dicoshot.interceptor.d.ts +1 -1
  26. package/dist/interceptors/dicoshot.interceptor.js +37 -24
  27. package/dist/interceptors/dicoshot.interceptor.js.map +1 -1
  28. package/dist/main.js.map +1 -1
  29. package/dist/utils/stack.util.d.ts +3 -0
  30. package/dist/utils/stack.util.js +20 -0
  31. package/dist/utils/stack.util.js.map +1 -0
  32. package/package.json +9 -19
  33. package/dist/tsconfig.build.tsbuildinfo +0 -1
package/README.ko.md ADDED
@@ -0,0 +1,415 @@
1
+ # dicoshot-nest
2
+
3
+ [English](README.md) | 한국어
4
+
5
+ NestJS 애플리케이션의 시작/종료, 예외 발생, 느린 응답을 Discord 채널로 자동 알림하는 SDK입니다. 모듈 등록만으로 동작합니다.
6
+
7
+ ## 특징
8
+
9
+ - **자동 알림**: `OnApplicationBootstrap`/`OnApplicationShutdown` 훅으로 별도 코드 없이 시작/종료 알림
10
+ - **예외 자동 알림**: 전역 `ExceptionFilter`로 처리되지 않은 예외를 Discord로 즉시 전송 (스택트레이스, 요청 바디 포함 가능), 에러가 발생한 정확한 위치(`file:line:col`)를 별도의 `Location` 필드로 표시
11
+ - **느린 응답 알림**: `NestInterceptor`로 응답 시간이 임계값을 초과하면 알림
12
+ - **커스텀 메시지**: `DicoshotService`를 주입받아 임의의 타이밍에 Discord 메시지 직접 발송, 또는 `@DicoshotNotify()`로 핸들러 성공 시 자동 발송
13
+ - **장애 격리**: webhook 전송 실패가 앱 기동/종료/요청 처리를 막지 않음 (WARN 로그만 출력)
14
+ - **MSA 친화**: hostname과 applicationName이 메시지에 자동 포함되어 인스턴스 구분 용이
15
+ - **환경 자동 감지**: `NODE_ENV`, `npm_package_version` 자동 포함
16
+ - **중복 알림 방지**: 필터와 인터셉터를 동시에 사용해도 같은 에러가 두 번 알림되지 않음
17
+ - **알림 언어 선택**: `locale` 옵션으로 알림 제목/필드 이름(Service, Environment, Status, Location 등)을 영어·한국어·일본어·중국어로 발송 — 그 외 언어도 직접 번역을 넘겨서 사용 가능
18
+
19
+ ## 모듈 구성
20
+
21
+ | 모듈 | 설명 |
22
+ | --------------- | ------------------------------------------------------------------- |
23
+ | `dicoshot-core` | NestJS 의존성 없는 순수 TypeScript. 메시지 모델, Webhook 클라이언트 |
24
+ | `dicoshot-nest` | NestJS DynamicModule, 라이프사이클 훅, ExceptionFilter, Interceptor |
25
+
26
+ ## 설치
27
+
28
+ ```bash
29
+ npm install dicoshot-nest
30
+ ```
31
+
32
+ ## 빠른 시작
33
+
34
+ ```typescript
35
+ // app.module.ts
36
+ import { Module } from '@nestjs/common';
37
+ import { DicoshotModule } from 'dicoshot-nest';
38
+
39
+ @Module({
40
+ imports: [
41
+ DicoshotModule.register({
42
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
43
+ applicationName: 'my-service',
44
+ }),
45
+ ],
46
+ })
47
+ export class AppModule {}
48
+ ```
49
+
50
+ 앱을 실행하면 Discord 채널에 다음과 같은 embed가 도착합니다.
51
+
52
+ - **시작 시**: 녹색 embed, 제목 "🟢 Application Started", 서비스명/환경/버전/hostname/시간 포함
53
+ - **종료 시**: 빨간 embed, 제목 "🔴 Application Stopped"
54
+
55
+ ## ConfigService 연동
56
+
57
+ ```typescript
58
+ DicoshotModule.registerAsync({
59
+ imports: [ConfigModule],
60
+ useFactory: (config: ConfigService) => ({
61
+ webhookUrl: config.get('DISCORD_WEBHOOK_URL'),
62
+ applicationName: config.get('APP_NAME'),
63
+ }),
64
+ inject: [ConfigService],
65
+ // filter/interceptor는 registerAsync 호출 시점에 별도로 지정합니다.
66
+ // (DynamicModule의 APP_FILTER/APP_INTERCEPTOR 등록 여부를 결정하기 때문에
67
+ // 비동기 팩토리 결과와 무관하게 등록 시점 값으로 고정됩니다)
68
+ filter: true,
69
+ interceptor: { slowThreshold: 2000 },
70
+ });
71
+ ```
72
+
73
+ ## 커스텀 메시지 직접 발송
74
+
75
+ `DicoshotService`를 주입받아 코드 어디서든 Discord 메시지를 발송할 수 있습니다.
76
+
77
+ ### sendCustom() — 편의 메서드
78
+
79
+ ```typescript
80
+ @Injectable()
81
+ export class DeployService {
82
+ constructor(private readonly dicoshot: DicoshotService) {}
83
+
84
+ async onDeployComplete(version: string) {
85
+ await this.dicoshot.sendCustom({
86
+ title: '배포 완료',
87
+ description: `v${version} 정상 배포`,
88
+ color: 'success', // 'success' | 'danger' | 'warning' | 'info'
89
+ fields: [{ name: '버전', value: `v${version}`, inline: true }],
90
+ mention: '<@&123456789012345678>', // description 뒤에 덧붙여짐
91
+ });
92
+ }
93
+ }
94
+ ```
95
+
96
+ 색상 프리셋:
97
+
98
+ | 값 | 색상 |
99
+ | ----------- | ------ |
100
+ | `'success'` | 녹색 |
101
+ | `'danger'` | 빨간색 |
102
+ | `'warning'` | 노란색 |
103
+ | `'info'` | 파란색 |
104
+
105
+ hex 값을 직접 지정할 수도 있습니다: `color: 0xFF0000`
106
+
107
+ ### send() — raw 메서드
108
+
109
+ Discord embed 구조를 직접 구성해서 발송합니다.
110
+
111
+ ```typescript
112
+ await this.dicoshot.send({
113
+ embeds: [
114
+ {
115
+ title: '알림',
116
+ description: '...',
117
+ color: 0x57f287,
118
+ fields: [{ name: '버전', value: 'v1.2.3', inline: true }],
119
+ },
120
+ ],
121
+ });
122
+ ```
123
+
124
+ > 두 메서드 모두 에러를 throw하지 않습니다. 전송 성공 여부를 나타내는 `boolean`을 반환하며, 실패 시에는 SDK의 다른 부분과 동일하게 WARN 로그만 남깁니다.
125
+
126
+ ### `@DicoshotNotify()` — 데코레이터
127
+
128
+ 컨트롤러(또는 Nest 파이프라인을 거치는 모든 핸들러)에 데코레이터를 붙이면, 해당 핸들러가 성공적으로 끝났을 때 커스텀 메시지를 자동으로 전송합니다. `@UseInterceptors()`를 따로 설정할 필요 없이, 인터셉터가 전역으로 등록되어 데코레이터가 없는 핸들러에서는 아무 동작도 하지 않습니다. 핸들러가 예외를 던지면 메시지를 보내지 않습니다(에러 알림은 `filter`/`interceptor`를 사용하세요).
129
+
130
+ ```typescript
131
+ @Controller('orders')
132
+ export class OrderController {
133
+ @Post()
134
+ @DicoshotNotify({ title: '새 주문 생성', color: 'success' })
135
+ create(@Body() dto: CreateOrderDto) {
136
+ return this.orderService.create(dto);
137
+ }
138
+
139
+ @Post(':id/deploy')
140
+ @DicoshotNotify({
141
+ title: (args) => `배포 완료: ${args[0]}`, // args = 핸들러 인자 배열, 순서대로
142
+ description: (_args, result) => `결과: ${JSON.stringify(result)}`,
143
+ color: 'success',
144
+ })
145
+ deploy(@Param('id') id: string) {
146
+ return this.orderService.deploy(id);
147
+ }
148
+ }
149
+ ```
150
+
151
+ `title`/`description`은 고정 문자열 또는 `(args, result) => string` 형태의 함수를 받을 수 있습니다. `args`는 핸들러의 인자 배열, `result`는 반환값입니다.
152
+
153
+ ## 예외 자동 알림 (`filter`)
154
+
155
+ `filter` 옵션을 켜면 `DicoshotExceptionFilter`가 전역 `APP_FILTER`로 등록됩니다. 기본적으로 처리되지 않은 예외와 HTTP status `500` 이상인 에러만 Discord로 전송합니다(`NotFoundException` 같은 4xx `HttpException`은 `minStatus`를 낮추지 않으면 알림하지 않습니다). HTTP 컨텍스트가 아닌 경우(WebSocket, RPC 등)는 알림하지 않고 무시합니다.
156
+
157
+ 스택트레이스를 사용할 수 있는 경우, 에러가 실제로 던져진 `file:line:col`을 스택의 맨 위 프레임에서 뽑아 `Location` 필드로 따로 보여줍니다 — 전체 `Stack Trace` 블록을 다 읽지 않아도 바로 위치를 확인할 수 있습니다.
158
+
159
+ ```typescript
160
+ DicoshotModule.register({
161
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
162
+ applicationName: 'order-service',
163
+ filter: true, // 또는 FilterOptions 객체
164
+ });
165
+ ```
166
+
167
+ Discord에는 이런 형태로 도착합니다:
168
+
169
+ > 🚨 **[production] order-service — TypeError**
170
+ > `Cannot read properties of undefined (reading 'id')`
171
+ >
172
+ > **Service** `order-service` **Environment** `production` **Status** `500`
173
+ > **Method** `POST` **Path** `/orders`
174
+ >
175
+ > **Location**
176
+ > `/app/src/order/order.service.ts:42:18`
177
+ >
178
+ > **Request Body**
179
+ > ```json
180
+ > {"productId":"abc123","quantity":2}
181
+ > ```
182
+ >
183
+ > **Stack Trace**
184
+ > ```
185
+ > TypeError: Cannot read properties of undefined (reading 'id')
186
+ > at OrderService.create (/app/src/order/order.service.ts:42:18)
187
+ > at OrderController.create (/app/src/order/order.controller.ts:15:30)
188
+ > ```
189
+
190
+ `Location`은 항상 `Stack Trace`의 맨 첫 줄과 같은 내용입니다 — 전체 스택을 다 안 읽어도 에러가 실제로 어디서 터졌는지 바로 보이도록 따로 빼서 보여주는 필드입니다.
191
+
192
+ ### FilterOptions
193
+
194
+ | 키 | 기본값 | 설명 |
195
+ | ---------------- | ------ | -------------------------------------------------------------- |
196
+ | `minStatus` | `500` | 이 값 이상인 status 코드만 알림 (처리되지 않은 예외는 항상 `500`으로 취급) |
197
+ | `ignore` | - | 알림하지 않을 HTTP status 코드 배열 (예: `[404]`) |
198
+ | `environment` | - | 이 환경에서만 알림 (`string` 또는 `string[]`, `NODE_ENV` 기준) |
199
+ | `mention` | - | embed 본문에 추가할 멘션 문자열 (예: `'<@&ROLE_ID>'`) |
200
+ | `throttle` | - | 동일 에러(클래스+메서드+경로) 반복 알림을 N초 동안 억제 |
201
+ | `includeStack` | `true` | 스택트레이스 포함 여부 |
202
+ | `includeRequest` | `true` | 요청 바디 포함 여부 |
203
+
204
+ ```typescript
205
+ DicoshotModule.register({
206
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
207
+ filter: {
208
+ ignore: [404, 401],
209
+ environment: 'production',
210
+ mention: '<@&123456789012345678>',
211
+ throttle: 60, // 같은 에러는 60초에 한 번만 알림
212
+ },
213
+ });
214
+ ```
215
+
216
+ 에러 알림은 `webhooks.error`가 설정되어 있으면 그 URL로, 없으면 기본 `webhookUrl`로 전송됩니다.
217
+
218
+ ## 느린 응답 / 에러 알림 (`interceptor`)
219
+
220
+ `interceptor` 옵션을 켜면 `DicoshotInterceptor`가 전역 `APP_INTERCEPTOR`로 등록되어, 응답 시간이 임계값을 초과하면 Discord로 알림합니다. `filter`가 켜져 있지 않은 경우에는 에러 알림도 인터셉터가 함께 처리합니다(필터가 켜져 있으면 중복 알림을 막기 위해 인터셉터는 에러 알림을 보내지 않습니다) — 이때도 위에서 설명한 `Location`, `Stack Trace` 필드가 동일하게 포함됩니다.
221
+
222
+ ```typescript
223
+ DicoshotModule.register({
224
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
225
+ interceptor: true, // 또는 InterceptorOptions 객체
226
+ });
227
+ ```
228
+
229
+ ### InterceptorOptions
230
+
231
+ | 키 | 기본값 | 설명 |
232
+ | --------------- | ------- | ---------------------------------------------------- |
233
+ | `slowThreshold` | `3000` | 느린 응답으로 판단할 기준 시간 (ms) |
234
+ | `excludePaths` | - | 알림에서 제외할 경로 prefix 배열 (예: `['/health']`) |
235
+ | `onlyErrors` | `false` | `true`이면 느린 응답 알림은 끄고 에러 알림만 수행 |
236
+ | `minStatus` | `500` | 이 값 이상인 status의 에러만 알림 (처리되지 않은 예외는 항상 `500`으로 취급) |
237
+
238
+ ```typescript
239
+ DicoshotModule.register({
240
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
241
+ interceptor: {
242
+ slowThreshold: 1500,
243
+ excludePaths: ['/health', '/metrics'],
244
+ },
245
+ });
246
+ ```
247
+
248
+ 느린 응답 알림은 `webhooks.slow`가 설정되어 있으면 그 URL로, 없으면 기본 `webhookUrl`로 전송됩니다.
249
+
250
+ ## Webhook 재전송 (`retry`)
251
+
252
+ webhook 전송이 실패하면(네트워크 오류, Discord 5xx, rate limit 등) 지수 백오프로 재시도합니다. Discord가 `429`와 함께 `Retry-After` 헤더를 보내면 그 값을 우선 사용하고, 그 외에는 `backoffMs * 2^시도횟수`로 대기합니다.
253
+
254
+ ```typescript
255
+ DicoshotModule.register({
256
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
257
+ retry: true, // 또는 RetryOptions 객체. 기본값은 꺼져 있음 (재시도 없음)
258
+ });
259
+ ```
260
+
261
+ ### RetryOptions
262
+
263
+ | 키 | 기본값 | 설명 |
264
+ | ----------- | ------ | -------------------------------------------------------- |
265
+ | `attempts` | `2` | 최초 시도 실패 후 재시도 횟수 |
266
+ | `backoffMs` | `500` | 첫 재시도까지의 대기 시간 (ms). 이후 시도마다 2배씩 증가 |
267
+
268
+ ```typescript
269
+ DicoshotModule.register({
270
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
271
+ retry: { attempts: 3, backoffMs: 300 },
272
+ });
273
+ ```
274
+
275
+ 모든 재시도가 실패하면 최종 에러가 호출자에게 전달됩니다. `DicoshotListener`/`DicoshotExceptionFilter`/`DicoshotInterceptor`는 이 에러를 잡아 WARN 로그만 남기므로 앱 동작에는 영향이 없습니다.
276
+
277
+ ## 설정
278
+
279
+ | 키 | 기본값 | 설명 |
280
+ | ------------------ | ---------- | --------------------------------------------------------------------------------------- |
281
+ | `webhookUrl` | - | Discord Webhook URL. 미설정 시 자동 비활성화 |
282
+ | `enabled` | `true` | 전체 활성화 토글 (시작/종료 알림에 적용) |
283
+ | `notifyOnStartup` | `true` | 시작 알림 발송 여부 |
284
+ | `notifyOnShutdown` | `true` | 종료 알림 발송 여부 |
285
+ | `applicationName` | - | embed에 표시될 서비스 이름 |
286
+ | `locale` | `'en'` | 알림 제목/필드 이름 언어: `'en'` \| `'ko'` \| `'ja'` \| `'zh'`, 또는 직접 만든 [`DicoshotMessages`](#예시-커스텀-언어) 객체 |
287
+ | `timeoutMs` | `5000` | HTTP 타임아웃 (ms) |
288
+ | `webhooks.error` | - | 예외 알림 전용 webhook URL (없으면 `webhookUrl` 사용) |
289
+ | `webhooks.slow` | - | 느린 응답 알림 전용 webhook URL (없으면 `webhookUrl` 사용) |
290
+ | `filter` | `false` | 예외 자동 알림 활성화 (`boolean` 또는 [`FilterOptions`](#filteroptions)) |
291
+ | `interceptor` | `false` | 느린 응답/에러 알림 활성화 (`boolean` 또는 [`InterceptorOptions`](#interceptoroptions)) |
292
+ | `retry` | `false` | webhook 전송 실패 시 재시도 활성화 (`boolean` 또는 [`RetryOptions`](#retryoptions)) |
293
+
294
+ ### 예시: 시작 시에만 알림
295
+
296
+ ```typescript
297
+ DicoshotModule.register({
298
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
299
+ notifyOnShutdown: false,
300
+ applicationName: 'order-service',
301
+ });
302
+ ```
303
+
304
+ ### 예시: 다른 언어로 알림 받기
305
+
306
+ ```typescript
307
+ DicoshotModule.register({
308
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
309
+ applicationName: '주문-서비스',
310
+ locale: 'ko', // 'en' | 'ko' | 'ja' | 'zh'
311
+ });
312
+ ```
313
+
314
+ 제목과 필드 이름(Service, Environment, Status, Location 등)이 번역됩니다. 런타임에서 가져오는 값들 — 예외 클래스 이름(`TypeError`, `NotFoundException` 등), 에러 메시지, 스택 트레이스, 요청 경로 — 은 그대로 전송되며 번역되지 않습니다.
315
+
316
+ ### 예시: 커스텀 언어
317
+
318
+ 팀 사용자들이 내장 언어를 안 쓴다면, locale 코드 대신 `DicoshotMessages` 객체를 통째로 넘기면 모든 제목/필드 이름이 그 객체 값으로 대체됩니다.
319
+
320
+ ```typescript
321
+ import { DicoshotMessages } from 'dicoshot-nest';
322
+
323
+ const fr: DicoshotMessages = {
324
+ startupTitle: '🟢 Application démarrée',
325
+ shutdownTitle: '🔴 Application arrêtée',
326
+ slowResponseLabel: 'Réponse lente',
327
+ field: {
328
+ service: 'Service',
329
+ environment: 'Environnement',
330
+ version: 'Version',
331
+ hostname: 'Hôte',
332
+ time: 'Heure',
333
+ status: 'Statut',
334
+ method: 'Méthode',
335
+ path: 'Chemin',
336
+ duration: 'Durée',
337
+ location: 'Emplacement',
338
+ stackTrace: "Pile d'appels",
339
+ requestBody: 'Corps de la requête',
340
+ },
341
+ };
342
+
343
+ DicoshotModule.register({
344
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
345
+ locale: fr,
346
+ });
347
+ ```
348
+
349
+ ### 예시: 에러/느린 응답을 다른 채널로 분리
350
+
351
+ ```typescript
352
+ DicoshotModule.register({
353
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL, // 시작/종료 알림용
354
+ webhooks: {
355
+ error: process.env.DISCORD_ERROR_WEBHOOK_URL, // 예외 알림용
356
+ slow: process.env.DISCORD_SLOW_WEBHOOK_URL, // 느린 응답 알림용
357
+ },
358
+ filter: { environment: 'production' },
359
+ interceptor: { slowThreshold: 2000 },
360
+ });
361
+ ```
362
+
363
+ ### 예시: 환경 변수로 webhook URL 주입
364
+
365
+ ```typescript
366
+ DicoshotModule.register({
367
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
368
+ });
369
+ ```
370
+
371
+ `webhookUrl`은 선택값입니다 — 비어있거나 설정하지 않으면 자동으로 비활성화되어 로컬 개발 환경에서 오류가 발생하지 않습니다.
372
+
373
+ ## 동작 방식
374
+
375
+ 1. `DicoshotModule.register()`/`registerAsync()`로 옵션을 NestJS DI 컨테이너에 등록합니다.
376
+ 2. `DicoshotNotifyInterceptor`는 항상 `APP_INTERCEPTOR`로 전역 등록되며, `@DicoshotNotify()`가 붙은 핸들러가 아니면 아무 동작도 하지 않습니다. `filter`/`interceptor` 옵션이 켜져 있으면 `DicoshotExceptionFilter`/`DicoshotInterceptor`도 각각 `APP_FILTER`/`APP_INTERCEPTOR`로 전역 등록됩니다.
377
+ 3. `DicoshotListener`가 NestJS 라이프사이클 훅을 구독해 `onApplicationBootstrap()`/`onApplicationShutdown()` 시점에 시작/종료 메시지를 발송합니다.
378
+ 4. 요청 처리 중 예외가 발생하면 `DicoshotExceptionFilter`(또는 `filter`가 꺼져 있을 때는 `DicoshotInterceptor`)가 에러 메시지를 발송합니다.
379
+ 5. 응답 시간이 `slowThreshold`를 초과하면 `DicoshotInterceptor`가 느린 응답 메시지를 발송합니다.
380
+ 6. `@DicoshotNotify()`가 붙은 핸들러가 성공적으로 끝나면 `DicoshotNotifyInterceptor`가 설정된 커스텀 메시지를 발송합니다.
381
+ 7. 모든 webhook 호출은 실패해도 예외를 삼키고 WARN 로그만 남기므로 앱 동작에 영향을 주지 않습니다.
382
+
383
+ ## MSA 환경
384
+
385
+ - 각 서비스가 `dicoshot-nest`를 독립적으로 사용합니다.
386
+ - 메시지에 `applicationName`과 `hostname`이 자동 포함되어 어느 서비스의 어느 인스턴스인지 식별 가능합니다.
387
+ - K8s 등에서 Pod 재시작이 잦은 경우 `notifyOnStartup: false`로 시작 알림만 끄거나, `enabled: false`로 전체 비활성화할 수 있습니다.
388
+ - 서비스별로 `filter.throttle`을 설정해 동일 에러가 반복 발생해도 Discord 채널이 도배되지 않도록 할 수 있습니다.
389
+
390
+ ## 요구사항
391
+
392
+ - Node.js 18 이상
393
+ - NestJS 10 이상
394
+
395
+ ## 빌드
396
+
397
+ ```bash
398
+ # 전체 빌드
399
+ npm run build --workspaces
400
+
401
+ # 개별 빌드
402
+ cd dicoshot-core && npm run build
403
+ cd dicoshot-nest && npm run build
404
+ ```
405
+
406
+ ## 범위 외 (현재 버전)
407
+
408
+ 다음 기능은 아직 포함되지 않습니다.
409
+
410
+ - 비동기 큐잉 (재시도 외 영구 실패 메시지 보관)
411
+ - 다중 webhook 동시 발송 또는 Slack 등 타 플랫폼
412
+
413
+ ## License
414
+
415
+ [MIT License](LICENSE) © 2026 DicoShot