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, type ReactNode } from 'react';
import { FoxButton } from '@fox/core/components/fox-button';
import { FoxButtonGroup } from '@fox/core/components/fox-button-group';
import { FoxSelectText } from '@fox/core/components/fox-select-text';
import {
FoxListContainer,
type FoxListColumn,
} from '@fox/core/components/fox-list-container';
import { CONTENT_KIND_LABEL, type ContentKind } from '@/lib/domain/content';
import {
CONTENT_PAGE_SIZE_OPTIONS,
CONTENT_SEARCH_FIELDS,
CONTENT_SORT_OPTIONS,
buildContentHref,
contentBreadcrumb,
contentCreatePath,
contentListPath,
type ContentPageSize,
type ContentQuery,
type ContentSortOption,
} from '@/lib/domain/content-query';
import { ContentListFilters } from './content-list-filters';
import type { CommonCode } from '@/lib/domain/common-code';
import type { GradesBySchool } from '@/lib/domain/content';
interface ContentListShellProps<T> {
kind: ContentKind;
columns: FoxListColumn<T>[];
rows: T[];
rowKey: (row: T) => string;
query: ContentQuery;
currentPage: number;
totalPages: number;
totalCount: number;
schools: readonly CommonCode[];
gradesBySchool: GradesBySchool;
/** 툴바 오른쪽에 덧붙일 것(금융OX퀴즈의 엑셀 버튼). */
extraActions?: ReactNode;
}
/** 콘텐츠관리 목록 3종이 공유하는 껍데기 — 열만 화면마다 다르다. */
export function ContentListShell<T>({
kind,
columns,
rows,
rowKey,
query,
currentPage,
totalPages,
totalCount,
schools,
gradesBySchool,
extraActions,
}: ContentListShellProps<T>) {
const router = useRouter();
function go(patch: Partial<ContentQuery>) {
router.push(buildContentHref(kind, query, patch));
}
// 검색 초기화 — 주소를 되돌리는 것만으론 아직 적용 안 한 입력값이 남는다. 목록 컨테이너를
// 다시 마운트해 검색 상자의 초안까지 비운다.
const [resetCount, setResetCount] = useState(0);
function resetSearch() {
setResetCount((count) => count + 1);
router.replace(contentListPath(kind));
}
return (
<FoxListContainer<T>
key={resetCount}
title={`콘텐츠 목록 (${CONTENT_KIND_LABEL[kind]})`}
breadcrumb={contentBreadcrumb(kind)}
caption={`${CONTENT_KIND_LABEL[kind]} 콘텐츠 목록`}
columns={columns}
rows={rows}
rowKey={rowKey}
totalCount={totalCount}
emptyMessage="등록된 콘텐츠가 없습니다."
filter={
<ContentListFilters
query={query}
schools={schools}
gradesBySchool={gradesBySchool}
// 백엔드가 searchLevelCd를 받기만 하고 조건에 쓰지 않아, 골라도 목록이 그대로다.
// SQL에 조건이 생기면 `kind === 'quiz'`로 되돌린다.
withLevel={false}
onChange={go}
/>
}
sorts={
<>
<FoxSelectText
size="sm"
ariaLabel="정렬"
icon={<i className="fox-ico fox-ico-ArrowsDownUp" aria-hidden="true" />}
options={CONTENT_SORT_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
value={query.sort}
onValueChange={(value) => go({ sort: value as ContentSortOption })}
/>
<FoxSelectText
size="sm"
ariaLabel="페이지 크기"
options={CONTENT_PAGE_SIZE_OPTIONS.map((size) => ({
value: String(size),
label: `${size}개씩 보기`,
}))}
value={String(query.pageSize)}
onValueChange={(value) =>
go({ pageSize: Number(value) as ContentPageSize, page: 1 })
}
/>
</>
}
searchHidden={CONTENT_SEARCH_FIELDS[kind].length === 0}
searchFields={CONTENT_SEARCH_FIELDS[kind].map((field) => ({
value: field.value,
label: field.label,
}))}
searchField={query.searchField}
keyword={query.keyword}
searchPlaceholder="검색어를 입력하세요."
toolbarActions={
<FoxButtonGroup size="md">
<FoxButton type="default" size="md" label="검색 초기화" onAction={resetSearch} />
{extraActions}
<FoxButton
type="primary"
size="md"
leadingIcon={<i className="fox-ico fox-ico-Plus" aria-hidden="true" />}
label="등록"
onAction={() => router.push(contentCreatePath(kind))}
/>
</FoxButtonGroup>
}
page={currentPage}
totalPages={totalPages}
buildHref={(patch) => buildContentHref(kind, query, patch as Partial<ContentQuery>)}
onQueryChange={go}
/>
);
}