import { MERCHANT_DETAIL_VISIBILITY_OPTIONS } from '@/lib/domain/merchant-detail'; export const MERCHANT_DETAILS_PATH = '/system/merchant-details'; export const MERCHANT_DETAIL_NEW_PATH = `${MERCHANT_DETAILS_PATH}/new`; export function merchantDetailPath(frcsNo: string): string { return `${MERCHANT_DETAILS_PATH}/${encodeURIComponent(frcsNo)}`; } export function merchantProductNewPath(frcsNo: string): string { return `${merchantDetailPath(frcsNo)}/products/new`; } export function merchantProductPath(frcsNo: string, productSn: string): string { return `${merchantDetailPath(frcsNo)}/products/${encodeURIComponent(productSn)}`; } export const FILTER_ALL = ''; export const MERCHANT_DETAIL_PAGE_SIZE_OPTIONS = [10, 30, 50] as const; export type MerchantDetailPageSize = (typeof MERCHANT_DETAIL_PAGE_SIZE_OPTIONS)[number]; export const DEFAULT_MERCHANT_DETAIL_PAGE_SIZE: MerchantDetailPageSize = 10; const DEFAULT_PAGE = 1; const MAX_KEYWORD_LENGTH = 100; const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; /** 오늘 — 로컬 달력 기준 `YYYY-MM-DD`. 서버·브라우저가 같은 규칙으로 계산한다. */ export function todayIsoDate(now: Date = new Date()): string { const offsetMs = now.getTimezoneOffset() * 60 * 1000; return new Date(now.getTime() - offsetMs).toISOString().slice(0, 10); } /** * 등록일 기간 규칙(사용자 확정) — 시작일은 오늘을 넘지 못하고, 종료일은 시작일보다 앞설 수 없다. * 어기는 값은 경계로 당긴다. 화면의 피커 제한과 URL 파싱이 같은 함수를 쓴다. */ export function clampDateRange( startDate: string, endDate: string, today: string = todayIsoDate() ): { startDate: string; endDate: string } { const start = startDate && startDate > today ? today : startDate; const end = endDate && start && endDate < start ? start : endDate; return { startDate: start, endDate: end }; } /** 기획 BO-MAL-001 ① — 업체명·카테고리·노출상태·등록일 네 조건을 AND로 결합한다. */ export type MerchantDetailQuery = { keyword: string; category: string; visibility: string; startDate: string; endDate: string; page: number; pageSize: MerchantDetailPageSize; }; type RawSearchParams = Record; function readParam(params: RawSearchParams, key: string): string { const value = params[key]; return (Array.isArray(value) ? value[0] : value) ?? ''; } function readDate(params: RawSearchParams, key: string): string { const raw = readParam(params, key); return ISO_DATE.test(raw) ? raw : ''; } function isPageSize(value: number): value is MerchantDetailPageSize { return (MERCHANT_DETAIL_PAGE_SIZE_OPTIONS as readonly number[]).includes(value); } export function parseMerchantDetailQuery( searchParams: RawSearchParams ): MerchantDetailQuery { const page = Number(readParam(searchParams, 'page')); const pageSize = Number(readParam(searchParams, 'pageSize')); const visibility = readParam(searchParams, 'visibility'); return { keyword: readParam(searchParams, 'keyword').trim().slice(0, MAX_KEYWORD_LENGTH), // 카테고리는 공통코드라 여기서 허용 목록을 알 수 없다 — 값 그대로 두고 서버가 거른다. category: readParam(searchParams, 'category').trim().slice(0, MAX_KEYWORD_LENGTH), visibility: MERCHANT_DETAIL_VISIBILITY_OPTIONS.some( (option) => option.value === visibility ) ? visibility : FILTER_ALL, ...clampDateRange(readDate(searchParams, 'startDate'), readDate(searchParams, 'endDate')), page: Number.isInteger(page) && page > 0 ? page : DEFAULT_PAGE, pageSize: isPageSize(pageSize) ? pageSize : DEFAULT_MERCHANT_DETAIL_PAGE_SIZE, }; } export function buildMerchantDetailHref( query: MerchantDetailQuery, overrides: Partial = {} ): string { const merged = { ...query, ...overrides }; const params = new URLSearchParams(); if (merged.keyword) params.set('keyword', merged.keyword); if (merged.category) params.set('category', merged.category); if (merged.visibility) params.set('visibility', merged.visibility); if (merged.startDate) params.set('startDate', merged.startDate); if (merged.endDate) params.set('endDate', merged.endDate); if (merged.pageSize !== DEFAULT_MERCHANT_DETAIL_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 ? `${MERCHANT_DETAILS_PATH}?${queryString}` : MERCHANT_DETAILS_PATH; }