V13-FE-006: consolidate approved UI and contract hardening
This commit is contained in:
+62
@@ -0,0 +1,62 @@
|
||||
import type { KbxBulkSelectionRequest, KbxGridColumn, KbxGridPasteIssue, KbxGridPasteResult, KbxGridType, KbxSelectionState } from '@kbx/contracts'
|
||||
|
||||
function normalizeNumber(raw: unknown, integer = false) {
|
||||
const text = String(raw ?? '').replace(/,/g, '').trim()
|
||||
if (!text) return { value: null, changed: raw !== null && raw !== '' }
|
||||
const value = Number(text)
|
||||
if (!Number.isFinite(value) || (integer && !Number.isInteger(value))) return { invalid: true as const, value: raw, changed: false }
|
||||
return { value, changed: String(value) !== String(raw) }
|
||||
}
|
||||
|
||||
function normalizeDate(raw: unknown) {
|
||||
const text = String(raw ?? '').trim()
|
||||
if (!text) return { value: '', changed: false }
|
||||
const compact = text.replace(/[./-]/g, '')
|
||||
if (!/^\d{8}$/.test(compact)) return { invalid: true as const, value: raw, changed: false }
|
||||
const y = Number(compact.slice(0, 4)), m = Number(compact.slice(4, 6)), d = Number(compact.slice(6, 8))
|
||||
const date = new Date(Date.UTC(y, m - 1, d))
|
||||
if (date.getUTCFullYear() !== y || date.getUTCMonth() !== m - 1 || date.getUTCDate() !== d) return { invalid: true as const, value: raw, changed: false }
|
||||
const value = `${compact.slice(0,4)}-${compact.slice(4,6)}-${compact.slice(6,8)}`
|
||||
return { value, changed: value !== text }
|
||||
}
|
||||
|
||||
function normalizeBoolean(raw: unknown) {
|
||||
const text = String(raw ?? '').trim().toLowerCase()
|
||||
if (['true','1','y','yes','예','사용','o'].includes(text)) return { value: true, changed: raw !== true }
|
||||
if (['false','0','n','no','아니오','미사용','x'].includes(text)) return { value: false, changed: raw !== false }
|
||||
return { invalid: true as const, value: raw, changed: false }
|
||||
}
|
||||
|
||||
export function normalizeKbxGridClipboardValue(type: KbxGridType | undefined, raw: unknown) {
|
||||
if (type === 'integer') return { ...normalizeNumber(raw, true), code: 'INVALID_NUMBER' as const }
|
||||
if (['decimal','quantity','money','percent'].includes(type ?? '')) return { ...normalizeNumber(raw), code: 'INVALID_NUMBER' as const }
|
||||
if (type === 'date') return { ...normalizeDate(raw), code: 'INVALID_DATE' as const }
|
||||
if (type === 'boolean') return { ...normalizeBoolean(raw), code: 'INVALID_BOOLEAN' as const }
|
||||
const value = typeof raw === 'string' ? raw.trim() : raw
|
||||
return { value, changed: value !== raw }
|
||||
}
|
||||
|
||||
export function normalizeKbxGridClipboardData<T extends object>(
|
||||
data: unknown[][],
|
||||
columns: KbxGridColumn<T>[],
|
||||
startColumnIndex: number,
|
||||
): { data: unknown[][]; result: KbxGridPasteResult } {
|
||||
const issues: KbxGridPasteIssue[] = []
|
||||
let normalizedCount = 0
|
||||
const normalized = data.map((row, rowOffset) => row.map((raw, columnOffset) => {
|
||||
const column = columns[startColumnIndex + columnOffset]
|
||||
if (!column) return raw
|
||||
const parsed = normalizeKbxGridClipboardValue(column.type, raw)
|
||||
if (parsed.invalid) issues.push({ rowOffset, columnOffset, field: column.field, code: parsed.code!, value: raw })
|
||||
if (parsed.changed) normalizedCount++
|
||||
return parsed.value
|
||||
}))
|
||||
const cellCount = normalized.reduce((sum, row) => sum + row.length, 0)
|
||||
return { data: normalized, result: { cellCount, normalizedCount, rejectedCount: issues.length, issues } }
|
||||
}
|
||||
|
||||
export function toKbxBulkSelectionRequest<TFilter, TId = string>(selection: KbxSelectionState<TId>, filter: TFilter): KbxBulkSelectionRequest<TFilter, TId> {
|
||||
return selection.mode === 'all-filtered'
|
||||
? { mode: 'filter', filter, excludedIds: selection.excludedIds ?? [] }
|
||||
: { mode: 'ids', ids: selection.selectedIds }
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import type { KbxGridStatusMap, KbxStatusSemantic } from '@kbx/contracts'
|
||||
|
||||
export interface KbxResolvedGridStatus {
|
||||
rawValue: string
|
||||
label: string
|
||||
semantic: KbxStatusSemantic
|
||||
unknown: boolean
|
||||
}
|
||||
|
||||
/** Domain raw value를 변경하지 않고 사용자 표시용 상태만 해석한다. */
|
||||
export function resolveKbxGridStatus(map:KbxGridStatusMap|undefined,value:unknown):KbxResolvedGridStatus {
|
||||
const rawValue=value==null?'':String(value)
|
||||
const definition=map?.definitions.find(item=>item.value===rawValue)
|
||||
if(definition)return {rawValue,label:definition.label,semantic:definition.semantic,unknown:false}
|
||||
return {
|
||||
rawValue,
|
||||
label:`${map?.unknownLabel??'정의되지 않음'} · ${rawValue||'빈 값'}`,
|
||||
semantic:'warning',
|
||||
unknown:true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user