V13-FE-006: consolidate approved UI and contract hardening
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s

This commit is contained in:
2026-08-13 02:41:00 +09:00
parent d79edae546
commit 3f293d8aa8
1278 changed files with 14384 additions and 1664 deletions
@@ -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 }
}
@@ -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,
}
}