Files
QuantEngineByItz/oms-wms-erp/src/components/primitives/FormField.vue
T
kjh2064 b34b0dd7d6
Validators (Pushes and Pull Requests) / UI & Storage Validation (pull_request) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (pull_request) Successful in 13s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (pull_request) Failing after 28s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / CI Workflow Lint (pull_request) Failing after 10s
Validators (Pushes and Pull Requests) / Security & Secrets (pull_request) Successful in 12s
Validators (Pushes and Pull Requests) / Notify PR Results (pull_request) Successful in 2s
Frontend CI Pipeline / ci-frontend-8-steps (pull_request) Failing after 2m50s
Add OMS WMS ERP platform
2026-07-27 00:45:39 +09:00

219 lines
5.8 KiB
Vue

<template>
<div class="form-group">
<label :for="fieldId" class="form-label">
{{ label }}
<span v-if="required" class="text-danger" aria-label="required">(필수)</span>
</label>
<input
v-if="inputComponent === 'input'"
:id="fieldId"
:value="modelValue"
:type="htmlInputType"
:class="['form-control', { 'is-invalid': !!error }]"
:aria-invalid="!!error"
:aria-describedby="error ? `${fieldId}-error` : undefined"
:required="required"
:placeholder="placeholder"
:min="min"
:max="max"
:pattern="pattern"
@blur="$emit('blur')"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<input
v-else-if="inputComponent === 'checkbox'"
:id="fieldId"
type="checkbox"
:checked="modelValue"
:class="['form-check-input', { 'is-invalid': !!error }]"
:aria-invalid="!!error"
:aria-describedby="error ? `${fieldId}-error` : undefined"
:required="required"
@blur="$emit('blur')"
@change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
/>
<select
v-else-if="inputComponent === 'select'"
:id="fieldId"
:value="modelValue"
:class="['form-control', { 'is-invalid': !!error }]"
:aria-invalid="!!error"
:aria-describedby="error ? `${fieldId}-error` : undefined"
:required="required"
@blur="$emit('blur')"
@change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
>
<slot name="options" />
</select>
<textarea
v-else
:id="fieldId"
:value="modelValue"
:class="['form-control', { 'is-invalid': !!error }]"
:aria-invalid="!!error"
:aria-describedby="error ? `${fieldId}-error` : undefined"
:required="required"
:placeholder="placeholder"
@blur="$emit('blur')"
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
/>
<small v-if="displayHint" :id="`${fieldId}-hint`" class="form-text text-muted">
{{ displayHint }}
</small>
<div v-if="error" :id="`${fieldId}-error`" class="invalid-feedback d-block">
{{ error }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { schemaTypeToInputType, type InputType, getHintText } from '@/utils/fieldTypes'
interface Props {
modelValue: string | number | boolean
label: string
fieldId?: string
type?: InputType | 'password' | 'select' | 'textarea'
schemaType?: string // JSON schema type (auto-infers input type)
schemaFormat?: string // JSON schema format (e.g., 'email', 'date')
enumValues?: any[] // For select inputs
error?: string
hint?: string
placeholder?: string
required?: boolean
min?: number
max?: number
step?: number // For number inputs (e.g., 0.01 for currency)
pattern?: string
}
const props = withDefaults(defineProps<Props>(), {
required: false
})
defineEmits<{
'update:modelValue': [value: string | number | boolean]
blur: []
input: []
}>()
const fieldId = computed(() => props.fieldId || `field-${Math.random().toString(36).slice(7)}`)
// 자동 타입 추론: 명시적 type이 없으면 schemaType/schemaFormat에서 추론
const resolvedType = computed<InputType | 'password' | 'select' | 'textarea'>(() => {
if (props.type) {
return props.type
}
// schemaType/schemaFormat에서 자동 추론
const inferred = schemaTypeToInputType(props.schemaType, props.schemaFormat, props.enumValues)
return inferred
})
const inputComponent = computed(() => {
switch (resolvedType.value) {
case 'select': return 'select'
case 'textarea': return 'textarea'
case 'checkbox': return 'checkbox'
default: return 'input'
}
})
// 힌트 자동 생성 (props.hint가 없으면)
const displayHint = computed(() => {
if (props.hint) return props.hint
return getHintText(resolvedType.value as InputType, {
min: props.min,
max: props.max,
pattern: props.pattern
})
})
// 타입별 HTML input type 결정
const htmlInputType = computed(() => {
const type = resolvedType.value
// HTML input type과 Vue용 type 구분
if (type === 'select' || type === 'textarea' || type === 'password') {
return 'text' // placeholder만 사용
}
return type === 'checkbox' ? 'checkbox' : (type as any)
})
</script>
<style scoped>
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.text-danger {
color: #dc3545;
margin-left: 0.25rem;
}
.form-control {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.form-control.is-invalid {
border-color: #dc3545;
}
.form-control.is-invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.form-text {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
color: #6c757d;
}
.invalid-feedback {
display: block;
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
}
textarea.form-control {
resize: vertical;
min-height: 6rem;
}
select.form-control {
appearance: none;
padding-right: 1.5rem;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
}
</style>