daiso 1.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +741 -0
  3. package/dist/api/cgvHandlers.js +97 -0
  4. package/dist/api/handlers.js +208 -0
  5. package/dist/api/megaboxHandlers.js +145 -0
  6. package/dist/api/response.js +22 -0
  7. package/dist/api/routes/cgvRoutes.js +22 -0
  8. package/dist/api/routes/daisoRoutes.js +27 -0
  9. package/dist/api/routes/megaboxRoutes.js +22 -0
  10. package/dist/api/routes/oliveyoungRoutes.js +17 -0
  11. package/dist/cli.js +140 -0
  12. package/dist/core/interfaces.js +6 -0
  13. package/dist/core/registry.js +121 -0
  14. package/dist/core/types.js +6 -0
  15. package/dist/index.js +123 -0
  16. package/dist/pages/openapi.js +6 -0
  17. package/dist/pages/openapiResponse.js +32 -0
  18. package/dist/pages/openapiSpec.js +43 -0
  19. package/dist/pages/openapiSpecComponents.js +419 -0
  20. package/dist/pages/openapiSpecComponentsCgv.js +123 -0
  21. package/dist/pages/openapiSpecPathsCgv.js +149 -0
  22. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +381 -0
  23. package/dist/pages/openapiSpecPathsMegabox.js +184 -0
  24. package/dist/pages/openapiYaml.js +58 -0
  25. package/dist/pages/privacy.js +262 -0
  26. package/dist/pages/prompt.js +425 -0
  27. package/dist/services/cgv/api.js +15 -0
  28. package/dist/services/cgv/client.js +146 -0
  29. package/dist/services/cgv/index.js +30 -0
  30. package/dist/services/cgv/timetable.js +16 -0
  31. package/dist/services/cgv/tools/findTheaters.js +43 -0
  32. package/dist/services/cgv/tools/getTimetable.js +47 -0
  33. package/dist/services/cgv/tools/searchMovies.js +40 -0
  34. package/dist/services/cgv/transport.js +91 -0
  35. package/dist/services/cgv/types.js +4 -0
  36. package/dist/services/daiso/api.js +53 -0
  37. package/dist/services/daiso/client.js +35 -0
  38. package/dist/services/daiso/index.js +42 -0
  39. package/dist/services/daiso/tools/checkInventory.js +122 -0
  40. package/dist/services/daiso/tools/findStores.js +145 -0
  41. package/dist/services/daiso/tools/getPriceInfo.js +90 -0
  42. package/dist/services/daiso/tools/searchProducts.js +74 -0
  43. package/dist/services/daiso/types.js +4 -0
  44. package/dist/services/megabox/api.js +8 -0
  45. package/dist/services/megabox/client.js +140 -0
  46. package/dist/services/megabox/index.js +26 -0
  47. package/dist/services/megabox/tools/findNearbyTheaters.js +74 -0
  48. package/dist/services/megabox/tools/getRemainingSeats.js +57 -0
  49. package/dist/services/megabox/tools/listNowShowing.js +51 -0
  50. package/dist/services/megabox/types.js +4 -0
  51. package/dist/services/oliveyoung/api.js +8 -0
  52. package/dist/services/oliveyoung/client.js +90 -0
  53. package/dist/services/oliveyoung/index.js +28 -0
  54. package/dist/services/oliveyoung/tools/checkInventory.js +87 -0
  55. package/dist/services/oliveyoung/tools/findNearbyStores.js +53 -0
  56. package/dist/services/oliveyoung/types.js +4 -0
  57. package/dist/tools/checkInventory.js +98 -0
  58. package/dist/tools/findStores.js +125 -0
  59. package/dist/tools/getPriceInfo.js +77 -0
  60. package/dist/tools/searchProducts.js +61 -0
  61. package/dist/types/index.js +4 -0
  62. package/dist/utils/cache.js +54 -0
  63. package/dist/utils/fetch.js +14 -0
  64. package/dist/utils/format.js +31 -0
  65. package/dist/utils/http.js +35 -0
  66. package/dist/utils/zyte.js +76 -0
  67. package/openapi.json +1938 -0
  68. package/openapi.yaml +1338 -0
  69. package/package.json +79 -0
