임동욱 임동욱 09-03
fix: 백엔드 필드명 변경에 맞춰 콘텐츠·퀴즈·포인트를 다시 맞춘다
백엔드가 콘텐츠 키를 cntntsSn -> cntntsId로, 퀴즈 필드를 quiz* -> cntntsQuiz*로
바꿨다(퀴즈 식별자도 quizSn -> cntntsId). 우리 쪽은 응답에서 옛 이름을 찾다 던지고
있어 금융쇼츠·4컷만화·금융OX퀴즈 목록이 전부 열리지 않는 상태였다.

누적포인트 목록 검색도 searchUserId -> searchLoginId로 바뀌어 맞춘다.

라우트 폴더 이름도 함께 바꿨다 — 동적 세그먼트라 주소에는 값만 나가므로 URL은
그대로다.

Co-Authored-By: Claude Opus 5 
@83e613581076f7cdc589af7fec39f0bf269425c7
app/(protected)/(basic)/contents/_actions.ts
--- app/(protected)/(basic)/contents/_actions.ts
+++ app/(protected)/(basic)/contents/_actions.ts
@@ -269,8 +269,8 @@
 ): Promise<ContentFormState> {
   await verifySession();
 
-  const cntntsSn = readString(formData, 'cntntsSn');
-  if (!cntntsSn) {
+  const cntntsId = readString(formData, 'cntntsId');
+  if (!cntntsId) {
     return { status: 'error', message: INVALID_REQUEST_MESSAGE };
   }
   if (readString(formData, 'videoUploading') === '1') {
@@ -291,7 +291,7 @@
 
     await updateContent(
       'shorts',
-      cntntsSn,
+      cntntsId,
       buildShortsPayload({
         ...validation.values,
         thumbnailFileId: await resolveThumbnailId(formData),
@@ -347,8 +347,8 @@
 ): Promise<ContentFormState> {
   await verifySession();
 
-  const cntntsSn = readString(formData, 'cntntsSn');
-  if (!cntntsSn) {
+  const cntntsId = readString(formData, 'cntntsId');
+  if (!cntntsId) {
     return { status: 'error', message: INVALID_REQUEST_MESSAGE };
   }
 
@@ -365,7 +365,7 @@
       return { status: 'error', errors: validation.errors };
     }
 
-    await updateContent('comic', cntntsSn, buildComicPayload(validation.values));
+    await updateContent('comic', cntntsId, buildComicPayload(validation.values));
     return null;
   });
   if (failed) {
@@ -408,8 +408,8 @@
 ): Promise<ContentFormState> {
   await verifySession();
 
-  const quizSn = readString(formData, 'quizSn');
-  if (!quizSn) {
+  const quizId = readString(formData, 'quizId');
+  if (!quizId) {
     return { status: 'error', message: INVALID_REQUEST_MESSAGE };
   }
 
@@ -422,7 +422,7 @@
       return { status: 'error', errors: validation.errors };
     }
 
-    await updateQuiz(quizSn, validation.values);
+    await updateQuiz(quizId, validation.values);
     return null;
   });
   if (failed) {
app/(protected)/(basic)/contents/comics/[cntntsId]/page.tsx (Renamed from app/(protected)/(basic)/contents/comics/[cntntsSn]/page.tsx)
--- app/(protected)/(basic)/contents/comics/[cntntsSn]/page.tsx
+++ app/(protected)/(basic)/contents/comics/[cntntsId]/page.tsx
@@ -14,7 +14,7 @@
 export const dynamic = 'force-dynamic';
 
 interface PageProps {
-  params: Promise<{ cntntsSn: string }>;
+  params: Promise<{ cntntsId: string }>;
 }
 
 export default async function Page({ params }: PageProps) {
@@ -22,8 +22,8 @@
 
   const { schools, gradesBySchool } = await fetchSchoolGradeCodes();
 
-  const { cntntsSn } = await params;
-  const item = await fetchContentDetail('comic', cntntsSn);
+  const { cntntsId } = await params;
+  const item = await fetchContentDetail('comic', cntntsId);
   if (!item) {
     notFound();
   }
app/(protected)/(basic)/contents/comics/_components/comic-form.tsx
--- app/(protected)/(basic)/contents/comics/_components/comic-form.tsx
+++ app/(protected)/(basic)/contents/comics/_components/comic-form.tsx
@@ -65,11 +65,11 @@
         onSubmit={(event) => submitFormAction(event, formAction)}
         className={styles.form}
       >
-        {isEdit && <input type="hidden" name="cntntsSn" value={item.cntntsSn} />}
+        {isEdit && <input type="hidden" name="cntntsId" value={item.cntntsId} />}
 
         {isEdit && (
           <ContentFormRow label="콘텐츠ID">
-            <FoxInput size="md" value={item.cntntsSn} readOnly />
+            <FoxInput size="md" value={item.cntntsId} readOnly />
           </ContentFormRow>
         )}
 
app/(protected)/(basic)/contents/comics/_components/comics-list.tsx
--- app/(protected)/(basic)/contents/comics/_components/comics-list.tsx
+++ app/(protected)/(basic)/contents/comics/_components/comics-list.tsx
@@ -95,7 +95,7 @@
       render: (row) => (
         <ContentRowActions
           kind="comic"
-          id={row.cntntsSn}
+          id={row.cntntsId}
           name={row.name || row.title}
         />
       ),
@@ -107,7 +107,7 @@
       kind="comic"
       columns={columns}
       rows={items}
-      rowKey={(row) => row.cntntsSn}
+      rowKey={(row) => row.cntntsId}
       query={query}
       currentPage={currentPage}
       totalPages={totalPages}
app/(protected)/(basic)/contents/quizzes/[quizId]/page.tsx (Renamed from app/(protected)/(basic)/contents/quizzes/[quizSn]/page.tsx)
--- app/(protected)/(basic)/contents/quizzes/[quizSn]/page.tsx
+++ app/(protected)/(basic)/contents/quizzes/[quizId]/page.tsx
@@ -9,7 +9,7 @@
 export const dynamic = 'force-dynamic';
 
 interface PageProps {
-  params: Promise<{ quizSn: string }>;
+  params: Promise<{ quizId: string }>;
 }
 
 export default async function Page({ params }: PageProps) {
@@ -17,8 +17,8 @@
 
   const { schools, gradesBySchool } = await fetchSchoolGradeCodes();
 
-  const { quizSn } = await params;
-  const item = await fetchQuizDetail(quizSn);
+  const { quizId } = await params;
+  const item = await fetchQuizDetail(quizId);
   if (!item) {
     notFound();
   }
app/(protected)/(basic)/contents/quizzes/_components/quiz-form.tsx
--- app/(protected)/(basic)/contents/quizzes/_components/quiz-form.tsx
+++ app/(protected)/(basic)/contents/quizzes/_components/quiz-form.tsx
@@ -69,13 +69,13 @@
         onSubmit={(event) => submitFormAction(event, formAction)}
         className={styles.form}
       >
-        {isEdit && <input type="hidden" name="quizSn" value={item.quizSn} />}
+        {isEdit && <input type="hidden" name="quizId" value={item.quizId} />}
         {/* 화면에 입력 칸이 없는 값이라 그대로 돌려보내 지워지지 않게 한다(도메인 주석 참조). */}
         <input type="hidden" name="quizPoint" value={item?.points ?? ''} />
 
         {isEdit && (
           <ContentFormRow label="콘텐츠ID">
-            <FoxInput size="md" value={item.quizSn} readOnly />
+            <FoxInput size="md" value={item.quizId} readOnly />
           </ContentFormRow>
         )}
 
app/(protected)/(basic)/contents/quizzes/_components/quizzes-list.tsx
--- app/(protected)/(basic)/contents/quizzes/_components/quizzes-list.tsx
+++ app/(protected)/(basic)/contents/quizzes/_components/quizzes-list.tsx
@@ -101,7 +101,7 @@
       render: (row) => (
         <ContentRowActions
           kind="quiz"
-          id={row.quizSn}
+          id={row.quizId}
           name={row.question || CONTENT_KIND_LABEL.quiz}
         />
       ),
@@ -113,7 +113,7 @@
       kind="quiz"
       columns={columns}
       rows={items}
-      rowKey={(row) => row.quizSn}
+      rowKey={(row) => row.quizId}
       query={query}
       currentPage={currentPage}
       totalPages={totalPages}
app/(protected)/(basic)/contents/shorts/[cntntsId]/page.tsx (Renamed from app/(protected)/(basic)/contents/shorts/[cntntsSn]/page.tsx)
--- app/(protected)/(basic)/contents/shorts/[cntntsSn]/page.tsx
+++ app/(protected)/(basic)/contents/shorts/[cntntsId]/page.tsx
@@ -12,7 +12,7 @@
 export const dynamic = 'force-dynamic';
 
 interface PageProps {
-  params: Promise<{ cntntsSn: string }>;
+  params: Promise<{ cntntsId: string }>;
 }
 
 export default async function Page({ params }: PageProps) {
@@ -20,8 +20,8 @@
 
   const { schools, gradesBySchool } = await fetchSchoolGradeCodes();
 
-  const { cntntsSn } = await params;
-  const item = await fetchContentDetail('shorts', cntntsSn);
+  const { cntntsId } = await params;
+  const item = await fetchContentDetail('shorts', cntntsId);
   if (!item) {
     notFound();
   }
app/(protected)/(basic)/contents/shorts/_components/shorts-form.tsx
--- app/(protected)/(basic)/contents/shorts/_components/shorts-form.tsx
+++ app/(protected)/(basic)/contents/shorts/_components/shorts-form.tsx
@@ -62,11 +62,11 @@
         onSubmit={(event) => submitFormAction(event, formAction)}
         className={styles.form}
       >
-        {isEdit && <input type="hidden" name="cntntsSn" value={item.cntntsSn} />}
+        {isEdit && <input type="hidden" name="cntntsId" value={item.cntntsId} />}
 
         {isEdit && (
           <ContentFormRow label="콘텐츠ID">
-            <FoxInput size="md" value={item.cntntsSn} readOnly />
+            <FoxInput size="md" value={item.cntntsId} readOnly />
           </ContentFormRow>
         )}
 
app/(protected)/(basic)/contents/shorts/_components/shorts-list.tsx
--- app/(protected)/(basic)/contents/shorts/_components/shorts-list.tsx
+++ app/(protected)/(basic)/contents/shorts/_components/shorts-list.tsx
@@ -95,7 +95,7 @@
       render: (row) => (
         <ContentRowActions
           kind="shorts"
-          id={row.cntntsSn}
+          id={row.cntntsId}
           name={row.title || row.name}
         />
       ),
@@ -107,7 +107,7 @@
       kind="shorts"
       columns={columns}
       rows={items}
-      rowKey={(row) => row.cntntsSn}
+      rowKey={(row) => row.cntntsId}
       query={query}
       currentPage={currentPage}
       totalPages={totalPages}
lib/data/repositories/content-quiz-repository.ts
--- lib/data/repositories/content-quiz-repository.ts
+++ lib/data/repositories/content-quiz-repository.ts
@@ -11,10 +11,10 @@
  *
  * ```
  * GET    /api/v1/mngr/cntntns/quiz/pagination      학교·학년·난이도·검색어
- * GET    /api/v1/mngr/cntntns/quiz/{quizSn}        단건
+ * GET    /api/v1/mngr/cntntns/quiz/{quizId}        단건
  * POST   /api/v1/mngr/cntntns/quiz                 어노테이션 없음 → form(중첩 목록은 인덱스 표기)
- * PUT    /api/v1/mngr/cntntns/quiz/{quizSn}        @RequestBody(JSON)
- * DELETE /api/v1/mngr/cntntns/quiz/{quizSn}        soft delete(DEL_YN)
+ * PUT    /api/v1/mngr/cntntns/quiz/{quizId}        @RequestBody(JSON)
+ * DELETE /api/v1/mngr/cntntns/quiz/{quizId}        soft delete(DEL_YN)
  * GET    /api/v1/mngr/cntntns/quiz/excel/download  엑셀 다운로드
  * ```
  *
@@ -80,21 +80,21 @@
     throw new Error('퀴즈 응답 항목의 형식이 올바르지 않습니다.');
   }
 
-  const quizSn = readString(raw, 'quizSn');
-  if (!quizSn) {
-    throw new Error('퀴즈 응답에 quizSn이 없습니다.');
+  const quizId = readString(raw, 'cntntsId');
+  if (!quizId) {
+    throw new Error('퀴즈 응답에 cntntsId가 없습니다.');
   }
 
   const points = raw.quizPoint;
   const schulGrd = toSchoolGrades(raw.schulGrdList);
 
   return {
-    quizSn,
+    quizId,
     keywordText: readString(raw, 'quizTitle') ?? '',
-    level: readString(raw, 'quizLev') ?? '',
-    question: readString(raw, 'quizQuestion') ?? '',
-    explanation: readString(raw, 'quizExplan') ?? '',
-    answer: readString(raw, 'quizAnswer') ?? '',
+    level: readString(raw, 'cntntsQuizLev') ?? '',
+    question: readString(raw, 'cntntsQuizQuestion') ?? '',
+    explanation: readString(raw, 'cntntsQuizExplan') ?? '',
+    answer: readString(raw, 'cntntsQuizAnswer') ?? '',
     points: typeof points === 'number' ? points : null,
     schoolCode: schulGrd.schoolCode,
     grades: schulGrd.grades,
@@ -180,11 +180,11 @@
   };
 }
 
-export async function fetchQuizDetail(quizSn: string): Promise<QuizItem | null> {
+export async function fetchQuizDetail(quizId: string): Promise<QuizItem | null> {
   const accessToken = await getSessionAccessToken();
 
   const result = await backendFetch<unknown>(
-    `${QUIZ_PATH}/${encodeURIComponent(quizSn)}`,
+    `${QUIZ_PATH}/${encodeURIComponent(quizId)}`,
     {
       method: 'GET',
       accessToken: accessToken ?? undefined,
@@ -230,10 +230,10 @@
    * 때문이다.
    */
   quizTitle: string;
-  quizLev: string;
-  quizQuestion: string;
-  quizExplan: string;
-  quizAnswer: string;
+  cntntsQuizLev: string;
+  cntntsQuizQuestion: string;
+  cntntsQuizExplan: string;
+  cntntsQuizAnswer: string;
   useYn: string;
   keywordList: QuizKeywordPayload[];
   schulGrdList: Array<{ schulCd: string; grdCd: string | null }>;
@@ -247,10 +247,10 @@
   return {
     ...(values.points === '' ? {} : { quizPoint: values.points }),
     quizTitle: values.keywords.map((item) => item.keyword).join(', '),
-    quizLev: values.level,
-    quizQuestion: values.question,
-    quizExplan: values.explanation,
-    quizAnswer: values.answer,
+    cntntsQuizLev: values.level,
+    cntntsQuizQuestion: values.question,
+    cntntsQuizExplan: values.explanation,
+    cntntsQuizAnswer: values.answer,
     useYn: values.isVisible ? 'Y' : 'N',
     keywordList: values.keywords.map((item) => ({
       [QUIZ_KEYWORD_FIELD.keyword]: item.keyword,
@@ -273,10 +273,10 @@
   const params: Record<string, string | number | undefined> = {
     quizPoint: payload.quizPoint,
     quizTitle: payload.quizTitle,
-    quizLev: payload.quizLev,
-    quizQuestion: payload.quizQuestion,
-    quizExplan: payload.quizExplan,
-    quizAnswer: payload.quizAnswer,
+    cntntsQuizLev: payload.cntntsQuizLev,
+    cntntsQuizQuestion: payload.cntntsQuizQuestion,
+    cntntsQuizExplan: payload.cntntsQuizExplan,
+    cntntsQuizAnswer: payload.cntntsQuizAnswer,
     useYn: payload.useYn,
   };
 
@@ -333,16 +333,16 @@
 }
 
 export async function updateQuiz(
-  quizSn: string,
+  quizId: string,
   values: QuizFormValues
 ): Promise<void> {
   await sendWrite(
-    `${QUIZ_PATH}/${encodeURIComponent(quizSn)}`,
+    `${QUIZ_PATH}/${encodeURIComponent(quizId)}`,
     'PUT',
     toJsonBody(buildQuizPayload(values))
   );
 }
 
-export async function deleteQuiz(quizSn: string): Promise<void> {
-  await sendWrite(`${QUIZ_PATH}/${encodeURIComponent(quizSn)}`, 'DELETE');
+export async function deleteQuiz(quizId: string): Promise<void> {
+  await sendWrite(`${QUIZ_PATH}/${encodeURIComponent(quizId)}`, 'DELETE');
 }
lib/data/repositories/content-repository.ts
--- lib/data/repositories/content-repository.ts
+++ lib/data/repositories/content-repository.ts
@@ -16,10 +16,10 @@
  *
  * ```
  * GET    /api/v1/mngr/cntntns/pagination    searchCntntsType 필수
- * GET    /api/v1/mngr/cntntns/{cntntsSn}    단건
+ * GET    /api/v1/mngr/cntntns/{cntntsId}    단건
  * POST   /api/v1/mngr/cntntns               어노테이션 없음 → form(중첩 목록은 인덱스 표기)
- * PUT    /api/v1/mngr/cntntns/{cntntsSn}    @RequestBody(JSON)
- * DELETE /api/v1/mngr/cntntns/{cntntsSn}    soft delete(DEL_YN)
+ * PUT    /api/v1/mngr/cntntns/{cntntsId}    @RequestBody(JSON)
+ * DELETE /api/v1/mngr/cntntns/{cntntsId}    soft delete(DEL_YN)
  * ```
  *
  * 이슈: 경로가 `cntnts`가 아니라 **`cntntns`**다(백엔드 오타). 고쳐지면 아래 상수만 바꾼다.
@@ -118,15 +118,15 @@
     throw new Error('콘텐츠 응답 항목의 형식이 올바르지 않습니다.');
   }
 
-  const cntntsSn = readString(raw, 'cntntsSn');
-  if (!cntntsSn) {
-    throw new Error('콘텐츠 응답에 cntntsSn이 없습니다.');
+  const cntntsId = readString(raw, 'cntntsId');
+  if (!cntntsId) {
+    throw new Error('콘텐츠 응답에 cntntsId이 없습니다.');
   }
 
   const schulGrd = toSchoolGrades(raw.schulGrdList);
 
   return {
-    cntntsSn,
+    cntntsId,
     cutFileNumbers: toFileNumbers(raw.fileList),
     name: readString(raw, 'cntntsNm') ?? '',
     title: readString(raw, 'cntntsTitle') ?? '',
@@ -237,7 +237,7 @@
   const matched = items
     .filter((item) => matchesContentQuery(item, query))
     // rnum은 resultMap에 없고 등록일은 일 단위라, 새 글이 먼저 오도록 일련번호로 세운다.
-    .sort((a, b) => Number(b.cntntsSn) - Number(a.cntntsSn));
+    .sort((a, b) => Number(b.cntntsId) - Number(a.cntntsId));
 
   const offset = (query.page - 1) * query.pageSize;
 
@@ -251,13 +251,13 @@
 /** 단건 조회 — 목록과 같은 SQL이라 키워드·학교급·파일 목록이 함께 온다. */
 export async function fetchContentDetail(
   kind: ContentApiKind,
-  cntntsSn: string
+  cntntsId: string
 ): Promise<ContentItem | null> {
   void kind;
   const accessToken = await getSessionAccessToken();
 
   const result = await backendFetch<unknown>(
-    `${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsSn)}`,
+    `${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsId)}`,
     {
       method: 'GET',
       accessToken: accessToken ?? undefined,
@@ -431,11 +431,11 @@
 
 export async function updateContent(
   kind: ContentApiKind,
-  cntntsSn: string,
+  cntntsId: string,
   payload: ContentWritePayload
 ): Promise<void> {
   await sendWrite(
-    `${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsSn)}`,
+    `${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsId)}`,
     'PUT',
     // 수정 SQL이 `CNTNTS_TYPE = #{cntntsType}`을 조건 없이 덮는다. 빼고 보내면 종류가 지워져
     // 목록(`CNTNTS_TYPE = 'V'|'T'`)에서 사라진다.
@@ -445,9 +445,9 @@
 
 export async function deleteContent(
   kind: ContentApiKind,
-  cntntsSn: string
+  cntntsId: string
 ): Promise<void> {
-  await sendWrite(`${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsSn)}`, 'DELETE');
+  await sendWrite(`${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsId)}`, 'DELETE');
 }
 
 export function contentThumbnailUrl(item: ContentItem): string | null {
lib/data/repositories/point-account-repository.ts
--- lib/data/repositories/point-account-repository.ts
+++ lib/data/repositories/point-account-repository.ts
@@ -141,7 +141,7 @@
       searchClsNo: query.classNo === null ? '' : String(query.classNo),
       searchNo: query.studentNo === null ? '' : String(query.studentNo),
       // 검색 대상이 아이디일 때만 백엔드가 걸러 준다 — 이름 검색은 아래에서 건다.
-      searchUserId: query.searchField === 'loginId' ? keyword : '',
+      searchLoginId: query.searchField === 'loginId' ? keyword : '',
       searchKeyword: keyword,
       pageIndex: query.page,
       recordCountPerPage: query.pageSize,
lib/domain/content.ts
--- lib/domain/content.ts
+++ lib/domain/content.ts
@@ -87,7 +87,7 @@
 
 /** 쇼츠·만화 한 건. 백엔드 `MngrCntntsToonVo`(= `CmmCntntsVo`)를 화면 언어로 옮긴 것. */
 export type ContentItem = {
-  cntntsSn: string;
+  cntntsId: string;
   /** 만화의 「콘텐츠명」. 쇼츠 목록에는 열이 없다. */
   name: string;
   /** 쇼츠의 「제목」. */
@@ -112,7 +112,7 @@
 
 /** 금융OX퀴즈 한 건. 백엔드 `MngrQuizVo`. */
 export type QuizItem = {
-  quizSn: string;
+  quizId: string;
   /** 백엔드 `quizTitle` — 목록의 「키워드」 열이다(제목이 아니다). */
   keywordText: string;
   level: string;
Add a comment
List