feat: 관리자 회원 검색·페이징을 백엔드에 맡긴다
백엔드에 검색이 생겼다 — `searchCondition` 1=이름 2=아이디 3=전화번호로 매퍼가 컬럼을 고르고, 별도 카운트 쿼리로 전체 건수를 센다. 지금까지는 전체(최대 1만 건)를 받아 프론트에서 거르고 잘랐다. 이름순만 예외로 남긴다. 백엔드에 정렬 파라미터가 없어 그때는 여전히 전체를 받는다. Co-Authored-By: Claude Opus 5
@0e2100736b2fbd1b94e7a79feaccb7907ea9c88f
--- lib/data/repositories/admin-member-repository.ts
+++ lib/data/repositories/admin-member-repository.ts
... | ... | @@ -10,8 +10,20 @@ |
| 10 | 10 |
const ADMIN_MEMBER_BASE_PATH = '/api/v1/mngr/admin'; |
| 11 | 11 |
const ADMIN_MEMBER_PAGINATION_PATH = `${ADMIN_MEMBER_BASE_PATH}/pagination`;
|
| 12 | 12 |
|
| 13 |
-// 이슈: 백엔드 `totalCount`가 전체가 아니라 현재 페이지 행 수라 페이징을 맡길 수 없다. |
|
| 14 |
-// 이름순 정렬 파라미터도 없어 전체를 받아 서버에서 자른다. |
|
| 13 |
+/** |
|
| 14 |
+ * 검색 대상 → 백엔드 `searchCondition`. 매퍼가 이 값으로 컬럼을 고른다 |
|
| 15 |
+ * (1=USER_NM · 2=LOGIN_ID · 3=USER_TEL_NO). |
|
| 16 |
+ */ |
|
| 17 |
+const SEARCH_CONDITION_BY_FIELD: Record<AdminMemberSearchField, string> = {
|
|
| 18 |
+ name: '1', |
|
| 19 |
+ loginId: '2', |
|
| 20 |
+ phoneNumber: '3', |
|
| 21 |
+}; |
|
| 22 |
+ |
|
| 23 |
+/** |
|
| 24 |
+ * 이름순 정렬에만 쓰는 상한. 백엔드에 정렬 파라미터가 없어 그때만 전체를 받아 여기서 |
|
| 25 |
+ * 정렬하고 자른다 — 검색·페이징은 백엔드가 한다. |
|
| 26 |
+ */ |
|
| 15 | 27 |
const ADMIN_MEMBER_FETCH_LIMIT = 10_000; |
| 16 | 28 |
|
| 17 | 29 |
function isRecord(value: unknown): value is Record<string, unknown> {
|
... | ... | @@ -64,12 +76,26 @@ |
| 64 | 76 |
}; |
| 65 | 77 |
} |
| 66 | 78 |
|
| 67 |
-async function fetchAllAdminMembers(): Promise<AdminMember[]> {
|
|
| 79 |
+/** 검색 조건을 백엔드 파라미터로 옮긴다. 검색어가 비면 조건 자체를 보내지 않는다. */ |
|
| 80 |
+function toSearchParams(query: AdminMemberQuery): Record<string, string> {
|
|
| 81 |
+ const keyword = query.keyword.trim(); |
|
| 82 |
+ if (!keyword) {
|
|
| 83 |
+ return {};
|
|
| 84 |
+ } |
|
| 85 |
+ return {
|
|
| 86 |
+ searchCondition: SEARCH_CONDITION_BY_FIELD[query.searchField], |
|
| 87 |
+ searchKeyword: keyword, |
|
| 88 |
+ }; |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+async function requestPage( |
|
| 92 |
+ params: Record<string, string | number> |
|
| 93 |
+): Promise<{ items: AdminMember[]; totalCount: number }> {
|
|
| 68 | 94 |
const accessToken = await getSessionAccessToken(); |
| 69 | 95 |
|
| 70 | 96 |
const result = await backendFetch<unknown>(ADMIN_MEMBER_PAGINATION_PATH, {
|
| 71 | 97 |
method: 'GET', |
| 72 |
- query: { pageIndex: 1, recordCountPerPage: ADMIN_MEMBER_FETCH_LIMIT },
|
|
| 98 |
+ query: params, |
|
| 73 | 99 |
accessToken: accessToken ?? undefined, |
| 74 | 100 |
cache: 'no-store', |
| 75 | 101 |
}); |
... | ... | @@ -83,29 +109,10 @@ |
| 83 | 109 |
throw new Error('관리자 회원 목록 응답의 형식이 올바르지 않습니다.');
|
| 84 | 110 |
} |
| 85 | 111 |
|
| 86 |
- return data.list.map(toAdminMember); |
|
| 87 |
-} |
|
| 112 |
+ const totalCount = |
|
| 113 |
+ typeof data.totalCount === 'number' ? data.totalCount : data.list.length; |
|
| 88 | 114 |
|
| 89 |
-const SEARCH_VALUE_BY_FIELD: Record< |
|
| 90 |
- AdminMemberSearchField, |
|
| 91 |
- (member: AdminMember) => string |
|
| 92 |
-> = {
|
|
| 93 |
- name: (member) => member.name, |
|
| 94 |
- loginId: (member) => member.loginId, |
|
| 95 |
- phoneNumber: (member) => member.phoneNumber ?? '', |
|
| 96 |
-}; |
|
| 97 |
- |
|
| 98 |
-function filterByKeyword( |
|
| 99 |
- items: AdminMember[], |
|
| 100 |
- query: AdminMemberQuery |
|
| 101 |
-): AdminMember[] {
|
|
| 102 |
- const keyword = query.keyword.trim().toLowerCase(); |
|
| 103 |
- if (!keyword) {
|
|
| 104 |
- return items; |
|
| 105 |
- } |
|
| 106 |
- |
|
| 107 |
- const readValue = SEARCH_VALUE_BY_FIELD[query.searchField]; |
|
| 108 |
- return items.filter((item) => readValue(item).toLowerCase().includes(keyword)); |
|
| 115 |
+ return { items: data.list.map(toAdminMember), totalCount };
|
|
| 109 | 116 |
} |
| 110 | 117 |
|
| 111 | 118 |
function sortItems( |
... | ... | @@ -123,18 +130,37 @@ |
| 123 | 130 |
totalCount: number; |
| 124 | 131 |
}; |
| 125 | 132 |
|
| 133 |
+/** |
|
| 134 |
+ * 검색·페이징은 백엔드가 한다 — 매퍼가 `searchCondition`으로 컬럼을 고르고 별도 카운트 |
|
| 135 |
+ * 쿼리로 전체 건수를 센다. |
|
| 136 |
+ * |
|
| 137 |
+ * **이름순만 예외다.** 백엔드에 정렬 파라미터가 없어 그때는 전체를 받아 여기서 정렬하고 |
|
| 138 |
+ * 자른다. 정렬이 붙으면 이 갈래를 지우면 된다. |
|
| 139 |
+ */ |
|
| 126 | 140 |
export async function fetchAdminMembers( |
| 127 | 141 |
query: AdminMemberQuery |
| 128 | 142 |
): Promise<AdminMemberPage> {
|
| 129 |
- const all = await fetchAllAdminMembers(); |
|
| 130 |
- const matched = sortItems(filterByKeyword(all, query), query); |
|
| 143 |
+ const search = toSearchParams(query); |
|
| 131 | 144 |
|
| 132 |
- const offset = (query.page - 1) * query.pageSize; |
|
| 145 |
+ if (query.sort === 'name') {
|
|
| 146 |
+ const { items } = await requestPage({
|
|
| 147 |
+ pageIndex: 1, |
|
| 148 |
+ recordCountPerPage: ADMIN_MEMBER_FETCH_LIMIT, |
|
| 149 |
+ ...search, |
|
| 150 |
+ }); |
|
| 151 |
+ const sorted = sortItems(items, query); |
|
| 152 |
+ const offset = (query.page - 1) * query.pageSize; |
|
| 153 |
+ return {
|
|
| 154 |
+ items: sorted.slice(offset, offset + query.pageSize), |
|
| 155 |
+ totalCount: sorted.length, |
|
| 156 |
+ }; |
|
| 157 |
+ } |
|
| 133 | 158 |
|
| 134 |
- return {
|
|
| 135 |
- items: matched.slice(offset, offset + query.pageSize), |
|
| 136 |
- totalCount: matched.length, |
|
| 137 |
- }; |
|
| 159 |
+ return requestPage({
|
|
| 160 |
+ pageIndex: query.page, |
|
| 161 |
+ recordCountPerPage: query.pageSize, |
|
| 162 |
+ ...search, |
|
| 163 |
+ }); |
|
| 138 | 164 |
} |
| 139 | 165 |
|
| 140 | 166 |
export async function findAdminMemberById( |
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?