import type { SubjectKind } from './event-subjects.js' /* * **이 물음을 파생 테이블로 답할 수 있나** — 순수 판단만 여기 둔다. * * 데이터베이스도 커널도 부르지 않는다. 그래서 시험이 이 파일을 그대로 부를 수 있다(데코레이터가 없다). * 판단이 조회 코드 안에 섞여 있으면 그 판단만 확인할 방법이 없고, 그러면 「어떤 물음이 빠른 경로로 * 가는가」가 아무도 읽지 못하는 규칙이 된다. * * 좁게 잡는다. 파생 테이블은 「대상 하나를 시각순으로」에 맞춰 생겼고, 그 모양이 아닌 물음에 억지로 * 답하면 목록이 알리지 않고 달라진다. 맞지 않으면 `null` 이고, 부르는 쪽은 저널로 간다. */ /** 화면이 저널에 묻던 축 → 파생 테이블의 대상 종류. */ export const AXIS_KIND: Record = { epc: 'item', orderId: 'order', locationId: 'location', moverId: 'equipment' // vocabulary-guard: allow — 저널의 승격 컬럼 이름(저장된 계약) } /** 이 경로가 다룰 수 있는 필터 이름 — 이 밖의 필터가 하나라도 있으면 저널로 보낸다. */ const ALLOWED_FILTERS = new Set(Object.keys(AXIS_KIND).concat('eventTime')) export interface SubjectQueryShape { kind: SubjectKind /** 화면이 건넨 축 값 — 짧게 줄어 있을 수 있다(되돌리는 일은 조회하는 쪽이 한다). */ value: string untilTime?: Date fromTime?: Date } /** * 목록 계약의 조건을 읽어 「대상 하나의 시각순 이력」인지 가린다 — 아니면 `null`. * * 받는 모양 * 축 하나(품목·오더·자리·설비)를 `eq` 또는 `search` 로 · 시각 상한이나 구간 · 시각 내림차순 정렬 * * 보내는 모양(저널로) * 자유 검색어 · 종류 필터 · 상관 식별자 · 축 둘 이상 · 그 밖의 정렬이나 연산자 */ export function readModelShape(params: any, sortings: any[]): SubjectQueryShape | null { const filters: any[] = Array.isArray(params?.filters) ? params.filters : [] if (typeof params?.search === 'string' && params.search.trim()) return null for (const f of filters) if (!ALLOWED_FILTERS.has(f?.name)) return null /* 정렬은 시각 내림차순 하나일 때만 — 파생 테이블의 목차가 그 축이다. */ if (sortings.length !== 1 || sortings[0]?.name !== 'eventTime' || sortings[0]?.desc !== true) return null const axisFilters = filters.filter(f => f.name !== 'eventTime') if (axisFilters.length !== 1) return null const axis = axisFilters[0] const kind = AXIS_KIND[axis.name] if (!kind) return null /* 축은 값 하나여야 한다 — 여러 값(`in`)이면 목차를 한 번에 타지 못한다. */ if (axis.operator !== 'eq' && axis.operator !== 'search') return null const value = typeof axis.value === 'string' ? axis.value.trim() : '' if (!value) return null /* 시각은 상한·구간만 받는다(시간여행). 그 밖의 연산자는 뜻이 달라지므로 저널로 보낸다. */ let untilTime: Date | undefined let fromTime: Date | undefined for (const f of filters) { if (f.name !== 'eventTime') continue if (f.operator === 'lte' || f.operator === 'lt') { untilTime = toDate(f.value) if (!untilTime) return null } else if (f.operator === 'between' && Array.isArray(f.value)) { fromTime = toDate(f.value[0]) untilTime = toDate(f.value[1]) if (!fromTime || !untilTime) return null } else return null } return { kind, value, untilTime, fromTime } } /** 값을 날짜로 — 못 읽으면 `undefined`(지어내지 않는다). */ export function toDate(v: unknown): Date | undefined { if (v instanceof Date) return Number.isNaN(v.getTime()) ? undefined : v if (typeof v !== 'string' && typeof v !== 'number') return undefined const d = new Date(v as any) return Number.isNaN(d.getTime()) ? undefined : d }