--- lib/data/repositories/content-repository.ts
+++ lib/data/repositories/content-repository.ts
... | ... | @@ -24,14 +24,14 @@ |
| 24 | 24 |
* |
| 25 | 25 |
* 이슈: 경로가 `cntnts`가 아니라 **`cntntns`**다(백엔드 오타). 고쳐지면 아래 상수만 바꾼다. |
| 26 | 26 |
* |
| 27 |
- * ⚠️ 백엔드 결함(보고함, 미수정) |
|
| 28 |
- * 1. `selectPaginationCount`가 `FROM TB_COM_CNTNTS`만 두고 `searchWhere`를 그대로 포함한다. |
|
| 29 |
- * 그 조건은 `c.`·`k.`·`g.` 별칭을 쓰므로 별칭을 모르는 count 쿼리에서 SQL이 깨진다. |
|
| 30 |
- * 2. 학교·학년 조건의 `<if test='searchSchulGradeCd != null != ""'>`가 OGNL에서 항상 참이고, |
|
| 31 |
- * 본문도 값 대신 `#{searchKeyword}`를 비교한다. 파라미터를 보내도 의미가 없어 보내지 않고,
|
|
| 32 |
- * 학교·학년 필터는 받아온 뒤 여기서 건다. |
|
| 33 |
- * 3. 목록 SQL이 키워드·학교급·파일을 모두 LEFT JOIN해 한 콘텐츠가 여러 행이 된다. LIMIT이 그 |
|
| 34 |
- * 늘어난 행에 걸려 한 페이지에 오는 콘텐츠 수가 `recordCountPerPage`보다 적을 수 있다. |
|
| 27 |
+ * ⚠️ 백엔드 결함(보고함, 미수정) — 그래서 목록은 전량 수령 후 로컬 검색·정렬·페이징이다. |
|
| 28 |
+ * 1. 검색 조건(`c.CNTNTS_TITLE`·`k.CNTNTS_KEYWORD`)이 별칭을 쓰는데 `selectPaginationCount`는 |
|
| 29 |
+ * `FROM TB_COM_CNTNTS`뿐이라 검색어를 실으면 count가 SQL 오류로 죽는다. |
|
| 30 |
+ * 2. 목록 정렬이 이중 역순이다 — rnum이 등록일 내림차순인데 `ORDER BY RNUM DESC`로 다시 뒤집어 |
|
| 31 |
+ * **옛 글부터** 나간다. 새로 등록한 글이 첫 페이지가 아니라 마지막 페이지에 붙는다. |
|
| 32 |
+ * 3. 키워드·학교급·파일 LEFT JOIN으로 한 콘텐츠가 여러 행이 되고 LIMIT이 그 행 기준이라, 페이지 |
|
| 33 |
+ * 경계에서 잘린 콘텐츠는 키워드·컷 목록이 반쪽으로 온다. |
|
| 34 |
+ * 4. 학교·학년 파라미터는 `<if>` 본문이 값 대신 `#{searchKeyword}`를 비교해 보내도 의미가 없다.
|
|
| 35 | 35 |
*/ |
| 36 | 36 |
|
| 37 | 37 |
const CONTENT_BASE_PATH = '/api/v1/mngr/cntntns'; |
... | ... | @@ -129,17 +129,14 @@ |
| 129 | 129 |
|
| 130 | 130 |
function buildListParams( |
| 131 | 131 |
kind: ContentApiKind, |
| 132 |
- query: ContentQuery, |
|
| 133 | 132 |
pageIndex: number, |
| 134 | 133 |
recordCountPerPage: number |
| 135 | 134 |
): Record<string, string | number> {
|
| 136 |
- const keyword = query.keyword.trim(); |
|
| 137 |
- |
|
| 138 | 135 |
return {
|
| 139 | 136 |
// 없으면 `CNTNTS_TYPE = NULL`이 되어 한 건도 오지 않는다. |
| 137 |
+ // 검색어는 보내지 않는다 — 검색 조건이 c.·k. 별칭을 쓰는데 count 쿼리에는 별칭이 없어 |
|
| 138 |
+ // 키워드를 실으면 count가 SQL 오류로 죽는다(파일 상단 ⚠️ 1). 검색은 아래에서 로컬로 건다. |
|
| 140 | 139 |
searchCntntsType: CONTENT_TYPE_CODE[kind], |
| 141 |
- searchCondition: keyword ? '1' : '', |
|
| 142 |
- searchKeyword: keyword, |
|
| 143 | 140 |
pageIndex, |
| 144 | 141 |
recordCountPerPage, |
| 145 | 142 |
}; |
... | ... | @@ -147,7 +144,6 @@ |
| 147 | 144 |
|
| 148 | 145 |
async function requestList( |
| 149 | 146 |
kind: ContentApiKind, |
| 150 |
- query: ContentQuery, |
|
| 151 | 147 |
pageIndex: number, |
| 152 | 148 |
recordCountPerPage: number |
| 153 | 149 |
): Promise<{ items: ContentItem[]; reportedTotalCount: number }> {
|
... | ... | @@ -155,7 +151,7 @@ |
| 155 | 151 |
|
| 156 | 152 |
const result = await backendFetch<unknown>(`${CONTENT_BASE_PATH}/pagination`, {
|
| 157 | 153 |
method: 'GET', |
| 158 |
- query: buildListParams(kind, query, pageIndex, recordCountPerPage), |
|
| 154 |
+ query: buildListParams(kind, pageIndex, recordCountPerPage), |
|
| 159 | 155 |
accessToken: accessToken ?? undefined, |
| 160 | 156 |
cache: 'no-store', |
| 161 | 157 |
canHaveNullData: true, |
... | ... | @@ -188,26 +184,50 @@ |
| 188 | 184 |
isTotalCountExact: boolean; |
| 189 | 185 |
}; |
| 190 | 186 |
|
| 187 |
+// TODO(백엔드): 목록 정렬·LIMIT이 고쳐지면 서버 페이징으로 되돌린다. 그전까지 이 상한을 |
|
| 188 |
+// 넘는 콘텐츠는 목록에서 조용히 잘린다. |
|
| 189 |
+const FETCH_LIMIT = 500; |
|
| 190 |
+ |
|
| 191 |
+function matchesContentQuery(item: ContentItem, query: ContentQuery): boolean {
|
|
| 192 |
+ if (query.school && item.schoolCode !== query.school) {
|
|
| 193 |
+ return false; |
|
| 194 |
+ } |
|
| 195 |
+ if (query.grade && !item.grades.includes(query.grade)) {
|
|
| 196 |
+ return false; |
|
| 197 |
+ } |
|
| 198 |
+ |
|
| 199 |
+ const keyword = query.keyword.trim().toLowerCase(); |
|
| 200 |
+ if (!keyword) {
|
|
| 201 |
+ return true; |
|
| 202 |
+ } |
|
| 203 |
+ if (query.searchField === 'keyword') {
|
|
| 204 |
+ return item.keywords.some((entry) => |
|
| 205 |
+ entry.keyword.toLowerCase().includes(keyword) |
|
| 206 |
+ ); |
|
| 207 |
+ } |
|
| 208 |
+ // title(쇼츠)·name(만화) 모두 같은 컬럼(CNTNTS_NM=CNTNTS_TITLE)이라 한 값을 본다. |
|
| 209 |
+ return item.title.toLowerCase().includes(keyword); |
|
| 210 |
+} |
|
| 211 |
+ |
|
| 191 | 212 |
export async function fetchContents( |
| 192 | 213 |
kind: ContentApiKind, |
| 193 | 214 |
query: ContentQuery |
| 194 | 215 |
): Promise<ContentPage> {
|
| 195 |
- const { items, reportedTotalCount } = await requestList(
|
|
| 196 |
- kind, |
|
| 197 |
- query, |
|
| 198 |
- query.page, |
|
| 199 |
- query.pageSize |
|
| 200 |
- ); |
|
| 216 |
+ // 서버 페이징을 쓰지 않는다 — 목록 SQL이 rnum(등록일 내림차순)을 다시 뒤집어 옛 글부터 |
|
| 217 |
+ // 내보내고, LIMIT도 조인으로 불어난 행 기준이라 새 글이 마지막 페이지로 밀린다(⚠️ 2·3). |
|
| 218 |
+ const { items } = await requestList(kind, 1, FETCH_LIMIT);
|
|
| 201 | 219 |
|
| 202 |
- const confirmedCount = (query.page - 1) * query.pageSize + items.length; |
|
| 220 |
+ const matched = items |
|
| 221 |
+ .filter((item) => matchesContentQuery(item, query)) |
|
| 222 |
+ // rnum은 resultMap에 없고 등록일은 일 단위라, 새 글이 먼저 오도록 일련번호로 세운다. |
|
| 223 |
+ .sort((a, b) => Number(b.cntntsSn) - Number(a.cntntsSn)); |
|
| 224 |
+ |
|
| 225 |
+ const offset = (query.page - 1) * query.pageSize; |
|
| 203 | 226 |
|
| 204 | 227 |
return {
|
| 205 |
- items, |
|
| 206 |
- // 백엔드 `PaginationUtil`이 count 없이 페이지 행 수를 총건수로 내려주는 결함이 있어 |
|
| 207 |
- // 하한값으로 보정한다(다른 목록 화면과 같은 처리). |
|
| 208 |
- totalCount: Math.max(reportedTotalCount, confirmedCount), |
|
| 209 |
- isTotalCountExact: |
|
| 210 |
- items.length < query.pageSize || reportedTotalCount > confirmedCount, |
|
| 228 |
+ items: matched.slice(offset, offset + query.pageSize), |
|
| 229 |
+ totalCount: matched.length, |
|
| 230 |
+ isTotalCountExact: true, |
|
| 211 | 231 |
}; |
| 212 | 232 |
} |
| 213 | 233 |
|
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?