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
File name
Commit message
Commit date
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { FoxButton } from '@fox/core/components/fox-button';
import { FoxInput } from '@fox/core/components/fox-input';
import {
FoxListContainer,
type FoxListColumn,
} from '@fox/core/components/fox-list-container';
import { FoxSelect } from '@fox/core/components/fox-select';
import { FoxSelectText } from '@fox/core/components/fox-select-text';
import { FoxIconButton } from '@fox/core/components/fox-icon-button';
import {
formatOptionalValue,
formatPoint,
type PointAccount,
} from '@/lib/domain/point-account';
import {
CLASS_OPTIONS,
FILTER_ALL,
GRADE_OPTIONS,
NUMBER_OPTIONS,
POINT_ACCOUNTS_PATH,
POINT_ACCOUNT_PAGE_SIZE_OPTIONS,
POINT_ACCOUNT_SEARCH_FIELD_OPTIONS,
buildPointAccountHref,
type PointAccountPageSize,
type PointAccountQuery,
type PointAccountSearchField,
} from '@/lib/domain/point-account-query';
import { PointDetailModal } from './point-detail-modal';
import styles from './point-account-filter.module.scss';
interface PointAccountListProps {
items: PointAccount[];
query: PointAccountQuery;
currentPage: number;
totalPages: number;
totalCount: number;
}
function numberOptions(values: readonly number[], suffix: string) {
return [
{ value: FILTER_ALL, label: `${suffix} 전체` },
...values.map((value) => ({
value: String(value),
label: `${value}${suffix}`,
})),
];
}
export function PointAccountList({
items,
query,
currentPage,
totalPages,
totalCount,
}: PointAccountListProps) {
const router = useRouter();
const [detailAccount, setDetailAccount] = useState<PointAccount | null>(null);
const [schoolNameDraft, setSchoolNameDraft] = useState(query.schoolName);
function go(patch: Partial<PointAccountQuery>) {
router.replace(buildPointAccountHref(query, { ...patch, page: 1 }));
}
function selectedNumber(value: number | null) {
return value === null ? FILTER_ALL : String(value);
}
function parseSelected(value: string): number | null {
return value === FILTER_ALL ? null : Number(value);
}
const columns: FoxListColumn<PointAccount>[] = [
{
key: 'no',
header: '번호',
width: 80,
render: (_row, index) => (currentPage - 1) * query.pageSize + index + 1,
},
{ key: 'loginId', header: '아이디', width: 140 },
{ key: 'studentName', header: '학생명', width: 120, emphasis: true },
{ key: 'schoolName', header: '학교명', width: 160 },
{ key: 'grade', header: '학년', width: 80, align: 'center' },
{ key: 'classNo', header: '반', width: 80, align: 'center' },
{ key: 'studentNo', header: '번호', width: 80, align: 'center' },
{
key: 'totalPoint',
header: '총 포인트',
width: 120,
align: 'end',
render: (row) => formatPoint(row.totalPoint),
},
{
key: 'deductedPoint',
header: '차감 포인트',
width: 120,
align: 'end',
render: (row) => formatPoint(row.deductedPoint),
},
{
key: 'usedPoint',
header: '사용 포인트',
width: 120,
align: 'end',
render: (row) => formatPoint(row.usedPoint),
},
{
key: 'currentPoint',
header: '현재 포인트',
width: 120,
align: 'end',
render: (row) => formatPoint(row.currentPoint),
},
{
key: 'updatedAt',
header: '수정일시',
width: 160,
render: (row) => formatOptionalValue(row.updatedAt),
},
{ key: 'siteName', header: '구분(사이트)', width: 160 },
{
key: 'actions',
header: '관리',
width: 100,
render: (row) => (
<FoxIconButton
type="default"
size="sm"
icon={<i className="fox-ico fox-ico-ArrowUpRight" aria-hidden="true" />}
label={`${row.studentName} 포인트 상세내역`}
onAction={() => setDetailAccount(row)}
/>
),
},
];
return (
<>
<FoxListContainer<PointAccount>
title="누적포인트 관리 목록"
breadcrumb={[
{ label: '홈', href: '/' },
{ label: '포인트관리' },
{ label: '누적포인트 관리' },
]}
caption="누적포인트 관리 목록"
columns={columns}
rows={items}
rowKey={(row) => row.id}
totalCount={totalCount}
emptyMessage="검색 결과가 없습니다. 다른 검색어로 다시 시도해 주세요."
sorts={
<FoxSelectText
size="sm"
ariaLabel="페이지 크기"
options={POINT_ACCOUNT_PAGE_SIZE_OPTIONS.map((size) => ({
value: String(size),
label: `${size}개씩 보기`,
}))}
value={String(query.pageSize)}
onValueChange={(value) =>
go({ pageSize: Number(value) as PointAccountPageSize })
}
/>
}
filter={
<div className={styles.filter}>
<div className={styles.school}>
<FoxInput
size="md"
aria-label="학교명"
placeholder="학교를 입력하세요."
value={schoolNameDraft}
onChange={setSchoolNameDraft}
onKeyDown={(event) => {
if (event.key === 'Enter') {
go({ schoolName: schoolNameDraft });
}
}}
/>
</div>
<div className={styles.select}>
<FoxSelect
size="md"
ariaLabel="학년"
options={numberOptions(GRADE_OPTIONS, '학년')}
value={selectedNumber(query.grade)}
onValueChange={(value) => go({ grade: parseSelected(value) })}
/>
</div>
<div className={styles.select}>
<FoxSelect
size="md"
ariaLabel="반"
options={numberOptions(CLASS_OPTIONS, '반')}
value={selectedNumber(query.classNo)}
onValueChange={(value) => go({ classNo: parseSelected(value) })}
/>
</div>
<div className={styles.select}>
<FoxSelect
size="md"
ariaLabel="번호"
options={numberOptions(NUMBER_OPTIONS, '번')}
value={selectedNumber(query.studentNo)}
onValueChange={(value) => go({ studentNo: parseSelected(value) })}
/>
</div>
</div>
}
searchFields={POINT_ACCOUNT_SEARCH_FIELD_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
searchField={query.searchField}
keyword={query.keyword}
searchPlaceholder="검색어를 입력하세요."
toolbarActions={
<FoxButton
type="default"
size="md"
label="초기화"
onAction={() => {
setSchoolNameDraft('');
router.replace(POINT_ACCOUNTS_PATH);
}}
/>
}
page={currentPage}
totalPages={totalPages}
buildHref={(patch) =>
buildPointAccountHref(query, { page: Number(patch.page ?? 1) })
}
onQueryChange={(patch) => {
if (patch.keyword !== undefined || patch.searchField !== undefined) {
go({
schoolName: schoolNameDraft,
keyword: String(patch.keyword ?? ''),
searchField: (patch.searchField ??
query.searchField) as PointAccountSearchField,
});
}
}}
/>
{detailAccount && (
<PointDetailModal
account={detailAccount}
onClose={() => setDetailAccount(null)}
/>
)}
</>
);
}