임동욱 임동욱 08-21
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
+++ lib/data/repositories/content-quiz-repository.ts
@@ -7,11 +7,11 @@
 
 /**
  * 금융OX퀴즈 Repository — 쇼츠·만화와 테이블(`TB_COM_QUIZ`)은 다르지만 콘텐츠관리 API 아래로
- * 들어와 규약은 같다(등록은 form, 수정은 JSON, 삭제는 soft delete).
+ * 들어와 규약은 같다(등록·수정 모두 JSON, 삭제는 soft delete).
  *
  * ```
  * GET    /api/v1/mngr/cntntns/quiz/pagination      학교·학년·난이도·검색어
- * POST   /api/v1/mngr/cntntns/quiz                 어노테이션 없음 → form(중첩 목록은 인덱스 표기)
+ * POST   /api/v1/mngr/cntntns/quiz                 @RequestBody(JSON)
  * PUT    /api/v1/mngr/cntntns/quiz/{quizSn}        @RequestBody(JSON)
  * DELETE /api/v1/mngr/cntntns/quiz/{quizSn}        soft delete(DEL_YN)
  * GET    /api/v1/mngr/cntntns/quiz/excel/download  엑셀 다운로드
@@ -236,40 +236,17 @@
   };
 }
 
-/** 등록만 form이다 — 컨트롤러가 `@RequestBody` 없이 받는다(쇼츠·만화와 같은 갈림). */
-function toFormParams(
-  payload: QuizWritePayload
-): Record<string, string | number | undefined> {
-  const params: Record<string, string | number | undefined> = {
-    quizTitle: payload.quizTitle,
-    quizLev: payload.quizLev,
-    quizQuestion: payload.quizQuestion,
-    quizExplan: payload.quizExplan,
-    quizAnswer: payload.quizAnswer,
-    useYn: payload.useYn,
-  };
-
-  payload.schulGrdList.forEach((item, index) => {
-    params[`schulGrdList[${index}].schulCd`] = item.schulCd;
-    // undefined면 파라미터 자체가 빠져 백엔드에서 null이 된다.
-    params[`schulGrdList[${index}].grdCd`] = item.grdCd ?? undefined;
-  });
-
-  return params;
-}
 
 async function sendWrite(
   path: string,
   method: 'POST' | 'PUT' | 'DELETE',
-  body?: unknown,
-  form?: Record<string, string | number | undefined>
+  body?: unknown
 ): Promise<void> {
   const accessToken = await getSessionAccessToken();
 
   const result = await backendFetch<unknown>(path, {
     method,
     ...(body === undefined ? {} : { body }),
-    ...(form === undefined ? {} : { form }),
     accessToken: accessToken ?? undefined,
     cache: 'no-store',
     canHaveNullData: true,
@@ -282,7 +259,7 @@
 }
 
 export async function createQuiz(values: QuizFormValues): Promise<void> {
-  await sendWrite(QUIZ_PATH, 'POST', undefined, toFormParams(buildQuizPayload(values)));
+  await sendWrite(QUIZ_PATH, 'POST', buildQuizPayload(values));
 }
 
 export async function updateQuiz(
lib/data/repositories/content-repository.ts
--- lib/data/repositories/content-repository.ts
+++ lib/data/repositories/content-repository.ts
@@ -17,7 +17,7 @@
  * ```
  * GET    /api/v1/mngr/cntntns/pagination    searchCntntsType 필수
  * GET    /api/v1/mngr/cntntns/{cntntsSn}    단건
- * POST   /api/v1/mngr/cntntns               어노테이션 없음 → form(중첩 목록은 인덱스 표기)
+ * POST   /api/v1/mngr/cntntns               @RequestBody(JSON)
  * PUT    /api/v1/mngr/cntntns/{cntntsSn}    @RequestBody(JSON)
  * DELETE /api/v1/mngr/cntntns/{cntntsSn}    soft delete(DEL_YN)
  * ```
@@ -297,34 +297,6 @@
   };
 }
 
-/**
- * 등록은 `@RequestBody`가 없어 form으로 간다. 중첩 목록은 Spring이 읽는 인덱스 표기
- * (`keywordList[0].cntntsKeyword`)로 편다 — JSON으로 보내면 한 칸도 바인딩되지 않는다.
- */
-function toFormParams(
-  payload: ContentWritePayload & { cntntsType?: string }
-): Record<string, string | number | undefined> {
-  const params: Record<string, string | number | undefined> = {
-    cntntsType: payload.cntntsType,
-    cntntsNm: payload.cntntsNm,
-    cntntsTitle: payload.cntntsTitle,
-    atchFileId: payload.atchFileId,
-    thumbAtchFileId: payload.thumbAtchFileId,
-    useYn: payload.useYn,
-  };
-
-  payload.keywordList.forEach((item, index) => {
-    params[`keywordList[${index}].cntntsKeyword`] = item.cntntsKeyword;
-    params[`keywordList[${index}].cntntsExpln`] = item.cntntsExpln;
-  });
-  payload.schulGrdList.forEach((item, index) => {
-    params[`schulGrdList[${index}].schulCd`] = item.schulCd;
-    // undefined면 파라미터 자체가 빠져 백엔드에서 null이 된다.
-    params[`schulGrdList[${index}].grdCd`] = item.grdCd ?? undefined;
-  });
-
-  return params;
-}
 
 /**
  * 보낸 것을 서버 콘솔에 그대로 남긴다.
@@ -337,7 +309,6 @@
   method: string,
   path: string,
   body: unknown,
-  form: Record<string, string | number | undefined> | undefined,
   failure: { code: number; message: string } | null
 ): void {
   if (failure === null && process.env.NODE_ENV === 'production') {
@@ -351,13 +322,6 @@
   const lines = [
     head,
     body === undefined ? null : `body(JSON):\n${JSON.stringify(body, null, 2)}`,
-    form === undefined
-      ? null
-      : `form:\n${new URLSearchParams(
-          Object.entries(form).flatMap(([key, value]) =>
-            value === undefined ? [] : [[key, String(value)] as [string, string]]
-          )
-        ).toString()}`,
   ].filter(Boolean);
 
   if (failure) {
@@ -370,22 +334,20 @@
 async function sendWrite(
   path: string,
   method: 'POST' | 'PUT' | 'DELETE',
-  body?: unknown,
-  form?: Record<string, string | number | undefined>
+  body?: unknown
 ): Promise<string | null> {
   const accessToken = await getSessionAccessToken();
 
   const result = await backendFetch<unknown>(path, {
     method,
     ...(body === undefined ? {} : { body }),
-    ...(form === undefined ? {} : { form }),
     accessToken: accessToken ?? undefined,
     cache: 'no-store',
     canHaveNullData: true,
     canHaveEmptyBody: true,
   });
 
-  logWrite(method, path, body, form, result.ok ? null : result);
+  logWrite(method, path, body, result.ok ? null : result);
 
   if (!result.ok) {
     throw new BackendRequestError(result);
@@ -399,7 +361,7 @@
   payload: ContentWritePayload,
   cntntsType: string
 ): Promise<string | null> {
-  return sendWrite(CONTENT_BASE_PATH, 'POST', undefined, toFormParams({ ...payload, cntntsType }));
+  return sendWrite(CONTENT_BASE_PATH, 'POST', { ...payload, cntntsType });
 }
 
 export async function updateContent(
Add a comment
List