@ahhaohho/response-dto 1.4.0 → 2.0.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 (2) hide show
  1. package/ResponseDTO.js +318 -64
  2. package/package.json +1 -1
package/ResponseDTO.js CHANGED
@@ -1,69 +1,323 @@
1
+ /**
2
+ * ResponseDTO 클래스 v2.0.0
3
+ * RFC 7807 (Problem Details for HTTP APIs) 표준을 점진적으로 도입하는 API 응답 클래스
4
+ *
5
+ * [하위 호환성 보장]
6
+ * - 기존 응답 구조 (type, status, data, message) 100% 유지
7
+ * - 에러 응답의 type은 기존대로 'client' 또는 'system' 유지
8
+ * - 새로운 RFC 7807 필드는 추가 필드로만 제공 (선택적 사용)
9
+ *
10
+ * [새로 추가된 필드] (에러 응답에만 해당)
11
+ * - title: 에러 제목 (예: 'Bad Request')
12
+ * - detail: 상세 설명 (message와 동일)
13
+ * - errorCode: 에러 코드 (예: 'BAD_REQUEST')
14
+ * - instance: 요청 경로 (선택적)
15
+ * - errors: 필드별 검증 오류 (선택적)
16
+ */
17
+
18
+ // HTTP 상태 코드별 표준 제목
19
+ const HTTP_STATUS_TITLES = {
20
+ 200: 'OK',
21
+ 201: 'Created',
22
+ 204: 'No Content',
23
+ 400: 'Bad Request',
24
+ 401: 'Unauthorized',
25
+ 403: 'Forbidden',
26
+ 404: 'Not Found',
27
+ 409: 'Conflict',
28
+ 422: 'Validation Error',
29
+ 429: 'Too Many Requests',
30
+ 500: 'Internal Server Error',
31
+ 503: 'Service Unavailable'
32
+ };
33
+
34
+ // 에러 코드 매핑
35
+ const ERROR_CODES = {
36
+ 400: 'BAD_REQUEST',
37
+ 401: 'UNAUTHORIZED',
38
+ 403: 'FORBIDDEN',
39
+ 404: 'NOT_FOUND',
40
+ 409: 'CONFLICT',
41
+ 422: 'VALIDATION_ERROR',
42
+ 429: 'TOO_MANY_REQUESTS',
43
+ 500: 'INTERNAL_SERVER_ERROR',
44
+ 503: 'SERVICE_UNAVAILABLE'
45
+ };
46
+
47
+ // 상태 코드에 따른 기존 type 값 (하위 호환)
48
+ const LEGACY_TYPE = {
49
+ client: [400, 401, 403, 404, 409, 422, 429],
50
+ system: [500, 503]
51
+ };
52
+
53
+ function getLegacyType(status) {
54
+ if (LEGACY_TYPE.client.includes(status)) return 'client';
55
+ if (LEGACY_TYPE.system.includes(status)) return 'system';
56
+ if (status >= 400 && status < 500) return 'client';
57
+ if (status >= 500) return 'system';
58
+ return 'success';
59
+ }
60
+
1
61
  class ResponseDTO {
2
- constructor(type, status, data = null, message = null) {
3
- this.type = type;
4
- this.status = status;
5
- this.data = data;
6
- this.message = message;
7
- }
8
-
9
- static success(data = null, message = null, type = 'success', status = 200) {
10
- return new ResponseDTO(type, status, data, message);
11
- }
12
-
13
- static created(data = null, message = null, type = 'success', status = 201) {
14
- return new ResponseDTO(type, status, data, message);
15
- }
16
-
17
- static noContent(message = null, type = 'success', status = 204) {
18
- return new ResponseDTO(type, status, null, message);
19
- }
20
-
21
- static badRequest(message, type = 'client', status = 400) {
22
- return new ResponseDTO(type, status, null, message);
23
- }
24
-
25
- static unauthorized(message, type = 'client', status = 401) {
26
- return new ResponseDTO(type, status, null, message);
62
+ /**
63
+ * ResponseDTO 생성자
64
+ * @param {string|Object} typeOrOptions - 응답 유형 또는 옵션 객체
65
+ * @param {number} [status] - HTTP 상태 코드 (레거시 형태)
66
+ * @param {any} [data] - 응답 데이터 (레거시 형태)
67
+ * @param {string} [message] - 응답 메시지 (레거시 형태)
68
+ */
69
+ constructor(typeOrOptions, status, data, message) {
70
+ if (typeof typeOrOptions === 'string') {
71
+ // 레거시 생성자: new ResponseDTO('success', 200, data, message)
72
+ this.type = typeOrOptions;
73
+ this.status = status;
74
+ this.data = data ?? null;
75
+ this.message = message ?? null;
76
+ } else {
77
+ // 생성자: new ResponseDTO({ type, status, data, message, ... })
78
+ const options = typeOrOptions;
79
+ this.type = options.type;
80
+ this.status = options.status;
81
+ this.data = options.data ?? null;
82
+ this.message = options.message ?? null;
83
+ this.title = options.title ?? null;
84
+ this.detail = options.detail ?? null;
85
+ this.instance = options.instance ?? null;
86
+ this.errorCode = options.errorCode ?? null;
87
+ this.errors = options.errors ?? null;
27
88
  }
28
-
29
- static forbidden(message, type = 'client', status = 403) {
30
- return new ResponseDTO(type, status, null, message);
31
- }
32
-
33
- static notFound(message, type = 'client', status = 404) {
34
- return new ResponseDTO(type, status, null, message);
35
- }
36
-
37
- static conflict(message, type = 'client', status = 409) {
38
- return new ResponseDTO(type, status, null, message);
39
- }
40
-
41
- static validationError(message, type = 'client', status = 422) {
42
- return new ResponseDTO(type, status, null, message);
43
- }
44
-
45
- static systemError(message, type = 'system', status = 500) {
46
- return new ResponseDTO(type, status, null, message);
47
- }
48
-
49
- static serviceUnavailable(message, type = 'system', status = 503) {
50
- return new ResponseDTO(type, status, null, message);
89
+ }
90
+
91
+ // ==================== 성공 응답 메서드 ====================
92
+
93
+ /**
94
+ * 상태 코드 200의 성공 응답
95
+ * @param {any} [data] - 응답 데이터
96
+ * @param {string} [message] - 성공 메시지
97
+ * @returns {ResponseDTO}
98
+ */
99
+ static success(data = null, message = null) {
100
+ return new ResponseDTO('success', 200, data, message);
101
+ }
102
+
103
+ /**
104
+ * 리소스 생성 성공 응답 (201)
105
+ * @param {any} [data] - 생성된 리소스 데이터
106
+ * @param {string} [message] - 성공 메시지
107
+ * @returns {ResponseDTO}
108
+ */
109
+ static created(data = null, message = null) {
110
+ return new ResponseDTO('success', 201, data, message);
111
+ }
112
+
113
+ /**
114
+ * 콘텐츠 없는 성공 응답 (204)
115
+ * @param {string} [message] - 성공 메시지
116
+ * @returns {ResponseDTO}
117
+ */
118
+ static noContent(message = null) {
119
+ return new ResponseDTO('success', 204, null, message);
120
+ }
121
+
122
+ // ==================== 에러 응답 메서드 ====================
123
+
124
+ /**
125
+ * 에러 응답 생성 헬퍼 (내부용)
126
+ * @private
127
+ */
128
+ static _createError(status, message, options = {}) {
129
+ const legacyType = getLegacyType(status);
130
+ const errorCode = options.errorCode || ERROR_CODES[status] || 'ERROR';
131
+
132
+ return new ResponseDTO({
133
+ type: legacyType, // 하위 호환: 'client' 또는 'system' 유지
134
+ status,
135
+ data: null,
136
+ message,
137
+ // RFC 7807 추가 필드
138
+ title: HTTP_STATUS_TITLES[status] || 'Error',
139
+ detail: message,
140
+ errorCode,
141
+ instance: options.instance || null,
142
+ errors: options.errors || null
143
+ });
144
+ }
145
+
146
+ /**
147
+ * 잘못된 요청 (400)
148
+ * @param {string} message - 에러 메시지
149
+ * @param {Object} [options] - 추가 옵션
150
+ * @param {string} [options.instance] - 요청 경로
151
+ * @param {Array} [options.errors] - 필드별 오류
152
+ * @param {string} [options.errorCode] - 커스텀 에러 코드
153
+ * @returns {ResponseDTO}
154
+ */
155
+ static badRequest(message, options = {}) {
156
+ return ResponseDTO._createError(400, message, options);
157
+ }
158
+
159
+ /**
160
+ * 인증 필요 (401)
161
+ * @param {string} message - 에러 메시지
162
+ * @param {Object} [options] - 추가 옵션
163
+ * @returns {ResponseDTO}
164
+ */
165
+ static unauthorized(message, options = {}) {
166
+ return ResponseDTO._createError(401, message, options);
167
+ }
168
+
169
+ /**
170
+ * 권한 부족 (403)
171
+ * @param {string} message - 에러 메시지
172
+ * @param {Object} [options] - 추가 옵션
173
+ * @returns {ResponseDTO}
174
+ */
175
+ static forbidden(message, options = {}) {
176
+ return ResponseDTO._createError(403, message, options);
177
+ }
178
+
179
+ /**
180
+ * 리소스 없음 (404)
181
+ * @param {string} message - 에러 메시지
182
+ * @param {Object} [options] - 추가 옵션
183
+ * @returns {ResponseDTO}
184
+ */
185
+ static notFound(message, options = {}) {
186
+ return ResponseDTO._createError(404, message, options);
187
+ }
188
+
189
+ /**
190
+ * 충돌 (409)
191
+ * @param {string} message - 에러 메시지
192
+ * @param {Object} [options] - 추가 옵션
193
+ * @returns {ResponseDTO}
194
+ */
195
+ static conflict(message, options = {}) {
196
+ return ResponseDTO._createError(409, message, options);
197
+ }
198
+
199
+ /**
200
+ * 유효성 검사 오류 (422)
201
+ * @param {string} message - 에러 메시지
202
+ * @param {Object} [options] - 추가 옵션
203
+ * @param {Array<{field: string, message: string, rejected?: any}>} [options.errors] - 필드별 오류
204
+ * @returns {ResponseDTO}
205
+ */
206
+ static validationError(message, options = {}) {
207
+ return ResponseDTO._createError(422, message, options);
208
+ }
209
+
210
+ /**
211
+ * 요청 제한 초과 (429)
212
+ * @param {string} message - 에러 메시지
213
+ * @param {Object} [options] - 추가 옵션
214
+ * @returns {ResponseDTO}
215
+ */
216
+ static tooManyRequests(message, options = {}) {
217
+ return ResponseDTO._createError(429, message, options);
218
+ }
219
+
220
+ /**
221
+ * 서버 내부 오류 (500)
222
+ * @param {string} message - 에러 메시지
223
+ * @param {Object} [options] - 추가 옵션
224
+ * @returns {ResponseDTO}
225
+ */
226
+ static systemError(message, options = {}) {
227
+ return ResponseDTO._createError(500, message, options);
228
+ }
229
+
230
+ /**
231
+ * 서비스 불가 (503)
232
+ * @param {string} message - 에러 메시지
233
+ * @param {Object} [options] - 추가 옵션
234
+ * @returns {ResponseDTO}
235
+ */
236
+ static serviceUnavailable(message, options = {}) {
237
+ return ResponseDTO._createError(503, message, options);
238
+ }
239
+
240
+ // ==================== 유틸리티 메서드 ====================
241
+
242
+ /**
243
+ * 성공 응답인지 확인
244
+ * @returns {boolean}
245
+ */
246
+ isSuccess() {
247
+ return this.type === 'success' || this.status < 400;
248
+ }
249
+
250
+ /**
251
+ * 에러 응답인지 확인
252
+ * @returns {boolean}
253
+ */
254
+ isError() {
255
+ return !this.isSuccess();
256
+ }
257
+
258
+ /**
259
+ * 클라이언트 에러인지 확인 (4xx)
260
+ * @returns {boolean}
261
+ */
262
+ isClientError() {
263
+ return this.status >= 400 && this.status < 500;
264
+ }
265
+
266
+ /**
267
+ * 서버 에러인지 확인 (5xx)
268
+ * @returns {boolean}
269
+ */
270
+ isServerError() {
271
+ return this.status >= 500;
272
+ }
273
+
274
+ /**
275
+ * JSON 직렬화
276
+ *
277
+ * [하위 호환 보장]
278
+ * 기존 응답 구조를 100% 유지하고, 새 필드는 추가로만 포함됩니다.
279
+ *
280
+ * 성공 응답: { type: 'success', status, data, message }
281
+ * 에러 응답: { type: 'client'|'system', status, data: null, message, title?, detail?, errorCode?, instance?, errors? }
282
+ *
283
+ * @returns {Object}
284
+ */
285
+ toJSON() {
286
+ // 기본 응답 구조 (하위 호환 100%)
287
+ const response = {
288
+ type: this.type,
289
+ status: this.status,
290
+ data: this.data,
291
+ message: this.message
292
+ };
293
+
294
+ // 리다이렉트 처리 (기존 로직 유지)
295
+ if (this.type === 'redirect' && this.data?.redirect) {
296
+ response.redirect = this.data.redirect;
297
+ delete response.data;
51
298
  }
52
-
53
- toJSON() {
54
- const response = {
55
- type: this.type,
56
- status: this.status,
57
- data: this.data,
58
- message: this.message,
59
- };
60
-
61
- // 리다이렉트의 경우 redirect 객체를 최상위로 이동
62
- if (this.type === 'redirect' && this.data?.redirect) {
63
- response.redirect = this.data.redirect;
64
- delete response.data;
65
- }
66
-
67
- return response;
299
+
300
+ // 에러 응답인 경우 RFC 7807 필드 추가 (있는 경우에만)
301
+ if (this.isError()) {
302
+ if (this.title) {
303
+ response.title = this.title;
304
+ }
305
+ if (this.detail) {
306
+ response.detail = this.detail;
307
+ }
308
+ if (this.errorCode) {
309
+ response.errorCode = this.errorCode;
310
+ }
311
+ if (this.instance) {
312
+ response.instance = this.instance;
313
+ }
314
+ if (this.errors && this.errors.length > 0) {
315
+ response.errors = this.errors;
316
+ }
68
317
  }
318
+
319
+ return response;
320
+ }
69
321
  }
322
+
323
+ module.exports = ResponseDTO;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahhaohho/response-dto",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "AhhaOhho API 응답을 위한 DTO 클래스",
5
5
  "main": "index.js",
6
6
  "scripts": {