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
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { UiSeverity } from '../contracts'
withDefaults(defineProps<{ value: string; severity?: UiSeverity }>(), { severity: 'info' })
import type { UiGridStatusSemantic } from '../../gridStatus'
withDefaults(defineProps<{ value: string; severity?: UiSeverity; semantic?: UiGridStatusSemantic; unknown?: boolean }>(), { severity: 'info', unknown: false })
</script>
<template><span class="ks-native-tag" :class="`is-${severity}`">{{ value }}</span></template>
<template><span class="ks-native-tag" :class="[`is-${severity}`, { 'is-unknown': unknown }]" :data-semantic="semantic" :aria-label="unknown ? `정의되지 않음: ${value}` : value"><span aria-hidden="true">{{ unknown ? '?' : '●' }}</span> {{ value }}</span></template>
@@ -1,5 +1,6 @@
<script setup lang="ts">
defineProps<{ modelValue: string; inputId?: string; disabled?: boolean; invalid?: boolean; placeholder?: string }>()
import type { UiTextFieldType } from '../contracts'
defineProps<{ modelValue: string; inputId?: string; type?: UiTextFieldType; disabled?: boolean; invalid?: boolean; placeholder?: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string]; blur: [event: FocusEvent] }>()
</script>
<template><input :id="inputId" class="ks-native-input" type="text" :value="modelValue" :disabled="disabled" :aria-invalid="invalid || undefined" :placeholder="placeholder" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" @blur="emit('blur', $event)" /></template>
<template><input :id="inputId" class="ks-native-input" :type="type ?? 'text'" :value="modelValue" :disabled="disabled" :aria-invalid="invalid || undefined" :placeholder="placeholder" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" @blur="emit('blur', $event)" /></template>
@@ -1,13 +1,14 @@
<script setup lang="ts">
import Tag from 'primevue/tag'
import type { UiSeverity } from '../contracts'
import type { UiGridStatusSemantic } from '../../gridStatus'
defineProps<{ value: string; severity?: UiSeverity; iconLabel?: string }>()
defineProps<{ value: string; severity?: UiSeverity; iconLabel?: string; semantic?: UiGridStatusSemantic; unknown?: boolean }>()
</script>
<template>
<Tag class="ks-status-tag" :severity="severity ?? 'info'">
<span v-if="iconLabel" aria-hidden="true">{{ iconLabel }}</span>
<Tag class="ks-status-tag" :severity="severity ?? 'info'" :data-semantic="semantic" :data-unknown="unknown || undefined" :aria-label="unknown ? `정의되지 않음: ${value}` : value">
<span aria-hidden="true">{{ iconLabel ?? (unknown ? '?' : '●') }}</span>
<span>{{ value }}</span>
</Tag>
</template>
@@ -1,7 +1,8 @@
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import type { UiTextFieldType } from '../contracts'
defineProps<{ modelValue: string; inputId?: string; disabled?: boolean; invalid?: boolean; placeholder?: string }>()
defineProps<{ modelValue: string; inputId?: string; type?: UiTextFieldType; disabled?: boolean; invalid?: boolean; placeholder?: string }>()
defineEmits<{ 'update:modelValue': [value: string]; blur: [event: FocusEvent] }>()
</script>
@@ -10,6 +11,7 @@ defineEmits<{ 'update:modelValue': [value: string]; blur: [event: FocusEvent] }>
class="ks-input"
:id="inputId"
:model-value="modelValue"
:type="type"
:disabled="disabled"
:invalid="invalid"
:placeholder="placeholder"
@@ -1,18 +1,20 @@
import { defineAsyncComponent, type Component } from 'vue'
import type { UiAdapter, UiAdapterCapability } from '../contracts'
import Button from './PrimeButtonAdapter.vue'
import TextField from './PrimeTextFieldAdapter.vue'
import TextArea from './PrimeTextAreaAdapter.vue'
import Select from './PrimeSelectAdapter.vue'
import MultiSelect from './PrimeMultiSelectAdapter.vue'
import Checkbox from './PrimeCheckboxAdapter.vue'
import DateField from './PrimeDateFieldAdapter.vue'
import NumberField from './PrimeNumberFieldAdapter.vue'
import Dialog from './PrimeDialogAdapter.vue'
import StatusTag from './PrimeStatusTagAdapter.vue'
import InlineMessage from './PrimeInlineMessageAdapter.vue'
import Paginator from './PrimePaginatorAdapter.vue'
import Tabs from './PrimeTabsAdapter.vue'
import DataGrid from './AgGridAdapter.vue'
const lazy = (loader: () => Promise<{ default: Component }>) => defineAsyncComponent({ loader, suspensible: false })
const Button = lazy(() => import('./PrimeButtonAdapter.vue'))
const TextField = lazy(() => import('./PrimeTextFieldAdapter.vue'))
const TextArea = lazy(() => import('./PrimeTextAreaAdapter.vue'))
const Select = lazy(() => import('./PrimeSelectAdapter.vue'))
const MultiSelect = lazy(() => import('./PrimeMultiSelectAdapter.vue'))
const Checkbox = lazy(() => import('./PrimeCheckboxAdapter.vue'))
const DateField = lazy(() => import('./PrimeDateFieldAdapter.vue'))
const NumberField = lazy(() => import('./PrimeNumberFieldAdapter.vue'))
const Dialog = lazy(() => import('./PrimeDialogAdapter.vue'))
const StatusTag = lazy(() => import('./PrimeStatusTagAdapter.vue'))
const InlineMessage = lazy(() => import('./PrimeInlineMessageAdapter.vue'))
const Paginator = lazy(() => import('./PrimePaginatorAdapter.vue'))
const Tabs = lazy(() => import('./PrimeTabsAdapter.vue'))
const DataGrid = lazy(() => import('./AgGridAdapter.vue'))
const capabilities: ReadonlySet<UiAdapterCapability> = new Set([
'button','text-field','text-area','select','multi-select','checkbox','date-field','number-field',
@@ -0,0 +1,21 @@
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
function sourceFiles(root: string): string[] {
return readdirSync(root, { withFileTypes: true }).flatMap(entry => {
const path = join(root, entry.name)
if (entry.isDirectory()) return sourceFiles(path)
return /\.(ts|vue)$/.test(entry.name) ? [path] : []
})
}
describe('UI vendor boundary', () => {
it('keeps PrimeVue and AG Grid imports inside the shared adapter', () => {
const featureRoot = join(process.cwd(), 'src', 'features')
const forbiddenImport = /(?:from|import\s*\()\s*["'](?:primevue|ag-grid)/
const violations = sourceFiles(featureRoot).filter(path => forbiddenImport.test(readFileSync(path, 'utf8')))
expect(violations).toEqual([])
})
})