File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
/**
* 콘텐츠관리 목록의 필터·검색·정렬·페이징 조건 — 세 화면이 같은 규칙을 쓴다.
* URL(searchParams)이 단일 진실원천이며 next/react 의존이 없다.
*/
import { CONTENT_KIND_LABEL, type ContentKind } from '@/lib/domain/content';
export const CONTENT_PATH: Record<ContentKind, string> = {
shorts: '/contents/shorts',
quiz: '/contents/quizzes',
comic: '/contents/comics',
};
export function contentListPath(kind: ContentKind): string {
return CONTENT_PATH[kind];
}
export function contentCreatePath(kind: ContentKind): string {
return `${CONTENT_PATH[kind]}/new`;
}
export function contentEditPath(kind: ContentKind, id: string): string {
return `${CONTENT_PATH[kind]}/${encodeURIComponent(id)}`;
}
export function contentBreadcrumb(
kind: ContentKind,
leaf?: string
): Array<{ label: string; href?: string }> {
const trail: Array<{ label: string; href?: string }> = [
{ label: '홈', href: '/' },
{ label: '콘텐츠관리' },
];
if (leaf) {
trail.push({ label: CONTENT_KIND_LABEL[kind], href: CONTENT_PATH[kind] });
trail.push({ label: leaf });
} else {
trail.push({ label: CONTENT_KIND_LABEL[kind] });
}
return trail;
}
export type ContentSortOption = 'created' | 'modified';
export const CONTENT_SORT_OPTIONS: ReadonlyArray<{
value: ContentSortOption;
label: string;
}> = [
{ value: 'created', label: '최신등록일순' },
{ value: 'modified', label: '최신수정일순' },
];
/** 기획 "리스트 — 최신 등록일 순으로 정렬". */
export const DEFAULT_CONTENT_SORT: ContentSortOption = 'created';
export const CONTENT_PAGE_SIZE_OPTIONS = [10, 30, 50] as const;
export type ContentPageSize = (typeof CONTENT_PAGE_SIZE_OPTIONS)[number];
export const DEFAULT_CONTENT_PAGE_SIZE: ContentPageSize = 10;
export type ContentSearchFieldOption = { value: string; label: string };
/** 검색 대상 — 기획의 화면별 문구 그대로. */
export const CONTENT_SEARCH_FIELDS: Record<
ContentKind,
readonly ContentSearchFieldOption[]
> = {
shorts: [
{ value: 'title', label: '제목' },
{ value: 'keyword', label: '키워드' },
],
// 백엔드 목록 SQL이 이름이 바뀌기 전 컬럼(QUIZ_ANSWER·QUIZ_QUESTION)을 보고 있어, 검색어를
// 실으면 목록 조회가 SQL 오류로 죽는다. 고쳐질 때까지 감춘다(사용자 확정).
quiz: [],
comic: [
{ value: 'name', label: '콘텐츠명' },
{ value: 'keyword', label: '키워드' },
],
};
const DEFAULT_PAGE = 1;
const MAX_KEYWORD_LENGTH = 100;
export type ContentQuery = {
/** 공통코드 `COM_SCHUL_CD` 코드값. 빈 값이면 전체. */
school: string;
/** 공통코드 `GRD_CD` 코드값. 빈 값이면 전체. */
grade: string;
/** 금융OX퀴즈 전용 난이도 필터. 다른 화면에서는 늘 빈 값이다. */
level: string;
searchField: string;
keyword: string;
sort: ContentSortOption;
page: number;
pageSize: ContentPageSize;
};
type RawSearchParams = Record<string, string | string[] | undefined>;
function readParam(params: RawSearchParams, key: string): string {
const value = params[key];
const raw = Array.isArray(value) ? value[0] : value;
return (raw ?? '').trim();
}
export function parseContentQuery(
kind: ContentKind,
searchParams: RawSearchParams
): ContentQuery {
const fields = CONTENT_SEARCH_FIELDS[kind];
const searchField = readParam(searchParams, 'searchField');
const sort = readParam(searchParams, 'sort');
const page = Number(readParam(searchParams, 'page'));
const pageSize = Number(readParam(searchParams, 'pageSize'));
return {
school: readParam(searchParams, 'school'),
grade: readParam(searchParams, 'grade'),
level: kind === 'quiz' ? readParam(searchParams, 'level') : '',
searchField: fields.some((field) => field.value === searchField)
? searchField
: fields[0].value,
keyword: readParam(searchParams, 'keyword').slice(0, MAX_KEYWORD_LENGTH),
sort: sort === 'modified' ? 'modified' : DEFAULT_CONTENT_SORT,
page: Number.isInteger(page) && page > 0 ? page : DEFAULT_PAGE,
pageSize: (CONTENT_PAGE_SIZE_OPTIONS as readonly number[]).includes(pageSize)
? (pageSize as ContentPageSize)
: DEFAULT_CONTENT_PAGE_SIZE,
};
}
export function buildContentHref(
kind: ContentKind,
query: ContentQuery,
overrides: Partial<ContentQuery> = {}
): string {
const merged = { ...query, ...overrides };
const params = new URLSearchParams();
const defaultField = CONTENT_SEARCH_FIELDS[kind][0].value;
if (merged.school) params.set('school', merged.school);
if (merged.grade) params.set('grade', merged.grade);
if (merged.level) params.set('level', merged.level);
if (merged.searchField !== defaultField) {
params.set('searchField', merged.searchField);
}
if (merged.keyword) params.set('keyword', merged.keyword);
if (merged.sort !== DEFAULT_CONTENT_SORT) params.set('sort', merged.sort);
if (merged.pageSize !== DEFAULT_CONTENT_PAGE_SIZE) {
params.set('pageSize', String(merged.pageSize));
}
if (merged.page !== DEFAULT_PAGE) params.set('page', String(merged.page));
const queryString = params.toString();
return queryString
? `${CONTENT_PATH[kind]}?${queryString}`
: CONTENT_PATH[kind];
}