File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import 'server-only';
import { getSessionAccessToken } from '@/lib/auth/dal';
import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch';
import type { AdminMember } from '@/lib/domain/admin-member';
import type {
AdminMemberQuery,
AdminMemberSearchField,
} from '@/lib/domain/admin-member-query';
const ADMIN_MEMBER_BASE_PATH = '/api/v1/mngr/admin';
const ADMIN_MEMBER_PAGINATION_PATH = `${ADMIN_MEMBER_BASE_PATH}/pagination`;
/**
* 검색 대상 → 백엔드 `searchCondition`. 매퍼가 이 값으로 컬럼을 고른다
* (1=USER_NM · 2=LOGIN_ID · 3=USER_TEL_NO).
*/
const SEARCH_CONDITION_BY_FIELD: Record<AdminMemberSearchField, string> = {
name: '1',
loginId: '2',
phoneNumber: '3',
};
/**
* 이름순 정렬에만 쓰는 상한. 백엔드에 정렬 파라미터가 없어 그때만 전체를 받아 여기서
* 정렬하고 자른다 — 검색·페이징은 백엔드가 한다.
*/
const ADMIN_MEMBER_FETCH_LIMIT = 10_000;
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object';
}
function readRequiredString(
source: Record<string, unknown>,
key: string
): string {
const value = source[key];
if (typeof value !== 'string' || value.length === 0) {
throw new Error(`관리자 회원 응답에 ${key}가 없습니다.`);
}
return value;
}
function readOptionalString(
source: Record<string, unknown>,
key: string
): string | null {
const value = source[key];
return typeof value === 'string' && value.length > 0 ? value : null;
}
function parseYesNo(value: unknown): boolean | null {
if (value === 'Y') return true;
if (value === 'N') return false;
return null;
}
function toAdminMember(raw: unknown): AdminMember {
if (!isRecord(raw)) {
throw new Error('관리자 회원 응답 항목의 형식이 올바르지 않습니다.');
}
const loginFailCnt = raw.loginFailCnt;
return {
id: readRequiredString(raw, 'userId'),
name: readRequiredString(raw, 'userNm'),
loginId: readRequiredString(raw, 'loginId'),
phoneNumber: readOptionalString(raw, 'userTelNo'),
email: readOptionalString(raw, 'userEmlAddr'),
roleCode: readOptionalString(raw, 'roleId') ?? '',
createdAt: readOptionalString(raw, 'frstRegDtStr'),
isLocked: parseYesNo(raw.acctLockYn),
isActive: parseYesNo(raw.useYn),
loginFailCount: typeof loginFailCnt === 'number' ? loginFailCnt : null,
};
}
/** 검색 조건을 백엔드 파라미터로 옮긴다. 검색어가 비면 조건 자체를 보내지 않는다. */
function toSearchParams(query: AdminMemberQuery): Record<string, string> {
const keyword = query.keyword.trim();
if (!keyword) {
return {};
}
return {
searchCondition: SEARCH_CONDITION_BY_FIELD[query.searchField],
searchKeyword: keyword,
};
}
async function requestPage(
params: Record<string, string | number>
): Promise<{ items: AdminMember[]; totalCount: number }> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<unknown>(ADMIN_MEMBER_PAGINATION_PATH, {
method: 'GET',
query: params,
accessToken: accessToken ?? undefined,
cache: 'no-store',
});
if (!result.ok) {
throw new BackendRequestError(result);
}
const data = result.data;
if (!isRecord(data) || !Array.isArray(data.list)) {
throw new Error('관리자 회원 목록 응답의 형식이 올바르지 않습니다.');
}
const totalCount =
typeof data.totalCount === 'number' ? data.totalCount : data.list.length;
return { items: data.list.map(toAdminMember), totalCount };
}
function sortItems(
items: AdminMember[],
query: AdminMemberQuery
): AdminMember[] {
if (query.sort !== 'name') {
return items;
}
return [...items].sort((a, b) => a.name.localeCompare(b.name, 'ko'));
}
export type AdminMemberPage = {
items: AdminMember[];
totalCount: number;
};
/**
* 검색·페이징은 백엔드가 한다 — 매퍼가 `searchCondition`으로 컬럼을 고르고 별도 카운트
* 쿼리로 전체 건수를 센다.
*
* **이름순만 예외다.** 백엔드에 정렬 파라미터가 없어 그때는 전체를 받아 여기서 정렬하고
* 자른다. 정렬이 붙으면 이 갈래를 지우면 된다.
*/
export async function fetchAdminMembers(
query: AdminMemberQuery
): Promise<AdminMemberPage> {
const search = toSearchParams(query);
if (query.sort === 'name') {
const { items } = await requestPage({
pageIndex: 1,
recordCountPerPage: ADMIN_MEMBER_FETCH_LIMIT,
...search,
});
const sorted = sortItems(items, query);
const offset = (query.page - 1) * query.pageSize;
return {
items: sorted.slice(offset, offset + query.pageSize),
totalCount: sorted.length,
};
}
return requestPage({
pageIndex: query.page,
recordCountPerPage: query.pageSize,
...search,
});
}
export async function findAdminMemberById(
id: string
): Promise<AdminMember | null> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<unknown>(
`${ADMIN_MEMBER_BASE_PATH}/${encodeURIComponent(id)}`,
{
method: 'GET',
accessToken: accessToken ?? undefined,
cache: 'no-store',
canHaveNullData: true,
}
);
if (!result.ok) {
throw new BackendRequestError(result);
}
return result.data === null ? null : toAdminMember(result.data);
}
export async function isAdminLoginIdTaken(loginId: string): Promise<boolean> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<unknown>(
`${ADMIN_MEMBER_BASE_PATH}/duplication/${encodeURIComponent(loginId.trim())}`,
{
method: 'GET',
accessToken: accessToken ?? undefined,
cache: 'no-store',
canHaveNullData: true,
}
);
if (!result.ok) {
throw new BackendRequestError(result);
}
return result.data !== null;
}
export type CreateAdminMemberInput = {
name: string;
loginId: string;
password: string;
phoneNumber: string;
email: string;
roleCode: string;
};
export type UpdateAdminMemberInput = {
password: string;
phoneNumber: string;
email: string;
roleCode: string;
isLocked: boolean;
};
function toYesNo(value: boolean): string {
return value ? 'Y' : 'N';
}
export async function createAdminMember(
input: CreateAdminMemberInput
): Promise<void> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<unknown>(ADMIN_MEMBER_BASE_PATH, {
method: 'POST',
// 이슈: 등록은 form(@ParameterObject), 수정은 JSON(@RequestBody)로 갈려 있다.
form: {
userNm: input.name,
loginId: input.loginId,
loginPw: input.password,
userEmlAddr: input.email,
userTelNo: input.phoneNumber,
roleId: input.roleCode,
},
accessToken: accessToken ?? undefined,
canHaveNullData: true,
});
if (!result.ok) {
throw new BackendRequestError(result);
}
}
export async function updateAdminMember(
id: string,
input: UpdateAdminMemberInput
): Promise<void> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<unknown>(
`${ADMIN_MEMBER_BASE_PATH}/${encodeURIComponent(id)}`,
{
method: 'PUT',
body: {
loginPw: input.password,
userEmlAddr: input.email,
userTelNo: input.phoneNumber,
roleId: input.roleCode,
acctLockYn: toYesNo(input.isLocked),
},
accessToken: accessToken ?? undefined,
canHaveNullData: true,
}
);
if (!result.ok) {
throw new BackendRequestError(result);
}
}
export async function deleteAdminMember(id: string): Promise<void> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<unknown>(
`${ADMIN_MEMBER_BASE_PATH}/${encodeURIComponent(id)}`,
{
method: 'DELETE',
accessToken: accessToken ?? undefined,
canHaveNullData: true,
}
);
if (!result.ok) {
throw new BackendRequestError(result);
}
}