- 검색 필터(구분·분류·계좌·태그) select → SheetSelect(바텀시트) - 내역 추가/수정 폼: 계좌·상환대상 ChipSelect → SheetSelect, 분류 CategoryPicker → 트리거 → 바텀시트(선택 시 자동 닫힘) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@ import { useDialog } from '@/composables/dialog'
|
|||||||
import { appLock } from '@/composables/appLock'
|
import { appLock } from '@/composables/appLock'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
import CategoryPicker from '@/components/ui/CategoryPicker.vue'
|
import CategoryPicker from '@/components/ui/CategoryPicker.vue'
|
||||||
import ChipSelect from '@/components/ui/ChipSelect.vue'
|
import SheetSelect from '@/components/ui/SheetSelect.vue'
|
||||||
|
import BottomSheet from '@/components/ui/BottomSheet.vue'
|
||||||
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
|
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
|
||||||
import { cardNotif } from '@/native/cardNotif'
|
import { cardNotif } from '@/native/cardNotif'
|
||||||
import { CARD_NOTIF_ENABLED } from '@/config/features'
|
import { CARD_NOTIF_ENABLED } from '@/config/features'
|
||||||
@@ -87,6 +88,9 @@ function resetFilters() {
|
|||||||
const formOpen = ref(false)
|
const formOpen = ref(false)
|
||||||
const editId = ref(null)
|
const editId = ref(null)
|
||||||
const form = reactive({ entryDate: '', type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: '', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, annualFee: null, installmentMonths: '', currency: 'KRW', foreignAmount: null, rate: null })
|
const form = reactive({ entryDate: '', type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: '', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, annualFee: null, installmentMonths: '', currency: 'KRW', foreignAmount: null, rate: null })
|
||||||
|
// 분류 선택 바텀시트 — 최종 분류가 정해지면 자동 닫힘
|
||||||
|
const catSheet = ref(false)
|
||||||
|
watch(() => form.category, (v) => { if (v) catSheet.value = false })
|
||||||
|
|
||||||
// ===== 외화 결제 =====
|
// ===== 외화 결제 =====
|
||||||
const CURRENCIES = ['KRW', 'USD', 'JPY', 'EUR', 'CNY', 'GBP', 'AUD', 'CAD', 'HKD', 'SGD', 'THB', 'VND', 'TWD', 'PHP', 'MYR']
|
const CURRENCIES = ['KRW', 'USD', 'JPY', 'EUR', 'CNY', 'GBP', 'AUD', 'CAD', 'HKD', 'SGD', 'THB', 'VND', 'TWD', 'PHP', 'MYR']
|
||||||
@@ -441,6 +445,26 @@ function onTypeChange() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 검색·필터 바텀시트 옵션(맨 앞 '전체'로 해제 가능)
|
||||||
|
const filterTypeOptions = [
|
||||||
|
{ value: '', label: '구분 전체' },
|
||||||
|
{ value: 'INCOME', label: '수입' },
|
||||||
|
{ value: 'EXPENSE', label: '지출' },
|
||||||
|
{ value: 'TRANSFER', label: '이체' },
|
||||||
|
]
|
||||||
|
const filterCategoryOptions = computed(() => [
|
||||||
|
{ value: '', label: '분류 전체' },
|
||||||
|
...allCategoryNames.value.map((c) => ({ value: c, label: c })),
|
||||||
|
])
|
||||||
|
const filterWalletOptions = computed(() => [
|
||||||
|
{ value: '', label: '계좌 전체' },
|
||||||
|
...wallets.value.map((w) => ({ value: w.id, label: w.name })),
|
||||||
|
])
|
||||||
|
const filterTagOptions = computed(() => [
|
||||||
|
{ value: '', label: '태그 전체' },
|
||||||
|
...tagOptions.value.map((t) => ({ value: t.id, label: t.name })),
|
||||||
|
])
|
||||||
|
|
||||||
// 태그 (태그 관리에 등록된 태그 선택)
|
// 태그 (태그 관리에 등록된 태그 선택)
|
||||||
const tagOptions = ref([]) // [{ id, name }]
|
const tagOptions = ref([]) // [{ id, name }]
|
||||||
const selectedTagIds = ref([])
|
const selectedTagIds = ref([])
|
||||||
@@ -1021,24 +1045,10 @@ onMounted(async () => {
|
|||||||
v-model="filters.keyword" type="text" class="f-keyword"
|
v-model="filters.keyword" type="text" class="f-keyword"
|
||||||
placeholder="메모·분류 검색" @keyup.enter="applyFilters"
|
placeholder="메모·분류 검색" @keyup.enter="applyFilters"
|
||||||
/>
|
/>
|
||||||
<select v-model="filters.type">
|
<div class="f-sel"><SheetSelect v-model="filters.type" :options="filterTypeOptions" title="구분" placeholder="구분 전체" /></div>
|
||||||
<option value="">구분 전체</option>
|
<div class="f-sel"><SheetSelect v-model="filters.category" :options="filterCategoryOptions" title="분류" placeholder="분류 전체" /></div>
|
||||||
<option value="INCOME">수입</option>
|
<div class="f-sel"><SheetSelect v-model="filters.walletId" :options="filterWalletOptions" title="계좌" placeholder="계좌 전체" /></div>
|
||||||
<option value="EXPENSE">지출</option>
|
<div class="f-sel"><SheetSelect v-model="filters.tagId" :options="filterTagOptions" title="태그" placeholder="태그 전체" /></div>
|
||||||
<option value="TRANSFER">이체</option>
|
|
||||||
</select>
|
|
||||||
<select v-model="filters.category">
|
|
||||||
<option value="">분류 전체</option>
|
|
||||||
<option v-for="c in allCategoryNames" :key="c" :value="c">{{ c }}</option>
|
|
||||||
</select>
|
|
||||||
<select v-model="filters.walletId">
|
|
||||||
<option value="">계좌 전체</option>
|
|
||||||
<option v-for="w in wallets" :key="w.id" :value="w.id">{{ w.name }}</option>
|
|
||||||
</select>
|
|
||||||
<select v-model="filters.tagId">
|
|
||||||
<option value="">태그 전체</option>
|
|
||||||
<option v-for="t in tagOptions" :key="t.id" :value="t.id">{{ t.name }}</option>
|
|
||||||
</select>
|
|
||||||
<IconBtn icon="search" title="적용" variant="primary" @click="applyFilters" />
|
<IconBtn icon="search" title="적용" variant="primary" @click="applyFilters" />
|
||||||
<IconBtn icon="refresh" title="초기화" @click="resetFilters" />
|
<IconBtn icon="refresh" title="초기화" @click="resetFilters" />
|
||||||
</div>
|
</div>
|
||||||
@@ -1186,11 +1196,12 @@ onMounted(async () => {
|
|||||||
{{ k.label }}
|
{{ k.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ChipSelect
|
<SheetSelect
|
||||||
v-if="form.walletKind"
|
v-if="form.walletKind"
|
||||||
v-model="form.walletId"
|
v-model="form.walletId"
|
||||||
:options="fromWalletOptions"
|
:options="fromWalletOptions"
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
|
title="계좌 선택" placeholder="계좌를 선택하세요"
|
||||||
:empty-text="`등록된 ${walletKindLabel}이(가) 없습니다`"
|
:empty-text="`등록된 ${walletKindLabel}이(가) 없습니다`"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1212,11 +1223,12 @@ onMounted(async () => {
|
|||||||
{{ k.label }}
|
{{ k.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ChipSelect
|
<SheetSelect
|
||||||
v-if="form.toWalletKind"
|
v-if="form.toWalletKind"
|
||||||
v-model="form.toWalletId"
|
v-model="form.toWalletId"
|
||||||
:options="toWalletOptions"
|
:options="toWalletOptions"
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
|
title="입금 계좌 선택" placeholder="계좌를 선택하세요"
|
||||||
empty-text="등록된 계좌가 없습니다"
|
empty-text="등록된 계좌가 없습니다"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1226,10 +1238,11 @@ onMounted(async () => {
|
|||||||
<template v-if="isRepayment">
|
<template v-if="isRepayment">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="field-label">대상(대출/카드)</span>
|
<span class="field-label">대상(대출/카드)</span>
|
||||||
<ChipSelect
|
<SheetSelect
|
||||||
v-model="form.toWalletId"
|
v-model="form.toWalletId"
|
||||||
:options="repayTargetOptions"
|
:options="repayTargetOptions"
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
|
title="상환 대상 선택" placeholder="대출/카드를 선택하세요"
|
||||||
empty-text="등록된 대출/카드가 없습니다"
|
empty-text="등록된 대출/카드가 없습니다"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1264,7 +1277,17 @@ onMounted(async () => {
|
|||||||
<label v-if="repayTargetIsCard">연회비<input v-model.number="form.annualFee" type="number" min="0" placeholder="원 (지출·분류 연회비)" :disabled="submitting" /></label>
|
<label v-if="repayTargetIsCard">연회비<input v-model.number="form.annualFee" type="number" min="0" placeholder="원 (지출·분류 연회비)" :disabled="submitting" /></label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<label v-if="form.type === 'INCOME' || form.type === 'EXPENSE'">분류
|
<div v-if="form.type === 'INCOME' || form.type === 'EXPENSE'" class="field">
|
||||||
|
<span class="field-label">분류</span>
|
||||||
|
<button
|
||||||
|
type="button" class="sheet-trigger" :class="{ empty: !form.category }"
|
||||||
|
:disabled="submitting" @click="catSheet = true"
|
||||||
|
>
|
||||||
|
<span class="st-label">{{ form.category || '분류를 선택하세요' }}</span>
|
||||||
|
<span class="st-caret">▾</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<BottomSheet v-model="catSheet" title="분류 선택">
|
||||||
<CategoryPicker
|
<CategoryPicker
|
||||||
v-model="form.category"
|
v-model="form.category"
|
||||||
:type="form.type"
|
:type="form.type"
|
||||||
@@ -1272,7 +1295,7 @@ onMounted(async () => {
|
|||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
@category-added="loadCategories"
|
@category-added="loadCategories"
|
||||||
/>
|
/>
|
||||||
</label>
|
</BottomSheet>
|
||||||
<label v-if="!isRepayment" class="amount-label">
|
<label v-if="!isRepayment" class="amount-label">
|
||||||
금액
|
금액
|
||||||
<div class="amount-row">
|
<div class="amount-row">
|
||||||
@@ -1502,6 +1525,43 @@ button.primary {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
}
|
}
|
||||||
|
.filter-bar .f-sel {
|
||||||
|
flex: 1 1 7rem;
|
||||||
|
min-width: 6.5rem;
|
||||||
|
}
|
||||||
|
/* 시트 트리거(필드형) — 분류 선택 등 */
|
||||||
|
.sheet-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.7rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.sheet-trigger:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.sheet-trigger.empty .st-label {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.st-label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.st-caret {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.summary {
|
.summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user