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( data: unknown[][], columns: KbxGridColumn[], 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(selection: KbxSelectionState, filter: TFilter): KbxBulkSelectionRequest { return selection.mode === 'all-filtered' ? { mode: 'filter', filter, excludedIds: selection.excludedIds ?? [] } : { mode: 'ids', ids: selection.selectedIds } }