Refine transaction list rows

This commit is contained in:
2026-08-13 16:38:59 +09:00
parent d3a4cf277e
commit cecd390b33
8 changed files with 100 additions and 57 deletions

View File

@@ -0,0 +1,59 @@
<template>
<NuxtLink
:to="`/transactions/${transaction.id}`"
class="grid grid-cols-[2.5rem_minmax(0,1fr)_auto] items-start gap-3 px-4 py-3 active:bg-paper md:px-5"
>
<div
class="mt-0.5 flex h-10 w-10 items-center justify-center rounded-md bg-paper text-sm font-bold"
:class="transaction.type === 'income' ? 'text-brand-700' : 'text-muted'"
aria-hidden="true"
>
{{ categoryInitial }}
</div>
<div class="min-w-0">
<p class="truncate text-[15px] font-bold leading-5 text-ink">
{{ transaction.title }}
</p>
<p class="mt-1 truncate text-xs font-medium leading-5 text-muted">
{{ metaLine }}
</p>
<p v-if="memoPreview" class="mt-1 truncate text-xs font-medium leading-5 text-muted">
{{ memoPreview }}
</p>
</div>
<p
class="whitespace-nowrap pt-0.5 text-right text-[15px] font-bold leading-5"
:class="transaction.type === 'income' ? 'text-brand-700' : 'text-ink'"
>
{{ formatWon(transaction.amount) }}
</p>
</NuxtLink>
</template>
<script setup lang="ts">
import type { Transaction } from '~/composables/useMoney'
const props = defineProps<{
transaction: Transaction
}>()
const categoryLabel = computed(() => {
const category = props.transaction.category
return category.parent ? `${category.parent.name} > ${category.name}` : category.name
})
const categoryInitial = computed(() => {
const category = props.transaction.category
const name = category.parent?.name || category.name || (props.transaction.type === 'income' ? '수' : '지')
return name.slice(0, 1)
})
const metaLine = computed(() => {
const time = new Intl.DateTimeFormat('ko-KR', { hour: '2-digit', minute: '2-digit' }).format(new Date(props.transaction.occurredAt))
return [time, props.transaction.asset.name, categoryLabel.value].filter(Boolean).join(' · ')
})
const memoPreview = computed(() => props.transaction.memo?.trim().slice(0, 60) || '')
</script>

View File

@@ -19,3 +19,5 @@
- 바텀 시트 선택 버튼에서 하단 화살표를 제거했다.
- 로그인 화면이 하단 탭 여백 때문에 스크롤되는 문제를 수정했다.
- 입력 중 스크롤이 튀는 현상을 줄이기 위해 바텀 시트 body overflow 변경과 추가 화면 sticky 저장 버튼을 제거했다.
- 거래 목록 행을 날짜 그룹, 카테고리 아이콘 자리, 기본 2줄 정보, 메모가 있을 때만 보이는 3번째 줄 구조로 변경했다.
- 메모 입력과 저장 길이를 최대 60자로 제한했다.

View File

@@ -64,7 +64,7 @@
<div>
<label class="label" for="memo">메모</label>
<textarea id="memo" v-model="form.memo" class="field min-h-24 resize-none" placeholder="자유롭게 기록"></textarea>
<textarea id="memo" v-model="form.memo" class="field min-h-24 resize-none" maxlength="60" placeholder="자유롭게 기록"></textarea>
</div>
<button class="btn-primary mt-5 w-full" type="submit" :disabled="pending">

View File

@@ -90,23 +90,11 @@
</div>
</div>
<NuxtLink
<TransactionListRow
v-for="tx in group.items"
:key="tx.id"
:to="`/transactions/${tx.id}`"
class="grid grid-cols-[1fr_auto] gap-3 px-4 py-3"
>
<div class="min-w-0">
<p class="truncate font-semibold">{{ tx.title }}</p>
<p class="mt-1 truncate text-sm text-muted">{{ categoryLabel(tx.category) }}</p>
</div>
<div class="text-right">
<p class="font-bold" :class="tx.type === 'income' ? 'text-brand-700' : 'text-red-600'">
{{ formatWon(tx.amount) }}
</p>
<p class="mt-1 text-sm text-muted">{{ timeLabel(tx.occurredAt) }}</p>
</div>
</NuxtLink>
:transaction="tx"
/>
</section>
<p v-if="!assetTransactions.length" class="px-4 py-12 text-center text-muted">
@@ -153,7 +141,7 @@
<script setup lang="ts">
import { addMonths, format, parse } from 'date-fns'
import type { Category, Transaction } from '~/composables/useMoney'
import type { Transaction } from '~/composables/useMoney'
const selectedMonth = ref(monthKey())
const selectedAssetId = ref('')
@@ -220,11 +208,4 @@ function selectPickerMonth(month: number) {
monthPickerOpen.value = false
}
function timeLabel(value: string) {
return new Intl.DateTimeFormat('ko-KR', { hour: '2-digit', minute: '2-digit' }).format(new Date(value))
}
function categoryLabel(category: Category & { parent?: Category | null }) {
return category.parent ? `${category.parent.name} > ${category.name}` : category.name
}
</script>

