/** * 콘텐츠관리 목록의 필터·검색·정렬·페이징 조건 — 세 화면이 같은 규칙을 쓴다. * URL(searchParams)이 단일 진실원천이며 next/react 의존이 없다. */ import { CONTENT_KIND_LABEL, type ContentKind } from '@/lib/domain/content'; export const CONTENT_PATH: Record = { 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; 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 = {} ): string { const merged = { ...query, ...overrides }; const params = new URLSearchParams(); // 검색 대상이 없는 화면(금융OX퀴즈)은 기본값도 없다 — 그때는 주소에 싣지 않는다. 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 && 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]; }