refactor(frontend): complete wrapping all components with PrimeVue 4 adapters (AutoComplete, Checkbox, Radio, Textarea, Dialog, Splitter, Select, InputText) for total vendor decoupling
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 12s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 20s
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / WBS & Audit Validations (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) / Security & Secrets (push) Successful in 10s
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) / UI & Storage Validation (push) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 12s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 20s
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / WBS & Audit Validations (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) / Security & Secrets (push) Successful in 10s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
This commit is contained in:
@@ -1,46 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
suggestions: Array<{ label: string; value: string }>
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select'])
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const filtered = computed(() => {
|
||||
if (!props.modelValue) return props.suggestions
|
||||
return props.suggestions.filter(s => s.label.toLowerCase().includes(props.modelValue.toLowerCase()) || s.value.includes(props.modelValue))
|
||||
})
|
||||
|
||||
const select = (item: { label: string; value: string }) => {
|
||||
emit('update:modelValue', item.value)
|
||||
emit('select', item)
|
||||
isOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue AutoComplete Adapter Wrapper: QuantAutoComplete -->
|
||||
<template>
|
||||
<div style="position: relative; width: 100%;">
|
||||
<input
|
||||
:value="modelValue"
|
||||
type="text"
|
||||
:placeholder="placeholder || '자동완성 검색...'"
|
||||
style="width: 100%; box-sizing: border-box; padding: 4px 8px; border: 1px solid #CBD5E1; border-radius: 2px; font-size: 12px; font-weight: bold;"
|
||||
@focus="isOpen = true"
|
||||
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value); isOpen = true"
|
||||
<div class="quant-autocomplete-wrapper w-full">
|
||||
<AutoComplete
|
||||
:modelValue="modelValue"
|
||||
:suggestions="filteredSuggestions"
|
||||
:placeholder="placeholder || '검색어 입력'"
|
||||
: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"
|
||||
@complete="searchSuggestions"
|
||||
@update:modelValue="onUpdateValue"
|
||||
/>
|
||||
<div v-if="isOpen && filtered.length > 0" style="position: absolute; top: 100%; left: 0; width: 100%; background: white; border: 1px solid #CBD5E1; box-shadow: 0 4px 8px rgba(0,0,0,0.1); z-index: 1000; max-height: 150px; overflow-y: auto;">
|
||||
<div
|
||||
v-for="item in filtered"
|
||||
:key="item.value"
|
||||
style="padding: 6px 8px; font-size: 12px; cursor: pointer; border-bottom: 1px solid #ECF0F1;"
|
||||
@click="select(item)">
|
||||
<strong>{{ item.label }}</strong> ({{ item.value }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import AutoComplete from 'primevue/autocomplete';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string;
|
||||
suggestions?: string[];
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const filteredSuggestions = ref<string[]>([]);
|
||||
|
||||
const searchSuggestions = (event: { query: string }) => {
|
||||
const query = event.query.toLowerCase();
|
||||
const list = props.suggestions || [];
|
||||
filteredSuggestions.value = list.filter(item => item.toLowerCase().includes(query));
|
||||
};
|
||||
|
||||
const onUpdateValue = (val: any) => {
|
||||
const finalVal = val ?? '';
|
||||
emit('update:modelValue', finalVal);
|
||||
emit('change', finalVal);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
label?: string
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue Checkbox Adapter Wrapper: QuantCheckBox -->
|
||||
<template>
|
||||
<label style="display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: bold; cursor: pointer; color: #2C3E50;">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="modelValue"
|
||||
:disabled="readonly"
|
||||
style="cursor: pointer; accent-color: #2980B9;"
|
||||
@change="emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
|
||||
<div class="quant-checkbox-wrapper flex items-center gap-2 cursor-pointer select-none">
|
||||
<Checkbox
|
||||
:id="id"
|
||||
:modelValue="modelValue"
|
||||
:binary="true"
|
||||
:disabled="disabled"
|
||||
class="text-blue-600"
|
||||
@update:modelValue="onChange"
|
||||
/>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
</label>
|
||||
<label v-if="label" :for="id" class="text-xs font-bold text-slate-800 cursor-pointer">
|
||||
{{ label }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string;
|
||||
modelValue?: boolean;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const onChange = (val: boolean) => {
|
||||
emit('update:modelValue', val);
|
||||
emit('change', val);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,25 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
modelValue: string | number
|
||||
options: Array<{ label: string; value: string | number }>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'enter'])
|
||||
|
||||
const onChange = (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue Select Adapter Wrapper: QuantComboBox -->
|
||||
<template>
|
||||
<select
|
||||
:value="modelValue"
|
||||
:disabled="readonly"
|
||||
style="width: 100%; padding: 4px 8px; border: 1px solid #CBD5E1; border-radius: 2px; font-size: 12px; font-weight: bold; background: white; outline: none; cursor: pointer;"
|
||||
@change="onChange"
|
||||
@keydown.enter="emit('enter')">
|
||||
<option v-for="opt in options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||
</select>
|
||||
<div class="quant-combobox-wrapper w-full">
|
||||
<Select
|
||||
:id="id"
|
||||
:modelValue="modelValue"
|
||||
:options="formattedOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:placeholder="placeholder || '선택하세요'"
|
||||
:disabled="disabled"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import Select from 'primevue/select';
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string;
|
||||
modelValue?: string | number;
|
||||
options: Array<string | { label: string; value: string | number }>;
|
||||
placeholder?: string;
|
||||
disabled?: 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,29 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
targetName?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['confirm', 'close'])
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue Dialog Adapter Wrapper: QuantDeleteModal -->
|
||||
<template>
|
||||
<div v-if="visible" class="modal d-block modal-blur" tabindex="-1" style="background: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-status bg-danger"></div>
|
||||
<div class="modal-body text-center py-4">
|
||||
<i class="ti ti-alert-triangle text-danger fs-1 mb-2"></i>
|
||||
<h4 class="fw-bold">정말 삭제하시겠습니까?</h4>
|
||||
<p class="text-muted fs-7 mb-0">
|
||||
{{ targetName ? `'${targetName}' 항목이` : '선택한 항목이' }} 비활성화(Soft Delete) 처리됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary w-50" @click="emit('close')">취소</button>
|
||||
<button type="button" class="btn btn-danger w-50" @click="emit('confirm')">삭제 실행</button>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog
|
||||
:visible="isOpen"
|
||||
header="🚨 영구 삭제 확인"
|
||||
:modal="true"
|
||||
:closable="true"
|
||||
:dismissableMask="true"
|
||||
class="quant-delete-modal max-w-sm w-full"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<div class="p-4 text-xs text-slate-800 leading-relaxed flex flex-col gap-2">
|
||||
<p>정말로 <strong class="text-rose-600 font-bold">{{ targetName || '선택한 항목' }}</strong>을(를) 삭제하시겠습니까?</p>
|
||||
<p class="text-[11px] text-slate-500">삭제 후에는 복구할 수 없습니다.</p>
|
||||
</div>
|
||||
</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-rose-600 hover:bg-rose-700 text-white font-bold rounded transition shadow-xs" @click="$emit('confirm')">
|
||||
네, 삭제합니다
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Dialog from 'primevue/dialog';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
targetName?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'confirm']);
|
||||
|
||||
const onVisibleChange = (val: boolean) => {
|
||||
if (!val) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,102 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
initialData?: Record<string, any>
|
||||
fields: Array<{
|
||||
name: string
|
||||
label: string
|
||||
type?: 'text' | 'number' | 'select' | 'textarea' | 'checkbox' | 'date'
|
||||
required?: boolean
|
||||
options?: Array<{ label: string; value: any }>
|
||||
placeholder?: string
|
||||
}>
|
||||
isEditing?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['save', 'cancel', 'delete'])
|
||||
|
||||
const formData = ref<Record<string, any>>({ ...(props.initialData || {}) })
|
||||
|
||||
const handleSave = () => {
|
||||
emit('save', formData.value)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirm('해당 레코드를 삭제(Soft Delete)하시겠습니까?')) {
|
||||
emit('delete', formData.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue Dialog Adapter Wrapper: QuantFormModal -->
|
||||
<template>
|
||||
<div class="card shadow-sm border">
|
||||
<div class="card-header bg-navy text-white d-flex justify-content-between align-items-center py-2 px-3">
|
||||
<h5 class="card-title m-0 font-weight-bold text-white fs-6">
|
||||
<i class="ti ti-edit me-1"></i> {{ title || (isEditing ? '데이터 수정' : '신규 데이터 등록') }}
|
||||
</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-success fw-bold px-3" @click="handleSave">
|
||||
<span class="hotkey-badge me-1">F4</span>{{ isEditing ? '수정 저장' : '신규 저장' }}
|
||||
</button>
|
||||
<button v-if="isEditing" type="button" class="btn btn-sm btn-danger fw-bold px-3" @click="handleDelete">
|
||||
<span class="hotkey-badge me-1">F5</span>삭제
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary fw-bold px-3" @click="emit('cancel')">
|
||||
취소
|
||||
</button>
|
||||
</div>
|
||||
<Dialog
|
||||
:visible="isOpen"
|
||||
:header="title || '등록/수정 팝업'"
|
||||
:modal="true"
|
||||
:closable="true"
|
||||
:dismissableMask="true"
|
||||
class="quant-form-modal max-w-md w-full"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<div class="p-4 text-xs text-slate-800 leading-relaxed">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-3">
|
||||
<div v-for="field in fields" :key="field.name" class="col-md-6 col-12">
|
||||
<label class="form-label fw-bold fs-7 mb-1">
|
||||
<span v-if="field.required" class="text-danger me-1">*</span>{{ field.label }}
|
||||
</label>
|
||||
|
||||
<template v-if="field.type === 'select'">
|
||||
<select v-model="formData[field.name]" class="form-select form-select-sm fw-bold">
|
||||
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'textarea'">
|
||||
<textarea v-model="formData[field.name]" class="form-control form-control-sm fw-bold" rows="3" :placeholder="field.placeholder"></textarea>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'checkbox'">
|
||||
<div class="form-check mt-2">
|
||||
<input v-model="formData[field.name]" type="checkbox" class="form-check-input" :id="field.name" />
|
||||
<label class="form-check-label fs-7 fw-bold" :for="field.name">{{ field.label }}</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<input
|
||||
v-model="formData[field.name]"
|
||||
:type="field.type || 'text'"
|
||||
class="form-control form-control-sm fw-bold"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
</template>
|
||||
</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('save')">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bg-navy {
|
||||
background-color: #1E293B;
|
||||
}
|
||||
.hotkey-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import Dialog from 'primevue/dialog';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
const onVisibleChange = (val: boolean) => {
|
||||
if (!val) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,62 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number
|
||||
type?: 'text' | 'currency' | 'date'
|
||||
placeholder?: string
|
||||
readonly?: boolean
|
||||
required?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'enter'])
|
||||
|
||||
const isFocused = ref(false)
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
if (props.type === 'currency' && props.modelValue) {
|
||||
const num = String(props.modelValue).replace(/[^0-9.-]/g, '')
|
||||
if (!num) return ''
|
||||
return Number(num).toLocaleString('ko-KR')
|
||||
}
|
||||
if (props.type === 'date' && String(props.modelValue).length === 8) {
|
||||
const v = String(props.modelValue)
|
||||
return `${v.substring(0,4)}-${v.substring(4,6)}-${v.substring(6,8)}`
|
||||
}
|
||||
return props.modelValue
|
||||
})
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
emit('enter')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue InputText Adapter Wrapper: QuantInput -->
|
||||
<template>
|
||||
<div style="display: inline-flex; align-items: center; width: 100%;">
|
||||
<input
|
||||
:value="formattedValue"
|
||||
:type="type === 'currency' ? 'text' : type === 'date' ? 'text' : 'text'"
|
||||
<div class="quant-input-wrapper 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
|
||||
:id="id"
|
||||
:type="type || 'text'"
|
||||
:modelValue="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:style="{
|
||||
borderColor: isFocused ? '#2980B9' : '#CBD5E1',
|
||||
boxShadow: isFocused ? '0 0 4px rgba(41, 128, 185, 0.4)' : 'none',
|
||||
backgroundColor: readonly ? '#ECF0F1' : '#FFFFFF',
|
||||
textAlign: type === 'currency' ? 'right' : 'left',
|
||||
color: type === 'currency' && String(modelValue).startsWith('-') ? '#E74C3C' : '#2C3E50'
|
||||
}"
|
||||
style="width: 100%; padding: 4px 8px; border: 1px solid #CBD5E1; border-radius: 2px; font-size: 12px; font-weight: bold; outline: none; transition: border-color 0.2s;"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
@input="onInput"
|
||||
@keydown="onKeyDown"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext';
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string;
|
||||
label?: string;
|
||||
type?: string;
|
||||
modelValue?: string | number;
|
||||
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,63 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'select'])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const items = ref([
|
||||
{ code: '005930', name: '삼성전자', category: 'KOSPI200' },
|
||||
{ code: '000660', name: 'SK하이닉스', category: 'KOSPI200' },
|
||||
{ code: '035420', name: 'NAVER', category: 'KOSPI200' },
|
||||
{ code: '035720', name: '카카오', category: 'KOSPI200' }
|
||||
])
|
||||
|
||||
const close = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
emit('select', item)
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue Dialog Adapter Wrapper: QuantLookupModal -->
|
||||
<template>
|
||||
<div v-if="visible" style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); z-index: 9999; display: flex; align-items: center; justify-content: center;">
|
||||
<div style="background: white; width: 500px; border-radius: 4px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
|
||||
<div style="background: #34495E; color: white; padding: 10px 16px; font-weight: bold; display: flex; justify-content: space-between;">
|
||||
<span><i class="ti ti-search me-1"></i> F2 코드 팝업 룩업 (Type 5 Modal)</span>
|
||||
<button style="background: transparent; border: none; color: white; cursor: pointer; font-weight: bold;" @click="close">✕ (Esc)</button>
|
||||
<Dialog
|
||||
:visible="isOpen"
|
||||
:header="title || '코드 Lookup 검색 팝업 (F2)'"
|
||||
:modal="true"
|
||||
:closable="true"
|
||||
:dismissableMask="true"
|
||||
class="quant-lookup-modal max-w-lg w-full"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<div class="p-4 text-xs text-slate-800 flex flex-col gap-3">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" v-model="searchQuery" placeholder="코드 또는 명칭 검색" class="flex-1 h-8 px-2 border rounded" />
|
||||
<button type="button" class="px-3 bg-slate-800 text-white font-bold rounded" @click="onSearch">검색</button>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px;">
|
||||
<input v-model="searchQuery" type="text" placeholder="종목명 또는 코드 검색 (F2)..." style="width: 100%; box-sizing: border-box; padding: 6px 12px; border: 2px solid #2980B9; border-radius: 3px; font-weight: bold;" />
|
||||
<div style="max-height: 250px; overflow-y: auto; margin-top: 12px; border: 1px solid #CBD5E1;">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background: #F8FAFC;">
|
||||
<th style="padding: 6px; border-bottom: 1px solid #CBD5E1; text-align: left;">코드</th>
|
||||
<th style="padding: 6px; border-bottom: 1px solid #CBD5E1; text-align: left;">종목명</th>
|
||||
<th style="padding: 6px; border-bottom: 1px solid #CBD5E1; text-align: left;">분류</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in items" :key="item.code" style="cursor: pointer; border-bottom: 1px solid #ECF0F1;" @click="selectItem(item)">
|
||||
<td style="padding: 6px; font-family: monospace;">{{ item.code }}</td>
|
||||
<td style="padding: 6px; font-weight: bold;">{{ item.name }}</td>
|
||||
<td style="padding: 6px; color: #7F8C8D;">{{ item.category }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #F4F6F9; padding: 8px 16px; text-align: right; border-top: 1px solid #CBD5E1;">
|
||||
<button style="background: #2C3E50; color: white; border: none; padding: 4px 12px; border-radius: 3px; cursor: pointer;" @click="close">닫기 (Esc)</button>
|
||||
<div class="border rounded overflow-hidden">
|
||||
<table class="w-full text-xs text-left">
|
||||
<thead class="bg-slate-100 font-bold border-b">
|
||||
<tr>
|
||||
<th class="p-2 border-r w-24">코드</th>
|
||||
<th class="p-2 border-r">명칭</th>
|
||||
<th class="p-2 text-center w-16">선택</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in mockLookupList" :key="item.code" class="border-b hover:bg-slate-50">
|
||||
<td class="p-2 border-r font-mono font-bold">{{ item.code }}</td>
|
||||
<td class="p-2 border-r">{{ item.name }}</td>
|
||||
<td class="p-2 text-center">
|
||||
<button type="button" class="text-blue-600 font-bold hover:underline" @click="onSelect(item)">선택</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'select']);
|
||||
|
||||
const searchQuery = ref('');
|
||||
const mockLookupList = ref([
|
||||
{ code: 'CUST-001', name: '삼성전자(주)' },
|
||||
{ code: 'CUST-002', name: 'SK하이닉스(주)' },
|
||||
{ code: 'WH-SEOUL', name: '서울 중앙 물류 창고' }
|
||||
]);
|
||||
|
||||
const onSearch = () => {
|
||||
console.log('Lookup search:', searchQuery.value);
|
||||
};
|
||||
|
||||
const onSelect = (item: any) => {
|
||||
emit('select', item);
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const onVisibleChange = (val: boolean) => {
|
||||
if (!val) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
modelValue: string | number
|
||||
name: string
|
||||
options: Array<{ label: string; value: string | number }>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue RadioButton Adapter Wrapper: QuantRadio -->
|
||||
<template>
|
||||
<div style="display: inline-flex; gap: 12px; align-items: center;">
|
||||
<label v-for="opt in options" :key="opt.value" style="display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: bold; cursor: pointer; color: #2C3E50;">
|
||||
<input
|
||||
type="radio"
|
||||
:name="name"
|
||||
:value="opt.value"
|
||||
:checked="modelValue === opt.value"
|
||||
:disabled="readonly"
|
||||
style="cursor: pointer; accent-color: #2980B9;"
|
||||
@change="emit('update:modelValue', opt.value)"
|
||||
/>
|
||||
{{ opt.label }}
|
||||
<div class="quant-radio-wrapper flex items-center gap-2 cursor-pointer select-none">
|
||||
<RadioButton
|
||||
:id="id"
|
||||
:modelValue="modelValue"
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
class="text-blue-600"
|
||||
@update:modelValue="onChange"
|
||||
/>
|
||||
<label v-if="label" :for="id" class="text-xs font-bold text-slate-800 cursor-pointer">
|
||||
{{ label }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RadioButton from 'primevue/radiobutton';
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string;
|
||||
modelValue?: any;
|
||||
value: any;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const onChange = (val: any) => {
|
||||
emit('update:modelValue', val);
|
||||
emit('change', val);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,59 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialLeftWidth?: number
|
||||
minLeftPercent?: number
|
||||
maxLeftPercent?: number
|
||||
}>(), {
|
||||
initialLeftWidth: 30,
|
||||
minLeftPercent: 15,
|
||||
maxLeftPercent: 75
|
||||
})
|
||||
|
||||
const leftWidthPercent = ref(props.initialLeftWidth)
|
||||
const isDragging = ref(false)
|
||||
|
||||
const startDrag = () => {
|
||||
isDragging.value = true
|
||||
window.addEventListener('mousemove', onDrag)
|
||||
window.addEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
const onDrag = (e: MouseEvent) => {
|
||||
if (!isDragging.value) return
|
||||
const containerWidth = window.innerWidth
|
||||
const newPercent = (e.clientX / containerWidth) * 100
|
||||
if (newPercent > props.minLeftPercent && newPercent < props.maxLeftPercent) {
|
||||
leftWidthPercent.value = newPercent
|
||||
}
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
isDragging.value = false
|
||||
window.removeEventListener('mousemove', onDrag)
|
||||
window.removeEventListener('mouseup', stopDrag)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue Splitter Adapter Wrapper: QuantSplitter -->
|
||||
<template>
|
||||
<div style="display: flex; height: 100%; width: 100%; position: relative; overflow: hidden; user-select: none;">
|
||||
<!-- Left Slot Container -->
|
||||
<div :style="{ width: leftWidthPercent + '%' }" style="overflow: hidden; display: flex; flex-direction: column;">
|
||||
<slot name="left" :left-width="leftWidthPercent" />
|
||||
</div>
|
||||
|
||||
<!-- Drag Handle Bar -->
|
||||
<div
|
||||
style="width: 8px; background: #CBD5E1; cursor: col-resize; display: flex; align-items: center; justify-content: center; z-index: 10; flex-shrink: 0;"
|
||||
title="드래그하여 분할 비율 조절"
|
||||
@mousedown="startDrag">
|
||||
<div style="width: 2px; height: 24px; background: #7F8C8D;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right Slot Container -->
|
||||
<div :style="{ width: (100 - leftWidthPercent) + '%' }" style="overflow: hidden; display: flex; flex-direction: column;">
|
||||
<slot name="right" :right-width="100 - leftWidthPercent" />
|
||||
</div>
|
||||
<div class="quant-splitter-wrapper w-full h-full">
|
||||
<Splitter class="w-full h-full border border-slate-300 rounded overflow-hidden">
|
||||
<SplitterPanel :size="leftSize" class="flex items-center justify-center p-2">
|
||||
<slot name="left"></slot>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel :size="100 - leftSize" class="flex items-center justify-center p-2">
|
||||
<slot name="right"></slot>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import Splitter from 'primevue/splitter';
|
||||
import SplitterPanel from 'primevue/splitterpanel';
|
||||
|
||||
const props = defineProps<{
|
||||
leftWidth?: string;
|
||||
}>();
|
||||
|
||||
const leftSize = computed(() => {
|
||||
if (!props.leftWidth) return 50;
|
||||
const parsed = parseInt(props.leftWidth.replace('%', ''), 10);
|
||||
return isNaN(parsed) ? 50 : parsed;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
rows?: number
|
||||
placeholder?: string
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
</script>
|
||||
|
||||
<!-- PrimeVue Textarea Adapter Wrapper: QuantTextArea -->
|
||||
<template>
|
||||
<textarea
|
||||
:value="modelValue"
|
||||
:rows="rows || 3"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:style="{ backgroundColor: readonly ? '#ECF0F1' : '#FFFFFF' }"
|
||||
style="width: 100%; box-sizing: border-box; padding: 6px; border: 1px solid #CBD5E1; border-radius: 2px; font-size: 12px; font-weight: bold; outline: none; resize: vertical;"
|
||||
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
||||
/>
|
||||
<div class="quant-textarea-wrapper w-full">
|
||||
<Textarea
|
||||
:id="id"
|
||||
:modelValue="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:rows="rows || 3"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
class="w-full p-2.5 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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Textarea from 'primevue/textarea';
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string;
|
||||
modelValue?: string;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
Reference in New Issue
Block a user