View File

@@ -22,28 +22,17 @@
</div>
</header>
<div class="divide-y divide-line">
<NuxtLink
v-for="tx in data?.transactions"
:key="tx.id"
:to="`/transactions/${tx.id}`"
class="grid grid-cols-[1fr_auto] gap-3 px-4 py-3"
>
<div class="min-w-0">
<p class="truncate font-semibold text-ink">{{ tx.title }}</p>
<p class="mt-1 text-xs text-muted">
{{ timeLabel(tx.occurredAt) }} · {{ tx.asset.name }}
</p>
<p class="mt-1 truncate text-xs text-muted">
{{ categoryLabel(tx.category) }}
</p>
</div>
<div class="text-right">
<p class="font-bold" :class="tx.type === 'income' ? 'text-brand-700' : 'text-red-600'">
{{ tx.type === 'income' ? '+' : '-' }}{{ formatWon(tx.amount) }}
</p>
</div>
</NuxtLink>
<div class="divide-y divide-line bg-white">
<section v-for="group in groupedTransactions" :key="group.date" class="py-2">
<h2 class="px-4 py-3 text-sm font-semibold text-muted md:px-5">
{{ group.label }}
</h2>
<TransactionListRow
v-for="tx in group.items"
:key="tx.id"
:transaction="tx"
/>
</section>
</div>
<div v-if="!data?.transactions.length" class="px-4 py-16 text-center text-muted">
@@ -54,24 +43,36 @@
<script setup lang="ts">
import { addMonths, format, parse } from 'date-fns'
import type { Category } from '~/composables/useMoney'
import type { Transaction } from '~/composables/useMoney'
const selectedMonth = ref(monthKey())
const { data } = await useFetch('/api/transactions', {
query: { month: selectedMonth }
})
const groupedTransactions = computed(() => {
const groups = new Map<string, { date: string; label: string; items: Transaction[] }>()
for (const tx of data.value?.transactions || []) {
const date = new Date(tx.occurredAt)
const key = format(date, 'yyyy-MM-dd')
const group = groups.get(key) || { date: key, label: dateLabel(date), items: [] }
group.items.push(tx)
groups.set(key, group)
}
return Array.from(groups.values()).sort((a, b) => b.date.localeCompare(a.date))
})
function moveMonth(amount: number) {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
selectedMonth.value = format(addMonths(date, amount), 'yyyy-MM')
}
function timeLabel(value: string) {
return new Intl.DateTimeFormat('ko-KR', { hour: '2-digit', minute: '2-digit' }).format(new Date(value))
function dateLabel(date: Date) {
const key = format(date, 'yyyy-MM-dd')
const today = format(new Date(), 'yyyy-MM-dd')
const yesterday = format(new Date(Date.now() - 24 * 60 * 60 * 1000), 'yyyy-MM-dd')
const weekday = new Intl.DateTimeFormat('ko-KR', { weekday: 'short' }).format(date)
const relative = key === today ? ' · 오늘' : key === yesterday ? ' · 어제' : ''
return `${format(date, 'M. d')}(${weekday})${relative}`
}
function categoryLabel(category: Category & { parent?: Category | null }) {
return category.parent ? `${category.parent.name} > ${category.name}` : category.name
}
</script>

View File

@@ -62,7 +62,7 @@
<div class="mt-4 border-y border-line py-4">
<label class="mb-2 block text-sm font-semibold text-muted" for="memo">메모</label>
<textarea id="memo" v-model="form.memo" class="field min-h-24 resize-none" placeholder="메모"></textarea>
<textarea id="memo" v-model="form.memo" class="field min-h-24 resize-none" maxlength="60" placeholder="메모"></textarea>
</div>
<div v-if="locationLabel" class="mt-3 flex items-center justify-between border-b border-line py-4">

View File

@@ -42,7 +42,7 @@ export default defineEventHandler(async (event) => {
categoryId: body.categoryId,
amount: Number(body.amount),
title: body.title.trim(),
memo: body.memo?.trim() || null,
memo: body.memo?.trim().slice(0, 60) || null,
latitude: body.latitude ?? null,
longitude: body.longitude ?? null
},

View File

@@ -38,7 +38,7 @@ export default defineEventHandler(async (event) => {
categoryId: body.categoryId,
amount: Number(body.amount),
title: body.title.trim(),
memo: body.memo?.trim() || null,
memo: body.memo?.trim().slice(0, 60) || null,
latitude: body.latitude ?? null,
longitude: body.longitude ?? null
}