refactor: 콘텐츠·퀴즈 등록을 form에서 JSON으로 통일
등록만 form이고 수정은 JSON이었다. 백엔드가 `insert`에 `@RequestBody`를 붙여 JSON으로 통일하기로 해(사용자 확정 사항) 보내는 쪽도 맞춘다. 중첩 목록을 인덱스 표기로 펴던 `toFormParams`가 양쪽에서 사라지고, 이제 `keywordList`·`schulGrdList`가 JSON 배열로 그대로 간다. form 갈래가 없어져 `sendWrite`의 `form` 인자와 로그의 form 출력도 걷어냈다. ⚠️ **백엔드가 `@RequestBody`를 붙이기 전까지는 등록이 실패한다.** `@RequestBody`가 없으면 Spring이 model attribute로 바인딩해 JSON 본문을 읽지 않는다(필드가 전부 null이 되거나 415). 대상은 두 곳: MngrCntntsApiController.insert(MngrCntntsRequestVo) MngrCntntsQuizApiController.insert(MngrCntntsQuizRequestVo) Swagger가 이미 JSON 스키마로 문서화하고 있어(springdoc이 어노테이션 없는 POST 복합 파라미터를 body로 추정) 어노테이션을 붙이면 문서와 실제가 비로소 일치한다. 확인(임시 프로브로 실제 요청 캡처): 퀴즈·만화 등록 모두 `Content-Type: application/json`, 본문이 스웨거 스키마와 같은 필드 구성. 학년 없는 학교는 `grdCd: null`로 나간다. Co-Authored-By: Claude Opus 5
@0d41d2a47c9867250a0d2c0be86454755a1a2320
--- lib/data/repositories/content-quiz-repository.ts
+++ lib/data/repositories/content-quiz-repository.ts
... | ... | @@ -7,11 +7,11 @@ |
| 7 | 7 |
|
| 8 | 8 |
/** |
| 9 | 9 |
* 금융OX퀴즈 Repository — 쇼츠·만화와 테이블(`TB_COM_QUIZ`)은 다르지만 콘텐츠관리 API 아래로 |
| 10 |
- * 들어와 규약은 같다(등록은 form, 수정은 JSON, 삭제는 soft delete). |
|
| 10 |
+ * 들어와 규약은 같다(등록·수정 모두 JSON, 삭제는 soft delete). |
|
| 11 | 11 |
* |
| 12 | 12 |
* ``` |
| 13 | 13 |
* GET /api/v1/mngr/cntntns/quiz/pagination 학교·학년·난이도·검색어 |
| 14 |
- * POST /api/v1/mngr/cntntns/quiz 어노테이션 없음 → form(중첩 목록은 인덱스 표기) |
|
| 14 |
+ * POST /api/v1/mngr/cntntns/quiz @RequestBody(JSON) |
|
| 15 | 15 |
* PUT /api/v1/mngr/cntntns/quiz/{quizSn} @RequestBody(JSON)
|
| 16 | 16 |
* DELETE /api/v1/mngr/cntntns/quiz/{quizSn} soft delete(DEL_YN)
|
| 17 | 17 |
* GET /api/v1/mngr/cntntns/quiz/excel/download 엑셀 다운로드 |
... | ... | @@ -236,40 +236,17 @@ |
| 236 | 236 |
}; |
| 237 | 237 |
} |
| 238 | 238 |
|
| 239 |
-/** 등록만 form이다 — 컨트롤러가 `@RequestBody` 없이 받는다(쇼츠·만화와 같은 갈림). */ |
|
| 240 |
-function toFormParams( |
|
| 241 |
- payload: QuizWritePayload |
|
| 242 |
-): Record<string, string | number | undefined> {
|
|
| 243 |
- const params: Record<string, string | number | undefined> = {
|
|
| 244 |
- quizTitle: payload.quizTitle, |
|
| 245 |
- quizLev: payload.quizLev, |
|
| 246 |
- quizQuestion: payload.quizQuestion, |
|
| 247 |
- quizExplan: payload.quizExplan, |
|
| 248 |
- quizAnswer: payload.quizAnswer, |
|
| 249 |
- useYn: payload.useYn, |
|
| 250 |
- }; |
|
| 251 |
- |
|
| 252 |
- payload.schulGrdList.forEach((item, index) => {
|
|
| 253 |
- params[`schulGrdList[${index}].schulCd`] = item.schulCd;
|
|
| 254 |
- // undefined면 파라미터 자체가 빠져 백엔드에서 null이 된다. |
|
| 255 |
- params[`schulGrdList[${index}].grdCd`] = item.grdCd ?? undefined;
|
|
| 256 |
- }); |
|
| 257 |
- |
|
| 258 |
- return params; |
|
| 259 |
-} |
|
| 260 | 239 |
|
| 261 | 240 |
async function sendWrite( |
| 262 | 241 |
path: string, |
| 263 | 242 |
method: 'POST' | 'PUT' | 'DELETE', |
| 264 |
- body?: unknown, |
|
| 265 |
- form?: Record<string, string | number | undefined> |
|
| 243 |
+ body?: unknown |
|
| 266 | 244 |
): Promise<void> {
|
| 267 | 245 |
const accessToken = await getSessionAccessToken(); |
| 268 | 246 |
|
| 269 | 247 |
const result = await backendFetch<unknown>(path, {
|
| 270 | 248 |
method, |
| 271 | 249 |
...(body === undefined ? {} : { body }),
|
| 272 |
- ...(form === undefined ? {} : { form }),
|
|
| 273 | 250 |
accessToken: accessToken ?? undefined, |
| 274 | 251 |
cache: 'no-store', |
| 275 | 252 |
canHaveNullData: true, |
... | ... | @@ -282,7 +259,7 @@ |
| 282 | 259 |
} |
| 283 | 260 |
|
| 284 | 261 |
export async function createQuiz(values: QuizFormValues): Promise<void> {
|
| 285 |
- await sendWrite(QUIZ_PATH, 'POST', undefined, toFormParams(buildQuizPayload(values))); |
|
| 262 |
+ await sendWrite(QUIZ_PATH, 'POST', buildQuizPayload(values)); |
|
| 286 | 263 |
} |
| 287 | 264 |
|
| 288 | 265 |
export async function updateQuiz( |
--- lib/data/repositories/content-repository.ts
+++ lib/data/repositories/content-repository.ts
... | ... | @@ -17,7 +17,7 @@ |
| 17 | 17 |
* ``` |
| 18 | 18 |
* GET /api/v1/mngr/cntntns/pagination searchCntntsType 필수 |
| 19 | 19 |
* GET /api/v1/mngr/cntntns/{cntntsSn} 단건
|
| 20 |
- * POST /api/v1/mngr/cntntns 어노테이션 없음 → form(중첩 목록은 인덱스 표기) |
|
| 20 |
+ * POST /api/v1/mngr/cntntns @RequestBody(JSON) |
|
| 21 | 21 |
* PUT /api/v1/mngr/cntntns/{cntntsSn} @RequestBody(JSON)
|
| 22 | 22 |
* DELETE /api/v1/mngr/cntntns/{cntntsSn} soft delete(DEL_YN)
|
| 23 | 23 |
* ``` |
... | ... | @@ -297,34 +297,6 @@ |
| 297 | 297 |
}; |
| 298 | 298 |
} |
| 299 | 299 |
|
| 300 |
-/** |
|
| 301 |
- * 등록은 `@RequestBody`가 없어 form으로 간다. 중첩 목록은 Spring이 읽는 인덱스 표기 |
|
| 302 |
- * (`keywordList[0].cntntsKeyword`)로 편다 — JSON으로 보내면 한 칸도 바인딩되지 않는다. |
|
| 303 |
- */ |
|
| 304 |
-function toFormParams( |
|
| 305 |
- payload: ContentWritePayload & { cntntsType?: string }
|
|
| 306 |
-): Record<string, string | number | undefined> {
|
|
| 307 |
- const params: Record<string, string | number | undefined> = {
|
|
| 308 |
- cntntsType: payload.cntntsType, |
|
| 309 |
- cntntsNm: payload.cntntsNm, |
|
| 310 |
- cntntsTitle: payload.cntntsTitle, |
|
| 311 |
- atchFileId: payload.atchFileId, |
|
| 312 |
- thumbAtchFileId: payload.thumbAtchFileId, |
|
| 313 |
- useYn: payload.useYn, |
|
| 314 |
- }; |
|
| 315 |
- |
|
| 316 |
- payload.keywordList.forEach((item, index) => {
|
|
| 317 |
- params[`keywordList[${index}].cntntsKeyword`] = item.cntntsKeyword;
|
|
| 318 |
- params[`keywordList[${index}].cntntsExpln`] = item.cntntsExpln;
|
|
| 319 |
- }); |
|
| 320 |
- payload.schulGrdList.forEach((item, index) => {
|
|
| 321 |
- params[`schulGrdList[${index}].schulCd`] = item.schulCd;
|
|
| 322 |
- // undefined면 파라미터 자체가 빠져 백엔드에서 null이 된다. |
|
| 323 |
- params[`schulGrdList[${index}].grdCd`] = item.grdCd ?? undefined;
|
|
| 324 |
- }); |
|
| 325 |
- |
|
| 326 |
- return params; |
|
| 327 |
-} |
|
| 328 | 300 |
|
| 329 | 301 |
/** |
| 330 | 302 |
* 보낸 것을 서버 콘솔에 그대로 남긴다. |
... | ... | @@ -337,7 +309,6 @@ |
| 337 | 309 |
method: string, |
| 338 | 310 |
path: string, |
| 339 | 311 |
body: unknown, |
| 340 |
- form: Record<string, string | number | undefined> | undefined, |
|
| 341 | 312 |
failure: { code: number; message: string } | null
|
| 342 | 313 |
): void {
|
| 343 | 314 |
if (failure === null && process.env.NODE_ENV === 'production') {
|
... | ... | @@ -351,13 +322,6 @@ |
| 351 | 322 |
const lines = [ |
| 352 | 323 |
head, |
| 353 | 324 |
body === undefined ? null : `body(JSON):\n${JSON.stringify(body, null, 2)}`,
|
| 354 |
- form === undefined |
|
| 355 |
- ? null |
|
| 356 |
- : `form:\n${new URLSearchParams(
|
|
| 357 |
- Object.entries(form).flatMap(([key, value]) => |
|
| 358 |
- value === undefined ? [] : [[key, String(value)] as [string, string]] |
|
| 359 |
- ) |
|
| 360 |
- ).toString()}`, |
|
| 361 | 325 |
].filter(Boolean); |
| 362 | 326 |
|
| 363 | 327 |
if (failure) {
|
... | ... | @@ -370,22 +334,20 @@ |
| 370 | 334 |
async function sendWrite( |
| 371 | 335 |
path: string, |
| 372 | 336 |
method: 'POST' | 'PUT' | 'DELETE', |
| 373 |
- body?: unknown, |
|
| 374 |
- form?: Record<string, string | number | undefined> |
|
| 337 |
+ body?: unknown |
|
| 375 | 338 |
): Promise<string | null> {
|
| 376 | 339 |
const accessToken = await getSessionAccessToken(); |
| 377 | 340 |
|
| 378 | 341 |
const result = await backendFetch<unknown>(path, {
|
| 379 | 342 |
method, |
| 380 | 343 |
...(body === undefined ? {} : { body }),
|
| 381 |
- ...(form === undefined ? {} : { form }),
|
|
| 382 | 344 |
accessToken: accessToken ?? undefined, |
| 383 | 345 |
cache: 'no-store', |
| 384 | 346 |
canHaveNullData: true, |
| 385 | 347 |
canHaveEmptyBody: true, |
| 386 | 348 |
}); |
| 387 | 349 |
|
| 388 |
- logWrite(method, path, body, form, result.ok ? null : result); |
|
| 350 |
+ logWrite(method, path, body, result.ok ? null : result); |
|
| 389 | 351 |
|
| 390 | 352 |
if (!result.ok) {
|
| 391 | 353 |
throw new BackendRequestError(result); |
... | ... | @@ -399,7 +361,7 @@ |
| 399 | 361 |
payload: ContentWritePayload, |
| 400 | 362 |
cntntsType: string |
| 401 | 363 |
): Promise<string | null> {
|
| 402 |
- return sendWrite(CONTENT_BASE_PATH, 'POST', undefined, toFormParams({ ...payload, cntntsType }));
|
|
| 364 |
+ return sendWrite(CONTENT_BASE_PATH, 'POST', { ...payload, cntntsType });
|
|
| 403 | 365 |
} |
| 404 | 366 |
|
| 405 | 367 |
export async function updateContent( |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?