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
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user