Files
money.sori.studio/pages/assets.vue
2026-08-13 16:38:59 +09:00

212 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section class="min-h-dvh bg-paper">
<header class="sticky top-0 z-10 border-b border-line bg-white">
<div class="flex h-14 items-center justify-between px-4">
<div class="flex items-center gap-2">
<button
v-if="selectedAsset"
type="button"
class="flex h-9 w-9 items-center justify-center rounded-md text-2xl leading-none active:bg-paper"
aria-label="자산 목록으로"
@click="selectedAssetId = ''"
>
‹
</button>
<h1 class="text-lg font-bold">{{ selectedAsset?.name || '자산' }}</h1>
</div>
<div class="flex items-center gap-1">
<button class="flex h-9 w-8 items-center justify-center rounded-md text-2xl leading-none active:bg-paper" type="button" @click="moveMonth(-1)">‹</button>
<button class="rounded-md px-2 py-2 text-base font-semibold active:bg-paper" type="button" @click="monthPickerOpen = true">
{{ displayMonth }}
</button>
<button class="flex h-9 w-8 items-center justify-center rounded-md text-2xl leading-none active:bg-paper" type="button" @click="moveMonth(1)">›</button>
</div>
</div>
<div v-if="!selectedAsset" class="grid grid-cols-3 border-t border-line px-4 py-3 text-center">
<div>
<p class="text-xs font-semibold text-muted">누적형</p>
<p class="mt-1 font-bold text-brand-700">{{ formatWon(cumulativeTotal) }}</p>
</div>
<div>
<p class="text-xs font-semibold text-muted">월별</p>
<p class="mt-1 font-bold text-red-600">{{ formatWon(monthlyTotal) }}</p>
</div>
<div>
<p class="text-xs font-semibold text-muted">합계</p>
<p class="mt-1 font-bold">{{ formatWon(totalBalance) }}</p>
</div>
</div>
</header>
<div v-if="!selectedAsset" class="divide-y divide-line border-b border-line bg-white">
<button
v-for="asset in assets"
:key="asset.id"
type="button"
class="flex w-full items-center justify-between px-4 py-4 text-left active:bg-paper"
@click="selectedAssetId = asset.id"
>
<span>
<span class="block text-base font-semibold text-ink">{{ asset.name }}</span>
<span class="mt-1 block text-xs text-muted">{{ asset.balanceType === 'cumulative' ? '계속 누적' : `${displayMonth}만 계산` }}</span>
</span>
<span class="text-base font-bold">{{ formatWon(asset.balance || 0) }}</span>
</button>
<p v-if="!assets.length" class="px-4 py-12 text-center text-muted">등록된 자산이 없습니다.</p>
</div>
<div v-else>
<section class="border-b border-line bg-white px-4 py-4">
<p class="text-sm font-semibold text-muted">현재 표시 잔액</p>
<p class="mt-1 text-2xl font-bold">{{ formatWon(selectedAsset.balance || 0) }}</p>
<div class="mt-5 grid grid-cols-3 text-center">
<div>
<p class="text-sm text-muted">입금</p>
<p class="mt-1 font-bold text-brand-700">{{ formatWon(assetSummary.income) }}</p>
</div>
<div>
<p class="text-sm text-muted">출금</p>
<p class="mt-1 font-bold text-red-600">{{ formatWon(assetSummary.expense) }}</p>
</div>
<div>
<p class="text-sm text-muted">합계</p>
<p class="mt-1 font-bold">{{ formatWon(assetSummary.diff) }}</p>
</div>
</div>
</section>
<div class="divide-y divide-line bg-white">
<section v-for="group in groupedTransactions" :key="group.date" class="py-2">
<div class="flex items-center justify-between px-4 py-2">
<div class="flex items-center gap-2">
<strong class="text-2xl">{{ group.day }}</strong>
<span class="rounded bg-slate-200 px-2 py-1 text-xs font-semibold text-muted">{{ group.weekday }}</span>
<span class="text-sm text-muted">{{ group.monthLabel }}</span>
</div>
<div class="flex gap-4 text-sm font-bold">
<span class="text-brand-700">{{ formatWon(group.income) }}</span>
<span class="text-red-600">{{ formatWon(group.expense) }}</span>
</div>
</div>
<TransactionListRow
v-for="tx in group.items"
:key="tx.id"
:transaction="tx"
/>
</section>
<p v-if="!assetTransactions.length" class="px-4 py-12 text-center text-muted">
이 자산의 {{ displayMonth }} 거래가 없습니다.
</p>
</div>
</div>
<Teleport to="body">
<Transition enter-active-class="transition duration-200 ease-out" enter-from-class="opacity-0" enter-to-class="opacity-100" leave-active-class="transition duration-150 ease-in" leave-from-class="opacity-100" leave-to-class="opacity-0">
<div v-if="monthPickerOpen" class="fixed inset-0 z-40 bg-ink/35" @click="monthPickerOpen = false" />
</Transition>
<Transition enter-active-class="transition duration-200 ease-out" enter-from-class="translate-y-full" enter-to-class="translate-y-0" leave-active-class="transition duration-150 ease-in" leave-from-class="translate-y-0" leave-to-class="translate-y-full">
<section v-if="monthPickerOpen" class="fixed inset-x-0 bottom-0 z-50 mx-auto max-w-lg rounded-t-2xl bg-white shadow-2xl">
<div class="border-b border-line px-4 pb-3 pt-2">
<div class="mx-auto mb-3 h-1 w-10 rounded-full bg-line" />
<div class="flex items-center justify-between">
<h2 class="text-lg font-bold">월 선택</h2>
<button type="button" class="text-sm font-semibold text-muted" @click="monthPickerOpen = false">닫기</button>
</div>
</div>
<div class="flex items-center justify-between px-4 py-4">
<button class="text-2xl" type="button" @click="pickerYear--">‹</button>
<strong class="text-xl">{{ pickerYear }}</strong>
<button class="text-2xl" type="button" @click="pickerYear++">›</button>
</div>
<div class="grid grid-cols-4 gap-y-2 px-4 pb-6">
<button
v-for="month in 12"
:key="month"
type="button"
class="rounded-md py-3 text-base font-bold"
:class="isSelectedPickerMonth(month) ? 'bg-red-50 text-red-500' : 'active:bg-paper'"
@click="selectPickerMonth(month)"
>
{{ month }}월
</button>
</div>
</section>
</Transition>
</Teleport>
</section>
</template>
<script setup lang="ts">
import { addMonths, format, parse } from 'date-fns'
import type { Transaction } from '~/composables/useMoney'
const selectedMonth = ref(monthKey())
const selectedAssetId = ref('')
const monthPickerOpen = ref(false)
const pickerYear = ref(Number(selectedMonth.value.slice(0, 4)))
const { data: assetData } = await useFetch('/api/assets', { query: { month: selectedMonth } })
const { data: transactionData } = await useFetch('/api/transactions', { query: { month: selectedMonth } })
const assets = computed(() => assetData.value || [])
const selectedAsset = computed(() => assets.value.find((asset: any) => asset.id === selectedAssetId.value))
const displayMonth = computed(() => {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
return format(date, 'yyyy년 M월')
})
const cumulativeTotal = computed(() => assets.value.filter((asset: any) => asset.balanceType === 'cumulative').reduce((sum: number, asset: any) => sum + (asset.balance || 0), 0))
const monthlyTotal = computed(() => assets.value.filter((asset: any) => asset.balanceType === 'monthly_reset').reduce((sum: number, asset: any) => sum + (asset.balance || 0), 0))
const totalBalance = computed(() => assets.value.reduce((sum: number, asset: any) => sum + (asset.balance || 0), 0))
const assetTransactions = computed(() => {
return (transactionData.value?.transactions || []).filter((tx: Transaction) => tx.assetId === selectedAssetId.value)
})
const assetSummary = computed(() => {
return assetTransactions.value.reduce((acc: any, tx: Transaction) => {
acc[tx.type] += tx.amount
acc.diff = acc.income - acc.expense
return acc
}, { income: 0, expense: 0, diff: 0 })
})
const groupedTransactions = computed(() => {
const groups = new Map<string, any>()
for (const tx of assetTransactions.value) {
const date = new Date(tx.occurredAt)
const key = format(date, 'yyyy-MM-dd')
const group = groups.get(key) || {
date: key,
day: format(date, 'd'),
weekday: new Intl.DateTimeFormat('ko-KR', { weekday: 'short' }).format(date),
monthLabel: format(date, 'yyyy.MM'),
income: 0,
expense: 0,
items: []
}
group[tx.type] += tx.amount
group.items.push(tx)
groups.set(key, group)
}
return Array.from(groups.values()).sort((a, b) => b.date.localeCompare(a.date))
})
watch(monthPickerOpen, (open) => {
if (open) pickerYear.value = Number(selectedMonth.value.slice(0, 4))
})
function moveMonth(amount: number) {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
selectedMonth.value = format(addMonths(date, amount), 'yyyy-MM')
}
function isSelectedPickerMonth(month: number) {
return selectedMonth.value === `${pickerYear.value}-${String(month).padStart(2, '0')}`
}
function selectPickerMonth(month: number) {
selectedMonth.value = `${pickerYear.value}-${String(month).padStart(2, '0')}`
monthPickerOpen.value = false
}
</script>