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 }));
}
// 검색 초기화 — 주소를 되돌리는 것만으론 아직 적용 안 한 입력값이 남는다. 목록 컨테이너를
// 다시 마운트해 검색 상자의 초안까지 비운다.
const [resetCount, setResetCount] = useState(0);
function resetSearch() {
setSchoolNameDraft('');
setResetCount((count) => count + 1);
router.replace(POINT_ACCOUNTS_PATH);
}
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: '아이디', align: 'start', width: 140 },
{ key: 'studentName', header: '학생명', align: 'start', width: 120, emphasis: true },
{ key: 'schoolName', header: '학교명', align: 'start', 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>
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="페이지 크기"
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={resetSearch} />
}
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)}
/>
)}
</>
);
}