임동욱 임동욱 08-20
merge: 콘텐츠 목록 로컬 페이징
@bdeecbc38c7daf8672cac1a52b40de5231d5956e
lib/data/repositories/content-repository.ts
--- lib/data/repositories/content-repository.ts
+++ lib/data/repositories/content-repository.ts
@@ -24,14 +24,14 @@
  *
  * 이슈: 경로가 `cntnts`가 아니라 **`cntntns`**다(백엔드 오타). 고쳐지면 아래 상수만 바꾼다.
  *
- * ⚠️ 백엔드 결함(보고함, 미수정)
- * 1. `selectPaginationCount`가 `FROM TB_COM_CNTNTS`만 두고 `searchWhere`를 그대로 포함한다.
- *    그 조건은 `c.`·`k.`·`g.` 별칭을 쓰므로 별칭을 모르는 count 쿼리에서 SQL이 깨진다.
- * 2. 학교·학년 조건의 `<if test='searchSchulGradeCd != null != ""'>`가 OGNL에서 항상 참이고,
- *    본문도 값 대신 `#{searchKeyword}`를 비교한다. 파라미터를 보내도 의미가 없어 보내지 않고,
- *    학교·학년 필터는 받아온 뒤 여기서 건다.
- * 3. 목록 SQL이 키워드·학교급·파일을 모두 LEFT JOIN해 한 콘텐츠가 여러 행이 된다. LIMIT이 그
- *    늘어난 행에 걸려 한 페이지에 오는 콘텐츠 수가 `recordCountPerPage`보다 적을 수 있다.
+ * ⚠️ 백엔드 결함(보고함, 미수정) — 그래서 목록은 전량 수령 후 로컬 검색·정렬·페이징이다.
+ * 1. 검색 조건(`c.CNTNTS_TITLE`·`k.CNTNTS_KEYWORD`)이 별칭을 쓰는데 `selectPaginationCount`는
+ *    `FROM TB_COM_CNTNTS`뿐이라 검색어를 실으면 count가 SQL 오류로 죽는다.
+ * 2. 목록 정렬이 이중 역순이다 — rnum이 등록일 내림차순인데 `ORDER BY RNUM DESC`로 다시 뒤집어
+ *    **옛 글부터** 나간다. 새로 등록한 글이 첫 페이지가 아니라 마지막 페이지에 붙는다.
+ * 3. 키워드·학교급·파일 LEFT JOIN으로 한 콘텐츠가 여러 행이 되고 LIMIT이 그 행 기준이라, 페이지
+ *    경계에서 잘린 콘텐츠는 키워드·컷 목록이 반쪽으로 온다.
+ * 4. 학교·학년 파라미터는 `<if>` 본문이 값 대신 `#{searchKeyword}`를 비교해 보내도 의미가 없다.
  */
 
 const CONTENT_BASE_PATH = '/api/v1/mngr/cntntns';
@@ -129,17 +129,14 @@
 
 function buildListParams(
   kind: ContentApiKind,
-  query: ContentQuery,
   pageIndex: number,
   recordCountPerPage: number
 ): Record<string, string | number> {
-  const keyword = query.keyword.trim();
-
   return {
     // 없으면 `CNTNTS_TYPE = NULL`이 되어 한 건도 오지 않는다.
+    // 검색어는 보내지 않는다 — 검색 조건이 c.·k. 별칭을 쓰는데 count 쿼리에는 별칭이 없어
+    // 키워드를 실으면 count가 SQL 오류로 죽는다(파일 상단 ⚠️ 1). 검색은 아래에서 로컬로 건다.
     searchCntntsType: CONTENT_TYPE_CODE[kind],
-    searchCondition: keyword ? '1' : '',
-    searchKeyword: keyword,
     pageIndex,
     recordCountPerPage,
   };
@@ -147,7 +144,6 @@
 
 async function requestList(
   kind: ContentApiKind,
-  query: ContentQuery,
   pageIndex: number,
   recordCountPerPage: number
 ): Promise<{ items: ContentItem[]; reportedTotalCount: number }> {
@@ -155,7 +151,7 @@
 
   const result = await backendFetch<unknown>(`${CONTENT_BASE_PATH}/pagination`, {
     method: 'GET',
-    query: buildListParams(kind, query, pageIndex, recordCountPerPage),
+    query: buildListParams(kind, pageIndex, recordCountPerPage),
     accessToken: accessToken ?? undefined,
     cache: 'no-store',
     canHaveNullData: true,
@@ -188,26 +184,50 @@
   isTotalCountExact: boolean;
 };
 
+// TODO(백엔드): 목록 정렬·LIMIT이 고쳐지면 서버 페이징으로 되돌린다. 그전까지 이 상한을
+// 넘는 콘텐츠는 목록에서 조용히 잘린다.
+const FETCH_LIMIT = 500;
+
+function matchesContentQuery(item: ContentItem, query: ContentQuery): boolean {
+  if (query.school && item.schoolCode !== query.school) {
+    return false;
+  }
+  if (query.grade && !item.grades.includes(query.grade)) {
+    return false;
+  }
+
+  const keyword = query.keyword.trim().toLowerCase();
+  if (!keyword) {
+    return true;
+  }
+  if (query.searchField === 'keyword') {
+    return item.keywords.some((entry) =>
+      entry.keyword.toLowerCase().includes(keyword)
+    );
+  }
+  // title(쇼츠)·name(만화) 모두 같은 컬럼(CNTNTS_NM=CNTNTS_TITLE)이라 한 값을 본다.
+  return item.title.toLowerCase().includes(keyword);
+}
+
 export async function fetchContents(
   kind: ContentApiKind,
   query: ContentQuery
 ): Promise<ContentPage> {
-  const { items, reportedTotalCount } = await requestList(
-    kind,
-    query,
-    query.page,
-    query.pageSize
-  );
+  // 서버 페이징을 쓰지 않는다 — 목록 SQL이 rnum(등록일 내림차순)을 다시 뒤집어 옛 글부터
+  // 내보내고, LIMIT도 조인으로 불어난 행 기준이라 새 글이 마지막 페이지로 밀린다(⚠️ 2·3).
+  const { items } = await requestList(kind, 1, FETCH_LIMIT);
 
-  const confirmedCount = (query.page - 1) * query.pageSize + items.length;
+  const matched = items
+    .filter((item) => matchesContentQuery(item, query))
+    // rnum은 resultMap에 없고 등록일은 일 단위라, 새 글이 먼저 오도록 일련번호로 세운다.
+    .sort((a, b) => Number(b.cntntsSn) - Number(a.cntntsSn));
+
+  const offset = (query.page - 1) * query.pageSize;
 
   return {
-    items,
-    // 백엔드 `PaginationUtil`이 count 없이 페이지 행 수를 총건수로 내려주는 결함이 있어
-    // 하한값으로 보정한다(다른 목록 화면과 같은 처리).
-    totalCount: Math.max(reportedTotalCount, confirmedCount),
-    isTotalCountExact:
-      items.length < query.pageSize || reportedTotalCount > confirmedCount,
+    items: matched.slice(offset, offset + query.pageSize),
+    totalCount: matched.length,
+    isTotalCountExact: true,
   };
 }
 
Add a comment
List