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`;
// 이슈: 백엔드 `totalCount`가 전체가 아니라 현재 페이지 행 수라 페이징을 맡길 수 없다.
// 이름순 정렬 파라미터도 없어 전체를 받아 서버에서 자른다.
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,
};
}
async function fetchAllAdminMembers(): Promise<AdminMember[]> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<unknown>(ADMIN_MEMBER_PAGINATION_PATH, {
method: 'GET',
query: { pageIndex: 1, recordCountPerPage: ADMIN_MEMBER_FETCH_LIMIT },
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('관리자 회원 목록 응답의 형식이 올바르지 않습니다.');
}
return data.list.map(toAdminMember);
}
const SEARCH_VALUE_BY_FIELD: Record<
AdminMemberSearchField,
(member: AdminMember) => string
> = {
name: (member) => member.name,
loginId: (member) => member.loginId,
phoneNumber: (member) => member.phoneNumber ?? '',
};
function filterByKeyword(
items: AdminMember[],
query: AdminMemberQuery
): AdminMember[] {
const keyword = query.keyword.trim().toLowerCase();
if (!keyword) {
return items;
}
const readValue = SEARCH_VALUE_BY_FIELD[query.searchField];
return items.filter((item) => readValue(item).toLowerCase().includes(keyword));
}
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;
};
export async function fetchAdminMembers(
query: AdminMemberQuery
): Promise<AdminMemberPage> {
const all = await fetchAllAdminMembers();
const matched = sortItems(filterByKeyword(all, query), query);
const offset = (query.page - 1) * query.pageSize;
return {
items: matched.slice(offset, offset + query.pageSize),
totalCount: matched.length,
};
}
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);
}
}