V13-FE-011: finalize search list layout slice

This commit is contained in:
2026-08-09 02:57:26 +09:00
parent 9efd202e76
commit 6422cb2b13
984 changed files with 120811 additions and 1498 deletions
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { useUiAdapter } from '../adapter/useUiAdapter'
import FieldShell from './FieldShell.vue'
const props = withDefaults(defineProps<{ modelValue: number | null; label: string; currency?: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; min?: number; max?: number }>(), { currency: 'KRW' })
const emit = defineEmits<{ 'update:modelValue': [value: number | null]; blur: [event: FocusEvent] }>()
const adapter = useUiAdapter()
</script>
<template>
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
<span class="ks-money-field">
<component
:is="adapter.components.NumberField"
:input-id="field.inputId"
:model-value="modelValue"
:disabled="disabled"
:invalid="field.invalid"
:min="min"
:max="max"
:min-fraction-digits="0"
:max-fraction-digits="0"
:aria-describedby="field.describedBy"
:aria-required="field.required || undefined"
@update:model-value="emit('update:modelValue', $event)"
@blur="emit('blur', $event)"
/>
<span class="ks-money-field__currency" aria-hidden="true">{{ props.currency }}</span>
</span>
</FieldShell>
</template>
<style scoped>
.ks-money-field { display: inline-flex; align-items: center; gap: var(--ks-space-2); }
.ks-money-field__currency { color: var(--ks-color-text-muted); font-size: var(--ks-font-caption); font-weight: 600; }
</style>
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useUiAdapter } from '../adapter/useUiAdapter'
import FieldShell from './FieldShell.vue'
const props = withDefaults(defineProps<{ modelValue: number | null; label: string; unit?: string; availableQuantity?: number; allowNegative?: boolean; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string }>(), { unit: '주', allowNegative: false })
const emit = defineEmits<{ 'update:modelValue': [value: number | null]; blur: [event: FocusEvent] }>()
const adapter = useUiAdapter()
const exceedsAvailable = computed(() =>
props.modelValue != null && props.availableQuantity != null && props.modelValue > props.availableQuantity
)
const effectiveError = computed(() => props.error ?? (exceedsAvailable.value ? `가용 수량(${props.availableQuantity} ${props.unit})을 초과했습니다.` : undefined))
const effectiveHelp = computed(() => props.availableQuantity != null ? `가용 ${props.availableQuantity} ${props.unit}` : props.help)
</script>
<template>
<FieldShell :label="label" :input-id="inputId" :required="required" :error="effectiveError" :help="effectiveHelp" v-slot="field">
<span class="ks-quantity-field">
<component
:is="adapter.components.NumberField"
:input-id="field.inputId"
:model-value="modelValue"
:disabled="disabled"
:invalid="field.invalid"
:min="allowNegative ? undefined : 0"
:min-fraction-digits="0"
:max-fraction-digits="0"
:aria-describedby="field.describedBy"
:aria-required="field.required || undefined"
@update:model-value="emit('update:modelValue', $event)"
@blur="emit('blur', $event)"
/>
<span class="ks-quantity-field__unit" aria-hidden="true">{{ props.unit }}</span>
</span>
</FieldShell>
</template>
<style scoped>
.ks-quantity-field { display: inline-flex; align-items: center; gap: var(--ks-space-2); }
.ks-quantity-field__unit { color: var(--ks-color-text-muted); font-size: var(--ks-font-caption); font-weight: 600; }
</style>
@@ -6,6 +6,8 @@ export { default as KsMultiSelect } from './KsMultiSelect.vue'
export { default as KsCheckbox } from './KsCheckbox.vue'
export { default as KsDateField } from './KsDateField.vue'
export { default as KsNumberField } from './KsNumberField.vue'
export { default as KsMoneyField } from './KsMoneyField.vue'
export { default as KsQuantityField } from './KsQuantityField.vue'
export { default as KsDialog } from './KsDialog.vue'
export { default as KsStatusTag } from './KsStatusTag.vue'
export { default as KsInlineMessage } from './KsInlineMessage.vue'
@@ -0,0 +1,36 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { uiAdapterKey } from '../../adapter/contracts'
import { nativeUiAdapter } from '../../adapter/native'
import KsMoneyField from '../KsMoneyField.vue'
import KsQuantityField from '../KsQuantityField.vue'
const globalProvide = { global: { provide: { [uiAdapterKey as unknown as string]: nativeUiAdapter } } }
describe('KsMoneyField', () => {
it('connects the label, shows the currency code, and emits numeric updates', async () => {
const wrapper = mount(KsMoneyField, { props: { modelValue: 1250000, label: '단가' }, ...globalProvide })
expect(wrapper.get('label').text()).toContain('단가')
expect(wrapper.text()).toContain('KRW')
await wrapper.get('input').setValue('2000000')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([2000000])
})
it('accepts a non-KRW currency code', () => {
const wrapper = mount(KsMoneyField, { props: { modelValue: 100, label: '금액', currency: 'USD' }, ...globalProvide })
expect(wrapper.text()).toContain('USD')
})
})
describe('KsQuantityField', () => {
it('shows the available quantity as help text when under the limit', () => {
const wrapper = mount(KsQuantityField, { props: { modelValue: 5, label: '주문수량', unit: 'EA', availableQuantity: 8 }, ...globalProvide })
expect(wrapper.text()).toContain('가용 8 EA')
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
})
it('raises an accessible error when the value exceeds the available quantity', () => {
const wrapper = mount(KsQuantityField, { props: { modelValue: 10, label: '주문수량', unit: 'EA', availableQuantity: 8 }, ...globalProvide })
expect(wrapper.get('[role="alert"]').text()).toContain('가용 수량(8 EA)을 초과했습니다.')
})
})