Files
money.sori.studio/pages/transactions/[id].vue
2026-08-13 15:58:42 +09:00

197 lines
7.3 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-screen bg-paper">
<header class="sticky top-0 z-10 flex h-16 items-center justify-between border-b border-line bg-white px-4">
<div class="flex items-center gap-3">
<button
type="button"
class="flex h-10 w-10 items-center justify-center rounded-md text-3xl leading-none active:bg-paper"
aria-label="뒤로"
@click="goBack"
>
‹
</button>
<h1 class="text-xl font-bold">{{ form.type === 'income' ? '수입' : '지출' }}</h1>
</div>
<button type="button" class="text-sm font-bold text-brand-700" :disabled="pending" @click="save">
저장
</button>
</header>
<form class="bg-white px-4 py-5" @submit.prevent="save">
<div class="mb-5 grid grid-cols-2 gap-2">
<button type="button" class="segmented-button border border-line py-3" :class="{ 'border-brand-700 text-brand-700': form.type === 'income' }" @click="setType('income')">수입</button>
<button type="button" class="segmented-button border border-line py-3" :class="{ 'border-red-500 text-red-500': form.type === 'expense' }" @click="setType('expense')">지출</button>
</div>
<div class="divide-y divide-line border-y border-line">
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 py-4">
<label class="text-lg font-semibold text-muted" for="occurredAt">날짜</label>
<input id="occurredAt" v-model="form.occurredAt" class="w-full bg-transparent py-2 text-lg font-semibold outline-none" type="datetime-local" required>
</div>
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 py-4">
<span class="text-lg font-semibold text-muted">자산</span>
<BottomSheetSelect
v-model="form.assetId"
title="자산 선택"
placeholder="자산 선택"
:options="assetOptions"
/>
</div>
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 py-4">
<span class="text-lg font-semibold text-muted">분류</span>
<BottomSheetSelect
v-model="form.categoryId"
title="분류 선택"
placeholder="분류 선택"
:options="categoryOptions"
/>
</div>
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 py-4">
<label class="text-lg font-semibold text-muted" for="amount">금액</label>
<input id="amount" v-model.number="form.amount" class="w-full bg-transparent py-2 text-right text-2xl font-bold outline-none" inputmode="numeric" min="1" type="number" required>
</div>
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 py-4">
<label class="text-lg font-semibold text-muted" for="title">내용</label>
<input id="title" v-model="form.title" class="w-full bg-transparent py-2 text-lg font-semibold outline-none" required>
</div>
</div>
<div class="mt-4 border-y border-line py-4">
<label class="mb-2 block text-lg font-semibold text-muted" for="memo">메모</label>
<textarea id="memo" v-model="form.memo" class="min-h-24 w-full resize-none bg-transparent text-lg outline-none" placeholder="메모"></textarea>
</div>
<div v-if="locationLabel" class="mt-3 flex items-center justify-between border-b border-line py-4">
<a class="font-semibold text-brand-700" :href="mapsUrl" target="_blank" rel="noreferrer">위치 보기</a>
<button type="button" class="text-sm font-semibold text-muted" @click="clearLocation">위치 삭제</button>
</div>
<div class="mt-6 grid grid-cols-3 gap-2">
<button type="button" class="btn-secondary gap-2" @click="remove">삭제</button>
<button type="button" class="btn-secondary gap-2" @click="copy">복사</button>
<button type="button" class="btn-secondary gap-2" disabled>자주사용</button>
</div>
<button class="btn-primary mt-6 w-full" type="submit" :disabled="pending">
{{ pending ? '저장 중...' : '저장하기' }}
</button>
</form>
</section>
</template>
<script setup lang="ts">
type TransactionType = 'income' | 'expense'
const route = useRoute()
const router = useRouter()
const id = computed(() => String(route.params.id))
const pending = ref(false)
const { data: transaction } = await useFetch<any>(() => `/api/transactions/${id.value}`)
const { data: assetsData } = await useFetch('/api/assets')
const { data: categoriesData } = await useFetch('/api/categories')
const form = reactive({
type: 'expense' as TransactionType,
occurredAt: formatDateInput(),
assetId: '',
categoryId: '',
amount: 0,
title: '',
memo: '',
latitude: null as number | null,
longitude: null as number | null
})
const assets = computed(() => assetsData.value || [])
const categories = computed(() => categoriesData.value || [])
const filteredCategories = computed(() => categories.value.filter((category: any) => category.type === form.type && category.active))
const assetOptions = computed(() => assets.value.map((asset: any) => ({
value: asset.id,
label: asset.name,
description: asset.balanceType === 'cumulative' ? '계속 누적' : '매월 새로 시작'
})))
const categoryOptions = computed(() => filteredCategories.value.map((category: any) => ({
value: category.id,
label: category.parent ? `${category.parent.name} > ${category.name}` : category.name,
description: category.type === 'income' ? '수입' : '지출'
})))
const locationLabel = computed(() => form.latitude && form.longitude)
const mapsUrl = computed(() => `https://www.google.com/maps?q=${form.latitude},${form.longitude}`)
watch(transaction, (value) => {
if (!value) return
Object.assign(form, {
type: value.type,
occurredAt: formatDateInput(new Date(value.occurredAt)),
assetId: value.assetId,
categoryId: value.categoryId,
amount: value.amount,
title: value.title,
memo: value.memo || '',
latitude: value.latitude ? Number(value.latitude) : null,
longitude: value.longitude ? Number(value.longitude) : null
})
}, { immediate: true })
watch(filteredCategories, (items: any[]) => {
if (!items.some(item => item.id === form.categoryId)) form.categoryId = items[0]?.id || ''
})
function setType(type: TransactionType) {
form.type = type
}
function clearLocation() {
form.latitude = null
form.longitude = null
}
function payload() {
return {
type: form.type,
occurredAt: form.occurredAt,
assetId: form.assetId,
categoryId: form.categoryId,
amount: form.amount,
title: form.title,
memo: form.memo,
latitude: form.latitude,
longitude: form.longitude
}
}
async function save() {
pending.value = true
await $fetch(`/api/transactions/${id.value}`, {
method: 'PATCH',
body: payload()
})
pending.value = false
await router.back()
}
async function remove() {
if (!confirm('이 거래를 삭제할까요?')) return
await $fetch(`/api/transactions/${id.value}`, { method: 'DELETE' })
await navigateTo('/')
}
async function copy() {
pending.value = true
const created = await $fetch<any>('/api/transactions', {
method: 'POST',
body: { ...payload(), occurredAt: formatDateInput() }
})
pending.value = false
await navigateTo(`/transactions/${created.id}`)
}
function goBack() {
router.back()
}
</script>