refactor(frontend): simplify redundant primitives and fields subcomponents by delegating directly to single source-of-truth Quant*.vue components
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 12s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 20s
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / UI & Storage Validation (pull_request) Failing after 10s
Validators (Pushes and Pull Requests) / Security & Secrets (pull_request) Successful in 10s
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 9s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 12s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / Database & Schema Validation (pull_request) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (pull_request) Failing after 19s
Validators (Pushes and Pull Requests) / Notify PR Results (pull_request) Successful in 1s

This commit is contained in:
2026-07-26 03:04:40 +09:00
parent e1f1aec04f
commit 2bcf857c4d
6 changed files with 46 additions and 343 deletions
@@ -1,82 +1,16 @@
<!-- Typed Field Layer: DateField (Quant Transparent Adapter Wrapper) -->
<!-- Unified Component Re-export: DateField -> QuantDatePicker -->
<template>
<div class="typed-date-field flex flex-col gap-1 w-full">
<div class="flex justify-between items-center">
<label v-if="label" class="text-xs font-bold text-slate-800">
{{ label }} <span v-if="required" class="text-rose-600">*</span>
</label>
<button
v-if="showTodayBtn && !disabled"
type="button"
class="text-[10px] text-blue-600 hover:text-blue-800 font-bold hover:underline"
@click="setToday"
>
오늘 날짜
</button>
</div>
<DatePicker
v-bind="$attrs"
:id="id"
:modelValue="dateValue"
dateFormat="yy-mm-dd"
:placeholder="placeholder || 'YYYY-MM-DD'"
:disabled="disabled"
class="w-full text-xs"
inputClass="w-full h-9 px-3 border border-slate-300 rounded text-xs text-slate-900 font-medium bg-white placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-slate-100"
showIcon
iconDisplay="input"
@update:modelValue="onDateChange"
>
<QuantDatePicker v-bind="$attrs">
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps || {}"></slot>
</template>
</DatePicker>
</div>
</QuantDatePicker>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import DatePicker from 'primevue/datepicker';
import QuantDatePicker from '../QuantDatePicker.vue';
defineOptions({
inheritAttrs: false
});
const props = defineProps<{
id?: string;
label?: string;
modelValue?: string | Date;
placeholder?: string;
required?: boolean;
disabled?: boolean;
showTodayBtn?: boolean;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
const dateValue = computed(() => {
if (!props.modelValue) return null;
if (props.modelValue instanceof Date) return props.modelValue;
const d = new Date(props.modelValue);
return isNaN(d.getTime()) ? null : d;
});
const onDateChange = (val: Date | null) => {
if (!val) {
emit('update:modelValue', '');
emit('change', '');
return;
}
const yyyy = val.getFullYear();
const mm = String(val.getMonth() + 1).padStart(2, '0');
const dd = String(val.getDate()).padStart(2, '0');
const strVal = `${yyyy}-${mm}-${dd}`;
emit('update:modelValue', strVal);
emit('change', strVal);
};
const setToday = () => {
const d = new Date();
onDateChange(d);
};
</script>
@@ -1,102 +1,16 @@
<!-- Typed Field Layer: NumberField (Strict Numeric Only Input Protection) -->
<!-- Unified Component Re-export: NumberField -> QuantNumber -->
<template>
<div class="typed-number-field flex flex-col gap-1 w-full">
<label v-if="label" class="text-xs font-bold text-slate-800">
{{ label }} <span v-if="required" class="text-rose-600">*</span>
</label>
<InputNumber
:modelValue="numericValue"
:placeholder="placeholder || '0'"
:readonly="readonly"
:disabled="disabled"
locale="ko-KR"
:minFractionDigits="0"
:maxFractionDigits="2"
inputClass="w-full h-9 px-3 border border-slate-300 rounded text-right font-mono text-xs text-slate-900 font-bold bg-white placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none disabled:bg-slate-100 disabled:text-slate-500 readonly:bg-slate-50 readonly:text-slate-700"
@keydown="onKeyDown"
@paste="onPaste"
@update:modelValue="handleInput"
/>
</div>
<QuantNumber v-bind="$attrs">
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps || {}"></slot>
</template>
</QuantNumber>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import InputNumber from 'primevue/inputnumber';
import QuantNumber from '../QuantNumber.vue';
const props = defineProps<{
label?: string;
modelValue?: string | number;
placeholder?: string;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
allowDecimal?: boolean;
allowNegative?: boolean;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
const numericValue = computed(() => {
if (props.modelValue === undefined || props.modelValue === null || props.modelValue === '') return null;
const num = Number(String(props.modelValue).replace(/,/g, ''));
return isNaN(num) ? null : num;
defineOptions({
inheritAttrs: false
});
// 키다운 레벨에서 문자 입력 원천 차단
const onKeyDown = (e: KeyboardEvent) => {
// 허용 키: 숫자, Backspace, Delete, Tab, Escape, Enter, 방향키, Home, End
const allowedKeys = [
'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
'Home', 'End'
];
if (allowedKeys.includes(e.key)) {
return;
}
// Ctrl/Cmd 단축키 허용 (복사, 붙여넣기, 전체선택 등)
if (e.ctrlKey || e.metaKey) {
return;
}
// 소수점 허용 조건 (.)
if (props.allowDecimal !== false && (e.key === '.' || e.key === 'Decimal')) {
return;
}
// 음수 허용 조건 (-)
if (props.allowNegative !== false && e.key === '-') {
return;
}
// 숫자가 아니면 키 입력 완전 방지
if (!/^[0-9]$/.test(e.key)) {
e.preventDefault();
}
};
// 붙여넣기 시 문자 필터링 (Sanitize)
const onPaste = (e: ClipboardEvent) => {
const pasteData = e.clipboardData?.getData('text') || '';
// 숫자와 소수점/음수부호 외에는 모두 제거
const regex = props.allowDecimal === false ? (props.allowNegative === false ? /[^0-9]/g : /[^0-9-]/g) : (props.allowNegative === false ? /[^0-9.]/g : /[^0-9.-]/g);
if (regex.test(pasteData)) {
e.preventDefault();
const sanitized = pasteData.replace(regex, '');
if (sanitized) {
const num = Number(sanitized);
if (!isNaN(num)) {
handleInput(num);
}
}
}
};
const handleInput = (val: number | null) => {
const finalVal = val === null ? '' : String(val);
emit('update:modelValue', finalVal);
emit('change', finalVal);
};
</script>
@@ -1,48 +1,16 @@
<!-- Typed Field Layer: StringField (Quant Transparent Adapter Wrapper) -->
<!-- Unified Component Re-export: StringField -> QuantInput -->
<template>
<div class="typed-string-field flex flex-col gap-1 w-full">
<label v-if="label" class="text-xs font-bold text-slate-800">
{{ label }} <span v-if="required" class="text-rose-600">*</span>
</label>
<InputText
v-bind="$attrs"
:id="id"
:modelValue="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
class="w-full h-9 px-3 border border-slate-300 rounded text-xs text-slate-900 font-medium bg-white placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-slate-100 readonly:bg-slate-50"
@update:modelValue="onInput"
>
<QuantInput v-bind="$attrs">
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps || {}"></slot>
</template>
</InputText>
</div>
</QuantInput>
</template>
<script setup lang="ts">
import InputText from 'primevue/inputtext';
import QuantInput from '../QuantInput.vue';
defineOptions({
inheritAttrs: false
});
const props = defineProps<{
id?: string;
label?: string;
modelValue?: string;
placeholder?: string;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
const onInput = (val: string | undefined) => {
const finalVal = val ?? '';
emit('update:modelValue', finalVal);
emit('change', finalVal);
};
</script>
@@ -1,49 +1,22 @@
<!-- Primitive Layer: DialogModal (Quant Transparent Adapter Wrapper) -->
<!-- Unified Component Re-export: DialogModal -> QuantDialog -->
<template>
<Dialog
v-bind="$attrs"
:visible="isOpen"
:header="title"
:modal="true"
:closable="true"
:dismissableMask="true"
class="primitive-dialog-modal max-w-md w-full"
@update:visible="onVisibleChange"
>
<div class="p-4 text-xs text-slate-800 leading-relaxed">
<slot></slot>
</div>
<template #footer>
<div class="flex justify-end gap-2 text-xs pt-2 border-t border-slate-200">
<button type="button" class="px-3 py-1.5 bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold rounded transition" @click="$emit('close')">
닫기
</button>
<button type="button" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded transition shadow-xs" @click="$emit('confirm')">
확인
</button>
</div>
<QuantDialog v-bind="$attrs" :visible="isOpen" @close="$emit('close')">
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps || {}"></slot>
</template>
</Dialog>
</QuantDialog>
</template>
<script setup lang="ts">
import Dialog from 'primevue/dialog';
import QuantDialog from '../QuantDialog.vue';
defineOptions({
inheritAttrs: false
});
const props = defineProps<{
isOpen: boolean;
title: string;
defineProps<{
isOpen?: boolean;
}>();
const emit = defineEmits(['close', 'confirm']);
const onVisibleChange = (val: boolean) => {
if (!val) {
emit('close');
}
};
defineEmits(['close']);
</script>
@@ -1,59 +1,16 @@
<!-- Primitive Layer: SelectInput (Quant Transparent Adapter Wrapper) -->
<!-- Unified Component Re-export: SelectInput -> QuantComboBox -->
<template>
<div class="primitive-select-wrapper w-full">
<Select
v-bind="$attrs"
:id="id"
:modelValue="modelValue"
:options="formattedOptions"
optionLabel="label"
optionValue="value"
:placeholder="placeholder || '선택하세요'"
:disabled="disabled"
:showClear="showClear !== false"
:filter="filter !== false"
class="w-full h-9 border border-slate-300 rounded text-xs text-slate-900 font-medium bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
@update:modelValue="onSelectChange"
>
<QuantComboBox v-bind="$attrs">
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps || {}"></slot>
</template>
</Select>
</div>
</QuantComboBox>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import Select from 'primevue/select';
import QuantComboBox from '../QuantComboBox.vue';
defineOptions({
inheritAttrs: false
});
const props = defineProps<{
id?: string;
modelValue?: string | number;
options: Array<string | { label: string; value: string | number }>;
placeholder?: string;
disabled?: boolean;
showClear?: boolean;
filter?: boolean;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
const formattedOptions = computed(() => {
if (!props.options) return [];
return props.options.map(opt => {
if (typeof opt === 'string') {
return { label: opt, value: opt };
}
return opt;
});
});
const onSelectChange = (val: any) => {
emit('update:modelValue', val);
emit('change', val);
};
</script>
@@ -1,59 +1,16 @@
<!-- Primitive Layer: TextInput (Quant Transparent Adapter Wrapper) -->
<!-- Unified Component Re-export: TextInput -> QuantInput -->
<template>
<div class="primitive-input-wrapper relative w-full flex items-center">
<InputText
v-bind="$attrs"
:id="id"
:type="type || 'text'"
:modelValue="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
class="primitive-input w-full h-9 pl-3 pr-8 border border-slate-300 rounded text-xs text-slate-900 font-medium bg-white placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none disabled:bg-slate-100 disabled:text-slate-500 disabled:cursor-not-allowed readonly:bg-slate-50 readonly:text-slate-700 transition shadow-xs"
@update:modelValue="onUpdateValue"
>
<QuantInput v-bind="$attrs">
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps || {}"></slot>
</template>
</InputText>
<button
v-if="modelValue && !disabled && !readonly"
type="button"
class="absolute right-2 text-slate-400 hover:text-slate-600 text-xs font-bold w-4 h-4 rounded-full flex items-center justify-center bg-slate-100 hover:bg-slate-200 transition"
title="1-Click 삭제"
@click="onClear"
>
</button>
</div>
</QuantInput>
</template>
<script setup lang="ts">
import InputText from 'primevue/inputtext';
import QuantInput from '../QuantInput.vue';
defineOptions({
inheritAttrs: false
});
const props = defineProps<{
id?: string;
type?: string;
modelValue?: string | number;
placeholder?: string;
disabled?: boolean;
readonly?: boolean;
}>();
const emit = defineEmits(['update:modelValue', 'change', 'clear']);
const onUpdateValue = (val: string | undefined) => {
const finalVal = val ?? '';
emit('update:modelValue', finalVal);
emit('change', finalVal);
};
const onClear = () => {
emit('update:modelValue', '');
emit('clear');
};
</script>