- 통계: 순자산 추이 주간(최근 3개월), 이번달 예상 지출(run-rate), 지난달 대비 지출 - 가계부: 내역→고정지출 바로 등록, 날짜별 접기/펴기(기본 접힘·오늘만 펼침·모두 토글) - 계좌 관리 탭 한 줄(넘치면 가로 스크롤) - 릴리스 노트 갱신(23~32) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { accountApi } from '@/api/accountApi'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
|
||||
@@ -328,6 +328,28 @@ function dayLabel(d) {
|
||||
return `${dt.getMonth() + 1}월 ${dt.getDate()}일 (${wd})`
|
||||
}
|
||||
|
||||
// ===== 날짜별 접기/펴기 (기본 접힘, 오늘만 펼침) =====
|
||||
const expandedDates = ref(new Set())
|
||||
function isExpanded(date) {
|
||||
return expandedDates.value.has(date)
|
||||
}
|
||||
function toggleDate(date) {
|
||||
const s = new Set(expandedDates.value)
|
||||
s.has(date) ? s.delete(date) : s.add(date)
|
||||
expandedDates.value = s
|
||||
}
|
||||
const allExpanded = computed(() => {
|
||||
const days = entriesByDay.value
|
||||
return days.length > 0 && days.every((g) => expandedDates.value.has(g.date))
|
||||
})
|
||||
function toggleAll() {
|
||||
expandedDates.value = allExpanded.value ? new Set() : new Set(entriesByDay.value.map((g) => g.date))
|
||||
}
|
||||
// 월이 바뀌면 모두 접고 오늘 날짜만 펼침 (현재 월일 때만 오늘이 목록에 존재)
|
||||
watch([year, month], () => {
|
||||
expandedDates.value = new Set([todayStr()])
|
||||
}, { immediate: true })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
@@ -511,6 +533,49 @@ async function remove(e) {
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 수정 중인 내역을 '매월' 고정지출로 바로 등록 (주기·시작일은 고정지출 화면에서 변경)
|
||||
async function registerAsRecurring() {
|
||||
if (form.type === 'REPAYMENT') return // 고정지출은 수입/지출/이체만
|
||||
const amount = Number(form.amount)
|
||||
if (!amount || amount <= 0) {
|
||||
formError.value = '금액을 올바르게 입력하세요.'
|
||||
return
|
||||
}
|
||||
const title =
|
||||
(form.memo || '').trim() ||
|
||||
(form.category || '').trim() ||
|
||||
(form.type === 'INCOME' ? '정기 수입' : form.type === 'TRANSFER' ? '정기 이체' : '고정 지출')
|
||||
const base = form.entryDate || todayStr()
|
||||
const [y, m, d] = base.split('-').map(Number)
|
||||
const dom = d
|
||||
// 이번 회차 중복 방지: 시작일을 내역 다음날로 → 다음 발생부터 생성
|
||||
const start = new Date(y, m - 1, d + 1)
|
||||
const startStr = `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}-${String(start.getDate()).padStart(2, '0')}`
|
||||
if (!confirm(`'${title}'\n매월 ${dom}일 · ${won(amount)} 고정지출로 등록할까요?\n(주기·시작일은 고정지출 화면에서 변경할 수 있어요)`)) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await accountApi.createRecurring({
|
||||
title,
|
||||
type: form.type,
|
||||
amount,
|
||||
category: form.type === 'TRANSFER' ? null : form.category || null,
|
||||
memo: form.memo || null,
|
||||
walletId: form.walletId || null,
|
||||
toWalletId: form.type === 'TRANSFER' ? form.toWalletId || null : null,
|
||||
frequency: 'MONTHLY',
|
||||
dayOfMonth: dom,
|
||||
startDate: startStr,
|
||||
active: true,
|
||||
})
|
||||
formOpen.value = false
|
||||
alert('고정지출로 등록했습니다. 고정지출 화면에서 주기를 변경할 수 있어요.')
|
||||
} catch (e) {
|
||||
formError.value = e.response?.data?.message || '고정지출 등록에 실패했습니다.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 진입 시 밀린 정기 거래를 먼저 반영한 뒤 목록을 불러온다 (중복 없이)
|
||||
try {
|
||||
@@ -596,15 +661,22 @@ onMounted(async () => {
|
||||
<p v-if="loading" class="msg">불러오는 중...</p>
|
||||
|
||||
<div v-else-if="entries.length" class="day-list">
|
||||
<div class="list-tools">
|
||||
<button type="button" class="toggle-all" @click="toggleAll">{{ allExpanded ? '모두 접기 ▴' : '모두 펴기 ▾' }}</button>
|
||||
</div>
|
||||
<section v-for="g in entriesByDay" :key="g.date" class="day-group">
|
||||
<div class="day-head">
|
||||
<span class="day-date">{{ dayLabel(g.date) }}</span>
|
||||
<div class="day-head" @click="toggleDate(g.date)">
|
||||
<span class="day-head-left">
|
||||
<span class="day-toggle">{{ isExpanded(g.date) ? '▾' : '▸' }}</span>
|
||||
<span class="day-date">{{ dayLabel(g.date) }}</span>
|
||||
<span v-if="!isExpanded(g.date)" class="day-count">{{ g.items.length }}건</span>
|
||||
</span>
|
||||
<span class="day-sums">
|
||||
<span v-if="g.income" class="income">+{{ won(g.income) }}</span>
|
||||
<span v-if="g.expense" class="expense">-{{ won(g.expense) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<ul class="day-items">
|
||||
<ul v-show="isExpanded(g.date)" class="day-items">
|
||||
<li v-for="e in g.items" :key="e.id" class="entry-item" :class="{ 'is-pending': e.pending }">
|
||||
<div class="ei-line1">
|
||||
<span class="ei-cat">
|
||||
@@ -773,6 +845,14 @@ onMounted(async () => {
|
||||
|
||||
<p v-if="formError" class="msg error">{{ formError }}</p>
|
||||
|
||||
<button
|
||||
v-if="editId && form.type !== 'REPAYMENT'"
|
||||
type="button"
|
||||
class="to-recurring"
|
||||
:disabled="submitting"
|
||||
@click="registerAsRecurring"
|
||||
>🔁 이 내역을 고정지출로 등록</button>
|
||||
|
||||
<div class="buttons">
|
||||
<IconBtn icon="close" title="취소" @click="formOpen = false" />
|
||||
<IconBtn icon="save" :title="editId ? '수정' : '등록'" variant="primary" type="submit" :disabled="submitting" />
|
||||
@@ -924,6 +1004,21 @@ button.primary {
|
||||
.day-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.list-tools {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.toggle-all {
|
||||
border: 1px solid var(--color-border);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 0.78rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.day-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -931,11 +1026,27 @@ button.primary {
|
||||
padding: 0.3rem 0.1rem;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
margin-bottom: 0.2rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.day-head-left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.day-toggle {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.day-date {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.day-count {
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.day-sums {
|
||||
display: flex;
|
||||
gap: 0.7rem;
|
||||
@@ -1348,6 +1459,23 @@ button.primary {
|
||||
border-radius: 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.to-recurring {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 0.6rem;
|
||||
padding: 0.55rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
background: transparent;
|
||||
border: 1px dashed hsla(160, 100%, 37%, 0.6);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.to-recurring:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
Reference in New Issue
Block a user