@@ -0,0 +1,381 @@
1
+ /**
2
+ * OpenAPI 경로 정의 (다이소/올리브영)
3
+ */
4
+ export const OPENAPI_PATHS_DAISO_OLIVEYOUNG = {
5
+ '/api/daiso/products': {
6
+ get: {
7
+ operationId: 'searchProducts',
8
+ summary: '제품 검색',
9
+ description: '키워드로 다이소 제품을 검색합니다. 수납박스, 펜, 정리함 등 다양한 키워드로 검색할 수 있습니다.',
10
+ parameters: [
11
+ {
12
+ name: 'q',
13
+ in: 'query',
14
+ required: true,
15
+ description: '검색 키워드 (예: 수납박스, 펜, 정리함)',
16
+ schema: { type: 'string' },
17
+ example: '수납박스',
18
+ },
19
+ {
20
+ name: 'page',
21
+ in: 'query',
22
+ required: false,
23
+ description: '페이지 번호',
24
+ schema: { type: 'integer', default: 1, minimum: 1 },
25
+ },
26
+ {
27
+ name: 'pageSize',
28
+ in: 'query',
29
+ required: false,
30
+ description: '페이지당 결과 수',
31
+ schema: { type: 'integer', default: 30, minimum: 1, maximum: 100 },
32
+ },
33
+ ],
34
+ responses: {
35
+ '200': {
36
+ description: '검색 성공',
37
+ content: {
38
+ 'application/json': {
39
+ schema: { $ref: '#/components/schemas/ProductSearchResponse' },
40
+ },
41
+ },
42
+ },
43
+ '400': {
44
+ description: '잘못된 요청 (검색어 누락)',
45
+ content: {
46
+ 'application/json': {
47
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
48
+ },
49
+ },
50
+ },
51
+ },
52
+ },
53
+ },
54
+ '/api/daiso/products/{productId}': {
55
+ get: {
56
+ operationId: 'getProduct',
57
+ summary: '제품 상세 정보',
58
+ description: '제품 ID로 상세 정보를 조회합니다.',
59
+ parameters: [
60
+ {
61
+ name: 'productId',
62
+ in: 'path',
63
+ required: true,
64
+ description: '제품 ID (제품 검색 결과의 id 값)',
65
+ schema: { type: 'string' },
66
+ example: '1234567890',
67
+ },
68
+ ],
69
+ responses: {
70
+ '200': {
71
+ description: '조회 성공',
72
+ content: {
73
+ 'application/json': {
74
+ schema: { $ref: '#/components/schemas/ProductDetailResponse' },
75
+ },
76
+ },
77
+ },
78
+ '404': {
79
+ description: '제품을 찾을 수 없음',
80
+ content: {
81
+ 'application/json': {
82
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ '/api/daiso/stores': {
90
+ get: {
91
+ operationId: 'findStores',
92
+ summary: '매장 찾기',
93
+ description: '키워드 또는 지역으로 다이소 매장을 검색합니다. keyword 또는 sido 중 하나는 필수입니다.',
94
+ parameters: [
95
+ {
96
+ name: 'keyword',
97
+ in: 'query',
98
+ required: false,
99
+ description: '매장명 또는 주소 키워드 (예: 강남, 홍대, 안산)',
100
+ schema: { type: 'string' },
101
+ example: '강남',
102
+ },
103
+ {
104
+ name: 'sido',
105
+ in: 'query',
106
+ required: false,
107
+ description: '시/도 (예: 서울, 경기, 부산)',
108
+ schema: { type: 'string' },
109
+ example: '서울',
110
+ },
111
+ {
112
+ name: 'gugun',
113
+ in: 'query',
114
+ required: false,
115
+ description: '구/군 (예: 강남구, 마포구)',
116
+ schema: { type: 'string' },
117
+ example: '강남구',
118
+ },
119
+ {
120
+ name: 'dong',
121
+ in: 'query',
122
+ required: false,
123
+ description: '동 (예: 역삼동, 합정동)',
124
+ schema: { type: 'string' },
125
+ },
126
+ {
127
+ name: 'limit',
128
+ in: 'query',
129
+ required: false,
130
+ description: '최대 결과 수',
131
+ schema: { type: 'integer', default: 50, minimum: 1, maximum: 100 },
132
+ },
133
+ ],
134
+ responses: {
135
+ '200': {
136
+ description: '검색 성공',
137
+ content: {
138
+ 'application/json': {
139
+ schema: { $ref: '#/components/schemas/StoreSearchResponse' },
140
+ },
141
+ },
142
+ },
143
+ '400': {
144
+ description: '잘못된 요청 (필수 파라미터 누락)',
145
+ content: {
146
+ 'application/json': {
147
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
148
+ },
149
+ },
150
+ },
151
+ },
152
+ },
153
+ },
154
+ '/api/daiso/inventory': {
155
+ get: {
156
+ operationId: 'checkInventory',
157
+ summary: '재고 확인',
158
+ description: '특정 제품의 온라인 재고와 매장별 재고를 확인합니다. 위치 정보를 제공하면 가까운 매장 순으로 정렬됩니다.',
159
+ parameters: [
160
+ {
161
+ name: 'productId',
162
+ in: 'query',
163
+ required: true,
164
+ description: '제품 ID (제품 검색 결과의 id 값)',
165
+ schema: { type: 'string' },
166
+ example: '1234567890',
167
+ },
168
+ {
169
+ name: 'lat',
170
+ in: 'query',
171
+ required: false,
172
+ description: '위도 (기본값: 서울 시청 37.5665)',
173
+ schema: { type: 'number', format: 'float', default: 37.5665 },
174
+ example: 37.4979,
175
+ },
176
+ {
177
+ name: 'lng',
178
+ in: 'query',
179
+ required: false,
180
+ description: '경도 (기본값: 서울 시청 126.978)',
181
+ schema: { type: 'number', format: 'float', default: 126.978 },
182
+ example: 127.0276,
183
+ },
184
+ {
185
+ name: 'keyword',
186
+ in: 'query',
187
+ required: false,
188
+ description: '매장 검색어 (예: 안산, 강남)',
189
+ schema: { type: 'string' },
190
+ },
191
+ {
192
+ name: 'page',
193
+ in: 'query',
194
+ required: false,
195
+ description: '페이지 번호',
196
+ schema: { type: 'integer', default: 1, minimum: 1 },
197
+ },
198
+ {
199
+ name: 'pageSize',
200
+ in: 'query',
201
+ required: false,
202
+ description: '페이지당 결과 수',
203
+ schema: { type: 'integer', default: 30, minimum: 1, maximum: 100 },
204
+ },
205
+ ],
206
+ responses: {
207
+ '200': {
208
+ description: '조회 성공',
209
+ content: {
210
+ 'application/json': {
211
+ schema: { $ref: '#/components/schemas/InventoryResponse' },
212
+ },
213
+ },
214
+ },
215
+ '400': {
216
+ description: '잘못된 요청 (제품 ID 누락)',
217
+ content: {
218
+ 'application/json': {
219
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ '/api/oliveyoung/stores': {
227
+ get: {
228
+ operationId: 'oliveyoungFindStores',
229
+ summary: '올리브영 매장 검색',
230
+ description: '위치(위도/경도)와 키워드로 올리브영 매장을 검색합니다.',
231
+ parameters: [
232
+ {
233
+ name: 'keyword',
234
+ in: 'query',
235
+ required: false,
236
+ description: '매장명 또는 지역 키워드 (예: 명동, 강남)',
237
+ schema: { type: 'string' },
238
+ example: '명동',
239
+ },
240
+ {
241
+ name: 'lat',
242
+ in: 'query',
243
+ required: false,
244
+ description: '위도 (기본값: 서울 시청 37.5665)',
245
+ schema: { type: 'number', format: 'float', default: 37.5665 },
246
+ },
247
+ {
248
+ name: 'lng',
249
+ in: 'query',
250
+ required: false,
251
+ description: '경도 (기본값: 서울 시청 126.978)',
252
+ schema: { type: 'number', format: 'float', default: 126.978 },
253
+ },
254
+ {
255
+ name: 'pageIdx',
256
+ in: 'query',
257
+ required: false,
258
+ description: '매장 검색 페이지 번호',
259
+ schema: { type: 'integer', default: 1, minimum: 1 },
260
+ },
261
+ {
262
+ name: 'limit',
263
+ in: 'query',
264
+ required: false,
265
+ description: '최대 결과 수',
266
+ schema: { type: 'integer', default: 20, minimum: 1, maximum: 100 },
267
+ },
268
+ ],
269
+ responses: {
270
+ '200': {
271
+ description: '검색 성공',
272
+ content: {
273
+ 'application/json': {
274
+ schema: { $ref: '#/components/schemas/OliveyoungStoreSearchResponse' },
275
+ },
276
+ },
277
+ },
278
+ '500': {
279
+ description: '올리브영 API 호출 실패',
280
+ content: {
281
+ 'application/json': {
282
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
283
+ },
284
+ },
285
+ },
286
+ },
287
+ },
288
+ },
289
+ '/api/oliveyoung/inventory': {
290
+ get: {
291
+ operationId: 'oliveyoungCheckInventory',
292
+ summary: '올리브영 재고 확인',
293
+ description: '상품 키워드로 올리브영 재고를 검색하고 주변 매장 정보를 함께 조회합니다.',
294
+ parameters: [
295
+ {
296
+ name: 'keyword',
297
+ in: 'query',
298
+ required: true,
299
+ description: '재고를 확인할 상품 키워드',
300
+ schema: { type: 'string' },
301
+ example: '선크림',
302
+ },
303
+ {
304
+ name: 'lat',
305
+ in: 'query',
306
+ required: false,
307
+ description: '위도 (기본값: 서울 시청 37.5665)',
308
+ schema: { type: 'number', format: 'float', default: 37.5665 },
309
+ },
310
+ {
311
+ name: 'lng',
312
+ in: 'query',
313
+ required: false,
314
+ description: '경도 (기본값: 서울 시청 126.978)',
315
+ schema: { type: 'number', format: 'float', default: 126.978 },
316
+ },
317
+ {
318
+ name: 'storeKeyword',
319
+ in: 'query',
320
+ required: false,
321
+ description: '주변 매장 필터 키워드',
322
+ schema: { type: 'string' },
323
+ },
324
+ {
325
+ name: 'page',
326
+ in: 'query',
327
+ required: false,
328
+ description: '상품 검색 페이지 번호',
329
+ schema: { type: 'integer', default: 1, minimum: 1 },
330
+ },
331
+ {
332
+ name: 'size',
333
+ in: 'query',
334
+ required: false,
335
+ description: '페이지당 결과 수',
336
+ schema: { type: 'integer', default: 20, minimum: 1, maximum: 100 },
337
+ },
338
+ {
339
+ name: 'sort',
340
+ in: 'query',
341
+ required: false,
342
+ description: '정렬 코드',
343
+ schema: { type: 'string', default: '01' },
344
+ },
345
+ {
346
+ name: 'includeSoldOut',
347
+ in: 'query',
348
+ required: false,
349
+ description: '품절 포함 여부 (true/false)',
350
+ schema: { type: 'boolean', default: false },
351
+ },
352
+ ],
353
+ responses: {
354
+ '200': {
355
+ description: '조회 성공',
356
+ content: {
357
+ 'application/json': {
358
+ schema: { $ref: '#/components/schemas/OliveyoungInventoryResponse' },
359
+ },
360
+ },
361
+ },
362
+ '400': {
363
+ description: '잘못된 요청 (검색어 누락)',
364
+ content: {
365
+ 'application/json': {
366
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
367
+ },
368
+ },
369
+ },
370
+ '500': {
371
+ description: '올리브영 API 호출 실패',
372
+ content: {
373
+ 'application/json': {
374
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
375
+ },
376
+ },
377
+ },
378
+ },
379
+ },
380
+ },
381
+ };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * OpenAPI 경로 정의 (메가박스)
3
+ */
4
+ export const OPENAPI_PATHS_MEGABOX = {
5
+ '/api/megabox/theaters': {
6
+ get: {
7
+ operationId: 'megaboxFindNearbyTheaters',
8
+ summary: '메가박스 주변 지점 조회',
9
+ description: '좌표 기준으로 메가박스 지점을 거리순으로 조회합니다.',
10
+ parameters: [
11
+ {
12
+ name: 'lat',
13
+ in: 'query',
14
+ required: false,
15
+ description: '위도 (기본값: 서울 시청 37.5665)',
16
+ schema: { type: 'number', format: 'float', default: 37.5665 },
17
+ },
18
+ {
19
+ name: 'lng',
20
+ in: 'query',
21
+ required: false,
22
+ description: '경도 (기본값: 서울 시청 126.978)',
23
+ schema: { type: 'number', format: 'float', default: 126.978 },
24
+ },
25
+ {
26
+ name: 'playDate',
27
+ in: 'query',
28
+ required: false,
29
+ description: '조회 날짜 (YYYYMMDD)',
30
+ schema: { type: 'string', example: '20260304' },
31
+ },
32
+ {
33
+ name: 'areaCode',
34
+ in: 'query',
35
+ required: false,
36
+ description: '지역 코드 (기본값: 11, 서울)',
37
+ schema: { type: 'string', default: '11' },
38
+ },
39
+ {
40
+ name: 'limit',
41
+ in: 'query',
42
+ required: false,
43
+ description: '최대 결과 수',
44
+ schema: { type: 'integer', default: 10, minimum: 1, maximum: 100 },
45
+ },
46
+ ],
47
+ responses: {
48
+ '200': {
49
+ description: '조회 성공',
50
+ content: {
51
+ 'application/json': {
52
+ schema: { $ref: '#/components/schemas/MegaboxTheaterSearchResponse' },
53
+ },
54
+ },
55
+ },
56
+ '500': {
57
+ description: '메가박스 API 호출 실패',
58
+ content: {
59
+ 'application/json': {
60
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ },
66
+ },
67
+ '/api/megabox/movies': {
68
+ get: {
69
+ operationId: 'megaboxListNowShowing',
70
+ summary: '메가박스 영화/회차 목록 조회',
71
+ description: '날짜, 지점, 영화 조건으로 메가박스 영화 및 상영 회차를 조회합니다.',
72
+ parameters: [
73
+ {
74
+ name: 'playDate',
75
+ in: 'query',
76
+ required: false,
77
+ description: '조회 날짜 (YYYYMMDD)',
78
+ schema: { type: 'string', example: '20260304' },
79
+ },
80
+ {
81
+ name: 'theaterId',
82
+ in: 'query',
83
+ required: false,
84
+ description: '지점 ID (예: 1372)',
85
+ schema: { type: 'string', example: '1372' },
86
+ },
87
+ {
88
+ name: 'movieId',
89
+ in: 'query',
90
+ required: false,
91
+ description: '영화 ID (예: 25104500)',
92
+ schema: { type: 'string', example: '25104500' },
93
+ },
94
+ {
95
+ name: 'areaCode',
96
+ in: 'query',
97
+ required: false,
98
+ description: '지역 코드 (기본값: 11, 서울)',
99
+ schema: { type: 'string', default: '11' },
100
+ },
101
+ ],
102
+ responses: {
103
+ '200': {
104
+ description: '조회 성공',
105
+ content: {
106
+ 'application/json': {
107
+ schema: { $ref: '#/components/schemas/MegaboxMovieListResponse' },
108
+ },
109
+ },
110
+ },
111
+ '500': {
112
+ description: '메가박스 API 호출 실패',
113
+ content: {
114
+ 'application/json': {
115
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
116
+ },
117
+ },
118
+ },
119
+ },
120
+ },
121
+ },
122
+ '/api/megabox/seats': {
123
+ get: {
124
+ operationId: 'megaboxGetRemainingSeats',
125
+ summary: '메가박스 잔여 좌석 조회',
126
+ description: '영화/지점/날짜 조건으로 회차별 잔여 좌석 수를 조회합니다.',
127
+ parameters: [
128
+ {
129
+ name: 'playDate',
130
+ in: 'query',
131
+ required: false,
132
+ description: '조회 날짜 (YYYYMMDD)',
133
+ schema: { type: 'string', example: '20260304' },
134
+ },
135
+ {
136
+ name: 'theaterId',
137
+ in: 'query',
138
+ required: false,
139
+ description: '지점 ID',
140
+ schema: { type: 'string' },
141
+ },
142
+ {
143
+ name: 'movieId',
144
+ in: 'query',
145
+ required: false,
146
+ description: '영화 ID',
147
+ schema: { type: 'string' },
148
+ },
149
+ {
150
+ name: 'areaCode',
151
+ in: 'query',
152
+ required: false,
153
+ description: '지역 코드 (기본값: 11, 서울)',
154
+ schema: { type: 'string', default: '11' },
155
+ },
156
+ {
157
+ name: 'limit',
158
+ in: 'query',
159
+ required: false,
160
+ description: '최대 결과 수',
161
+ schema: { type: 'integer', default: 50, minimum: 1, maximum: 200 },
162
+ },
163
+ ],
164
+ responses: {
165
+ '200': {
166
+ description: '조회 성공',
167
+ content: {
168
+ 'application/json': {
169
+ schema: { $ref: '#/components/schemas/MegaboxSeatListResponse' },
170
+ },
171
+ },
172
+ },
173
+ '500': {
174
+ description: '메가박스 API 호출 실패',
175
+ content: {
176
+ 'application/json': {
177
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
178
+ },
179
+ },
180
+ },
181
+ },
182
+ },
183
+ },
184
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * OpenAPI YAML 변환 유틸리티
3
+ */
4
+ /**
5
+ * JSON을 YAML로 변환 (간단한 구현)
6
+ */
7
+ export function jsonToYaml(obj, indent = 0) {
8
+ const spaces = ' '.repeat(indent);
9
+ if (obj === null || obj === undefined) {
10
+ return 'null';
11
+ }
12
+ if (typeof obj === 'boolean' || typeof obj === 'number') {
13
+ return String(obj);
14
+ }
15
+ if (typeof obj === 'string') {
16
+ // 여러 줄 문자열 처리
17
+ if (obj.includes('\n')) {
18
+ const lines = obj.split('\n');
19
+ return '|\n' + lines.map((line) => spaces + ' ' + line).join('\n');
20
+ }
21
+ // 특수 문자가 포함된 경우 따옴표로 감싸기
22
+ if (obj.includes(':') ||
23
+ obj.includes('#') ||
24
+ obj.includes("'") ||
25
+ obj.includes('"') ||
26
+ obj.includes('\\')) {
27
+ return JSON.stringify(obj);
28
+ }
29
+ return obj;
30
+ }
31
+ if (Array.isArray(obj)) {
32
+ if (obj.length === 0) {
33
+ return '[]';
34
+ }
35
+ return obj.map((item) => `\n${spaces}- ${jsonToYaml(item, indent + 1).trimStart()}`).join('');
36
+ }
37
+ if (typeof obj === 'object') {
38
+ const entries = Object.entries(obj);
39
+ if (entries.length === 0) {
40
+ return '{}';
41
+ }
42
+ return entries
43
+ .map(([key, value]) => {
44
+ const yamlValue = jsonToYaml(value, indent + 1);
45
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
46
+ return `\n${spaces}${key}:${yamlValue}`;
47
+ }
48
+ if (Array.isArray(value)) {
49
+ return `\n${spaces}${key}:${yamlValue}`;
50
+ }
51
+ return `\n${spaces}${key}: ${yamlValue}`;
52
+ })
53
+ .join('');
54
+ }
55
+ return String(obj);
56
+ }
57
+ // 테스트에서 YAML 변환 분기 검증을 위해 노출
58
+ export const __testOnlyJsonToYaml = jsonToYaml;