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

This commit is contained in:
2026-07-26 02:46:10 +09:00
parent 64009419b3
commit aa8438e9bf
12 changed files with 400 additions and 418 deletions
@@ -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>
+31 -19
View File
@@ -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>
+44 -23
View File
@@ -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>
+38 -96
View File
@@ -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>
+35 -56
View File
@@ -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>
+31 -23
View File
@@ -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>
+26 -56
View File
@@ -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>
+34 -20
View File
@@ -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>
+4
View File
@@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}
View File