File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { FoxBadge, type FoxBadgeColor } from '@fox/core/components/fox-badge';
import { FoxButton } from '@fox/core/components/fox-button';
import { FoxButtonGroup } from '@fox/core/components/fox-button-group';
import {
FoxListContainer,
type FoxListColumn,
} from '@fox/core/components/fox-list-container';
import { FoxSelectText } from '@fox/core/components/fox-select-text';
import { FoxStatusIndicator } from '@fox/core/components/fox-status-indicator';
import {
formatAdminLockStatusLabel,
formatAdminRoleLabel,
formatOptionalValue,
type AdminMember,
} from '@/lib/domain/admin-member';
import {
ADMIN_MEMBERS_EXCEL_PATH,
ADMIN_MEMBERS_PATH,
ADMIN_MEMBER_PAGE_SIZE_OPTIONS,
ADMIN_MEMBER_SEARCH_FIELD_OPTIONS,
ADMIN_MEMBER_SORT_OPTIONS,
buildAdminMemberHref,
type AdminMemberPageSize,
type AdminMemberQuery,
type AdminMemberSearchField,
type AdminMemberSortOption,
} from '@/lib/domain/admin-member-query';
import { AdminMemberCreateModal } from './admin-member-create-modal';
import { AdminMemberRowActions } from './admin-member-row-actions';
interface AdminMemberListProps {
items: AdminMember[];
query: AdminMemberQuery;
currentPage: number;
totalPages: number;
totalCount: number;
currentAdminId: string;
}
const FILL_COLUMN_WIDTH = 277;
const BADGE_COLOR_BY_ROLE: Record<string, FoxBadgeColor> = {
ROLE_SYSTEM: 'accent',
ROLE_ADMIN: 'primary',
};
export function AdminMemberList({
items,
query,
currentPage,
totalPages,
totalCount,
currentAdminId,
}: AdminMemberListProps) {
const router = useRouter();
const [isCreateOpen, setIsCreateOpen] = useState(false);
function go(patch: Partial<AdminMemberQuery>) {
router.replace(buildAdminMemberHref(query, { ...patch, page: 1 }));
}
// 검색 초기화 — 주소를 되돌리는 것만으론 아직 적용 안 한 입력값이 남는다. 목록 컨테이너를
// 다시 마운트해 검색 상자의 초안까지 비운다.
const [resetCount, setResetCount] = useState(0);
function resetSearch() {
setResetCount((count) => count + 1);
router.replace(ADMIN_MEMBERS_PATH);
}
const columns: FoxListColumn<AdminMember>[] = [
{
key: 'no',
header: '번호',
width: 80,
render: (_row, index) => (currentPage - 1) * query.pageSize + index + 1,
},
{ key: 'name', header: '이름', align: 'start', width: FILL_COLUMN_WIDTH },
{ key: 'loginId', header: '아이디', align: 'start', width: FILL_COLUMN_WIDTH },
{
key: 'phoneNumber',
header: '휴대전화번호',
align: 'start',
width: 200,
render: (row) => formatOptionalValue(row.phoneNumber),
},
{
key: 'email',
header: '이메일',
align: 'start',
width: FILL_COLUMN_WIDTH,
render: (row) => formatOptionalValue(row.email),
},
{
key: 'role',
header: '역할',
width: 120,
render: (row) => (
<FoxBadge
type="pastel"
color={BADGE_COLOR_BY_ROLE[row.roleCode] ?? 'neutral'}
size="md"
>
{formatAdminRoleLabel(row.roleCode)}
</FoxBadge>
),
},
{
key: 'isLocked',
header: '잠김여부',
width: 120,
render: (row) =>
row.isLocked === null ? (
formatAdminLockStatusLabel(row.isLocked)
) : (
<FoxStatusIndicator
type={row.isLocked ? 'danger' : 'success'}
label={formatAdminLockStatusLabel(row.isLocked)}
/>
),
},
{
key: 'createdAt',
header: '생성일',
width: 160,
render: (row) => formatOptionalValue(row.createdAt),
},
{
key: 'actions',
header: '관리',
width: 160,
render: (row) => (
<AdminMemberRowActions
member={row}
isSelf={row.id === currentAdminId}
/>
),
},
];
return (
<>
<FoxListContainer<AdminMember>
key={resetCount}
title="관리자 회원 목록"
breadcrumb={[
{ label: '홈', href: '/' },
{ label: '회원관리' },
{ label: '관리자 회원' },
]}
caption="관리자 회원 목록"
columns={columns}
rows={items}
rowKey={(row) => row.id}
totalCount={totalCount}
emptyMessage="검색 결과가 없습니다. 다른 검색어로 다시 시도해 주세요."
sorts={
<>
<FoxSelectText
size="sm"
ariaLabel="정렬"
icon={<i className="fox-ico fox-ico-ArrowsDownUp" aria-hidden="true" />}
options={ADMIN_MEMBER_SORT_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
value={query.sort}
onValueChange={(value) => go({ sort: value as AdminMemberSortOption })}
/>
<FoxSelectText
size="sm"
ariaLabel="페이지 크기"
options={ADMIN_MEMBER_PAGE_SIZE_OPTIONS.map((size) => ({
value: String(size),
label: `${size}개씩 보기`,
}))}
value={String(query.pageSize)}
onValueChange={(value) =>
go({ pageSize: Number(value) as AdminMemberPageSize })
}
/>
</>
}
searchFields={ADMIN_MEMBER_SEARCH_FIELD_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
searchField={query.searchField}
keyword={query.keyword}
toolbarActions={
<FoxButtonGroup size="md">
<FoxButton type="default" size="md" label="검색 초기화" onAction={resetSearch} />
<form action={ADMIN_MEMBERS_EXCEL_PATH} method="get">
<FoxButton
type="secondary"
size="md"
htmlType="submit"
leadingIcon={<i className="fox-ico fox-ico-DownloadSimple" aria-hidden="true" />}
label="엑셀 다운로드"
/>
</form>
<FoxButton
type="primary"
size="md"
leadingIcon={<i className="fox-ico fox-ico-Plus" aria-hidden="true" />}
label="신규 등록"
onAction={() => setIsCreateOpen(true)}
/>
</FoxButtonGroup>
}
page={currentPage}
totalPages={totalPages}
buildHref={(patch) =>
buildAdminMemberHref(query, { page: Number(patch.page ?? 1) })
}
onQueryChange={(patch) => {
if (patch.keyword !== undefined || patch.searchField !== undefined) {
go({
keyword: String(patch.keyword ?? ''),
searchField: (patch.searchField ??
query.searchField) as AdminMemberSearchField,
});
}
}}
/>
{isCreateOpen && (
<AdminMemberCreateModal onClose={() => setIsCreateOpen(false)} />
)}
</>
);
}