fix(frontend): enforce strict numeric-only input protection in NumberField.vue and QuantInput.vue (type=number)
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 20s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 10s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
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) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped

This commit is contained in:
2026-07-26 02:52:18 +09:00
parent 3df5f7b95f
commit 79026960b4
2 changed files with 105 additions and 5 deletions
+49 -4
View File
@@ -1,4 +1,4 @@
<!-- PrimeVue InputText Adapter Wrapper: QuantInput -->
<!-- PrimeVue InputText Adapter Wrapper: QuantInput (Strict Numeric Filter Support) -->
<template>
<div class="quant-input-wrapper flex flex-col gap-1 w-full">
<label v-if="label" class="text-xs font-bold text-slate-800">
@@ -6,18 +6,21 @@
</label>
<InputText
:id="id"
:type="type || 'text'"
:modelValue="modelValue"
:type="inputType"
:modelValue="displayValue"
: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"
@keydown="onKeyDown"
@paste="onPaste"
@update:modelValue="onInput"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import InputText from 'primevue/inputtext';
const props = defineProps<{
@@ -33,8 +36,50 @@ const props = defineProps<{
const emit = defineEmits(['update:modelValue', 'change']);
const isNumeric = computed(() => props.type === 'number');
const inputType = computed(() => (isNumeric.value ? 'text' : props.type || 'text'));
const displayValue = computed(() => {
if (props.modelValue === undefined || props.modelValue === null) return '';
return String(props.modelValue);
});
const onKeyDown = (e: KeyboardEvent) => {
if (!isNumeric.value) return;
const allowedKeys = [
'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
'Home', 'End', '.', '-'
];
if (allowedKeys.includes(e.key) || e.ctrlKey || e.metaKey) {
return;
}
if (!/^[0-9]$/.test(e.key)) {
e.preventDefault();
}
};
const onPaste = (e: ClipboardEvent) => {
if (!isNumeric.value) return;
const pasteData = e.clipboardData?.getData('text') || '';
if (/[^0-9.-]/g.test(pasteData)) {
e.preventDefault();
const sanitized = pasteData.replace(/[^0-9.-]/g, '');
onInput(sanitized);
}
};
const onInput = (val: string | undefined) => {
const finalVal = val ?? '';
let finalVal = val ?? '';
if (isNumeric.value) {
// 숫자가 아닌 모든 문자 원천 정제
finalVal = finalVal.replace(/[^0-9.-]/g, '');
}
emit('update:modelValue', finalVal);
emit('change', finalVal);
};
@@ -1,4 +1,4 @@
<!-- Typed Field Layer: NumberField (PrimeVue InputNumber Adapter Wrapper) -->
<!-- Typed Field Layer: NumberField (Strict Numeric Only Input Protection) -->
<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">
@@ -13,6 +13,8 @@
: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>
@@ -29,6 +31,8 @@ const props = defineProps<{
required?: boolean;
readonly?: boolean;
disabled?: boolean;
allowDecimal?: boolean;
allowNegative?: boolean;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
@@ -39,6 +43,57 @@ const numericValue = computed(() => {
return isNaN(num) ? null : num;
});
// 키다운 레벨에서 문자 입력 원천 차단
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);