/** * 코드관리 화면(SYS_COD_001)의 URL 조건 — 순수 규칙만 담는다(next/react 의존 없음). * * 이 화면은 목록이 둘이라 상태가 셋이다. **선택된 공통코드(`comCd`)**, 공통코드 목록의 검색· * 페이지, 상세코드 목록의 페이지. 셋 다 URL이 소유한다 — 다른 목록 화면과 같은 규칙이고, * 새로고침·뒤로가기·링크 공유가 그대로 동작한다. * * **선택이 바뀌면 상세 페이지는 1로 돌아간다** — 다른 그룹의 3페이지는 의미가 없다. * 그 규칙은 `buildCommonCodeHref`가 강제한다(호출부가 잊어도 어긋나지 않게). * * 페이징·검색을 URL에 두면서도 백엔드에는 넘기지 않는다 — 백엔드가 두 목록 모두 전체를 * 반환하고 검색도 완전일치라, 자르고 거르는 일은 서버 컴포넌트가 한다(Repository 주석 참조). */ /** 라우트 경로 — 이 파일 안에서만 하드코딩하고 나머지는 이 상수를 참조한다. */ export const COMMON_CODES_PATH = '/system/codes'; /** 검색 대상 — 시안(SYS_COD_001 ①) "코드명 / 코드ID". */ export type CommonCodeSearchField = 'name' | 'comCd'; export const COMMON_CODE_SEARCH_FIELD_OPTIONS: ReadonlyArray<{ value: CommonCodeSearchField; label: string; }> = [ { value: 'name', label: '코드명' }, { value: 'comCd', label: '코드ID' }, ]; export const DEFAULT_COMMON_CODE_SEARCH_FIELD: CommonCodeSearchField = 'name'; /** 시안의 두 목록은 한 화면에 나란히 서므로 페이지 크기를 고르는 자리가 없다 — 고정값이다. */ export const COMMON_CODE_PAGE_SIZE = 10; export const COMMON_CODE_DETAIL_PAGE_SIZE = 10; const DEFAULT_PAGE = 1; const MAX_KEYWORD_LENGTH = 100; export type CommonCodeQuery = { /** 선택된 공통코드. 아직 고르지 않았으면 null이고, 화면이 첫 행으로 채운다. */ comCd: string | null; searchField: CommonCodeSearchField; keyword: string; /** 공통코드 목록의 페이지. */ page: number; /** 상세코드 목록의 페이지. */ detailPage: number; }; type RawSearchParams = Record; function readParam(params: RawSearchParams, key: string): string | undefined { const value = params[key]; return Array.isArray(value) ? value[0] : value; } function readPage(params: RawSearchParams, key: string): number { const value = Number(readParam(params, key)); return Number.isInteger(value) && value > 0 ? value : DEFAULT_PAGE; } function isSearchField( value: string | undefined ): value is CommonCodeSearchField { return ( value !== undefined && COMMON_CODE_SEARCH_FIELD_OPTIONS.some((option) => option.value === value) ); } /** * URL의 searchParams를 검증된 `CommonCodeQuery`로 정규화한다. searchParams는 사용자가 임의로 * 조작할 수 있는 값이라 신뢰하지 않는다 — 허용 목록을 벗어나면 기본값으로 떨어진다. * * `comCd`만은 허용 목록을 여기서 확인할 수 없다(코드 목록이 서버에 있다) — 존재 여부는 화면이 * 조회 결과와 맞춰 보고 없으면 첫 행으로 대체한다. */ export function parseCommonCodeQuery( searchParams: RawSearchParams ): CommonCodeQuery { const searchFieldRaw = readParam(searchParams, 'searchField'); const comCd = readParam(searchParams, 'comCd')?.trim(); return { comCd: comCd ? comCd.slice(0, 50) : null, searchField: isSearchField(searchFieldRaw) ? searchFieldRaw : DEFAULT_COMMON_CODE_SEARCH_FIELD, keyword: (readParam(searchParams, 'keyword') ?? '') .trim() .slice(0, MAX_KEYWORD_LENGTH), page: readPage(searchParams, 'page'), detailPage: readPage(searchParams, 'detailPage'), }; } /** * `CommonCodeQuery`(+ 부분 override)를 링크로 직렬화한다. `parseCommonCodeQuery`의 역연산이며 * 기본값과 같은 필드는 URL에서 생략해 링크를 짧게 유지한다. * * **공통코드 선택이 바뀌면 상세 페이지를 1로 되돌린다** — 호출부가 잊어도 어긋나지 않도록 * 여기서 강제한다(override로 detailPage를 함께 준 경우는 그 값을 존중한다). */ export function buildCommonCodeHref( query: CommonCodeQuery, overrides: Partial = {} ): string { const merged = { ...query, ...overrides }; if ( overrides.comCd !== undefined && overrides.comCd !== query.comCd && overrides.detailPage === undefined ) { merged.detailPage = DEFAULT_PAGE; } const params = new URLSearchParams(); if (merged.comCd) { params.set('comCd', merged.comCd); } if (merged.searchField !== DEFAULT_COMMON_CODE_SEARCH_FIELD) { params.set('searchField', merged.searchField); } if (merged.keyword) { params.set('keyword', merged.keyword); } if (merged.page !== DEFAULT_PAGE) { params.set('page', String(merged.page)); } if (merged.detailPage !== DEFAULT_PAGE) { params.set('detailPage', String(merged.detailPage)); } const queryString = params.toString(); return queryString ? `${COMMON_CODES_PATH}?${queryString}` : COMMON_CODES_PATH; }