feat(entries): 검색 필터·내역 폼 선택을 바텀시트로
Deploy / deploy (push) Failing after 14m48s

- 검색 필터(구분·분류·계좌·태그) select → SheetSelect(바텀시트)
- 내역 추가/수정 폼: 계좌·상환대상 ChipSelect → SheetSelect,
  분류 CategoryPicker → 트리거 → 바텀시트(선택 시 자동 닫힘)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-09 23:06:42 +09:00
parent d20d3c3027
commit c7875b0577
+84 -24
View File
@@ -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;