feat: nuxt-claude 프로젝트 초기 커밋
Some checks failed
ci / ci (22, ubuntu-latest) (push) Failing after 25m52s
Some checks failed
ci / ci (22, ubuntu-latest) (push) Failing after 25m52s
Made-with: Cursor
This commit is contained in:
105
app/pages/used-sales/[id].vue
Normal file
105
app/pages/used-sales/[id].vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { STATUS_LABELS, PLATFORM_LABELS } from '~/types/used-sale'
|
||||
import type { UsedSale, UsedSaleInsert, SaleStatus, SalePlatform } from '~/types/used-sale'
|
||||
|
||||
definePageMeta({ middleware: 'auth' })
|
||||
|
||||
const route = useRoute()
|
||||
const { getSale, updateSale, deleteSale } = useUsedSales()
|
||||
|
||||
const sale = ref<UsedSale | null>(null)
|
||||
const showEdit = ref(false)
|
||||
|
||||
sale.value = await getSale(route.params.id as string)
|
||||
|
||||
if (!sale.value) {
|
||||
await navigateTo('/used-sales')
|
||||
}
|
||||
|
||||
async function handleUpdate(data: UsedSaleInsert) {
|
||||
if (!sale.value) return
|
||||
const updated = await updateSale(sale.value.id, data)
|
||||
if (updated) {
|
||||
sale.value = updated
|
||||
showEdit.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!sale.value || !confirm('이 판매 등록을 삭제하시겠습니까?')) return
|
||||
await deleteSale(sale.value.id)
|
||||
await navigateTo('/used-sales')
|
||||
}
|
||||
|
||||
function formatPrice(price?: number) {
|
||||
if (!price && price !== 0) return '-'
|
||||
return price.toLocaleString('ko-KR') + '원'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="sale" class="p-6 space-y-6 max-w-2xl">
|
||||
<div class="flex items-center gap-4">
|
||||
<NuxtLink to="/used-sales">
|
||||
<UButton icon="i-lucide-arrow-left" color="neutral" variant="ghost" />
|
||||
</NuxtLink>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ sale.item_name }}</h1>
|
||||
<UsedSalesUsedSaleBadge :status="sale.status as SaleStatus" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<UButton icon="i-lucide-pencil" label="수정" color="neutral" variant="outline" @click="showEdit = true" />
|
||||
<UButton icon="i-lucide-trash-2" label="삭제" color="error" variant="outline" @click="handleDelete" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UCard>
|
||||
<dl class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500 dark:text-gray-400">판매 플랫폼</dt>
|
||||
<dd class="mt-1 font-medium text-gray-900 dark:text-white">
|
||||
{{ PLATFORM_LABELS[sale.platform as SalePlatform] }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500 dark:text-gray-400">판매 상태</dt>
|
||||
<dd class="mt-1">
|
||||
<UsedSalesUsedSaleBadge :status="sale.status as SaleStatus" />
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500 dark:text-gray-400">희망가격</dt>
|
||||
<dd class="mt-1 font-bold text-lg text-gray-900 dark:text-white">{{ formatPrice(sale.sale_price) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500 dark:text-gray-400">최종 판매가</dt>
|
||||
<dd class="mt-1 font-bold text-lg text-primary-600">{{ formatPrice(sale.final_price) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500 dark:text-gray-400">등록일</dt>
|
||||
<dd class="mt-1 font-medium text-gray-900 dark:text-white">{{ sale.listed_at }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500 dark:text-gray-400">판매완료일</dt>
|
||||
<dd class="mt-1 font-medium text-gray-900 dark:text-white">{{ sale.sold_at || '-' }}</dd>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<dt class="text-sm text-gray-500 dark:text-gray-400">메모</dt>
|
||||
<dd class="mt-1 font-medium text-gray-900 dark:text-white whitespace-pre-wrap">
|
||||
{{ sale.notes || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</UCard>
|
||||
|
||||
<UModal :open="showEdit" title="판매 수정" @update:open="showEdit = $event">
|
||||
<template #body>
|
||||
<UsedSalesUsedSaleForm
|
||||
:initial="sale"
|
||||
@submit="handleUpdate"
|
||||
@cancel="showEdit = false"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
160
app/pages/used-sales/index.vue
Normal file
160
app/pages/used-sales/index.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import { STATUS_LABELS, PLATFORM_LABELS, STATUS_OPTIONS } from '~/types/used-sale'
|
||||
import type { UsedSale, UsedSaleInsert, SaleStatus, SalePlatform } from '~/types/used-sale'
|
||||
|
||||
definePageMeta({ middleware: 'auth' })
|
||||
|
||||
const { sales, totalRevenue, byStatus, loading, error, fetchSales, createSale, updateSale, deleteSale } = useUsedSales()
|
||||
|
||||
await fetchSales()
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingSale = ref<UsedSale | undefined>(undefined)
|
||||
const activeTab = ref<SaleStatus | 'all'>('all')
|
||||
|
||||
const tabOptions = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: 'listing', label: `판매중 (${byStatus.value.listing.length})` },
|
||||
{ value: 'reserved', label: `예약중 (${byStatus.value.reserved.length})` },
|
||||
{ value: 'sold', label: `판매완료 (${byStatus.value.sold.length})` },
|
||||
{ value: 'cancelled', label: `취소 (${byStatus.value.cancelled.length})` }
|
||||
]
|
||||
|
||||
const filtered = computed(() => {
|
||||
if (activeTab.value === 'all') return sales.value
|
||||
return sales.value.filter(s => s.status === activeTab.value)
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ key: 'item_name', label: '장비명' },
|
||||
{ key: 'platform', label: '플랫폼' },
|
||||
{ key: 'sale_price', label: '희망가' },
|
||||
{ key: 'final_price', label: '최종가' },
|
||||
{ key: 'status', label: '상태' },
|
||||
{ key: 'listed_at', label: '등록일' },
|
||||
{ key: 'actions', label: '' }
|
||||
]
|
||||
|
||||
function openCreate() {
|
||||
editingSale.value = undefined
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function openEdit(sale: UsedSale) {
|
||||
editingSale.value = sale
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit(data: UsedSaleInsert) {
|
||||
if (editingSale.value) {
|
||||
await updateSale(editingSale.value.id, data)
|
||||
} else {
|
||||
await createSale(data)
|
||||
}
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm('이 판매 등록을 삭제하시겠습니까?')) return
|
||||
await deleteSale(id)
|
||||
}
|
||||
|
||||
async function markAsSold(sale: UsedSale) {
|
||||
await updateSale(sale.id, {
|
||||
status: 'sold',
|
||||
final_price: sale.final_price ?? sale.sale_price,
|
||||
sold_at: new Date().toISOString().split('T')[0]
|
||||
})
|
||||
}
|
||||
|
||||
function formatPrice(price?: number) {
|
||||
if (!price && price !== 0) return '-'
|
||||
return price.toLocaleString('ko-KR') + '원'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">중고 판매 관리</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400 mt-1">
|
||||
총 판매수익 {{ formatPrice(totalRevenue) }}
|
||||
</p>
|
||||
</div>
|
||||
<UButton icon="i-lucide-plus" label="판매 등록" @click="openCreate" />
|
||||
</div>
|
||||
|
||||
<!-- Status cards -->
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<UCard v-for="opt in tabOptions.slice(1)" :key="opt.value">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-500">{{ STATUS_LABELS[opt.value as SaleStatus] }}</p>
|
||||
<p class="text-2xl font-bold mt-1">{{ byStatus[opt.value as SaleStatus].length }}</p>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<UButton
|
||||
v-for="opt in tabOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:color="activeTab === opt.value ? 'primary' : 'neutral'"
|
||||
:variant="activeTab === opt.value ? 'solid' : 'ghost'"
|
||||
size="sm"
|
||||
@click="activeTab = opt.value as SaleStatus | 'all'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UAlert v-if="error" color="error" :description="error" />
|
||||
|
||||
<!-- Table -->
|
||||
<UCard>
|
||||
<UTable :data="filtered" :columns="columns" :loading="loading">
|
||||
<template #platform-cell="{ row }">
|
||||
{{ PLATFORM_LABELS[row.original.platform as SalePlatform] }}
|
||||
</template>
|
||||
<template #sale_price-cell="{ row }">
|
||||
{{ formatPrice(row.original.sale_price) }}
|
||||
</template>
|
||||
<template #final_price-cell="{ row }">
|
||||
{{ formatPrice(row.original.final_price) }}
|
||||
</template>
|
||||
<template #status-cell="{ row }">
|
||||
<UsedSalesUsedSaleBadge :status="row.original.status as SaleStatus" />
|
||||
</template>
|
||||
<template #actions-cell="{ row }">
|
||||
<div class="flex gap-1 justify-end">
|
||||
<NuxtLink :to="`/used-sales/${row.original.id}`">
|
||||
<UButton icon="i-lucide-eye" size="sm" color="neutral" variant="ghost" />
|
||||
</NuxtLink>
|
||||
<UButton
|
||||
v-if="row.original.status === 'listing' || row.original.status === 'reserved'"
|
||||
icon="i-lucide-check"
|
||||
size="sm"
|
||||
color="success"
|
||||
variant="ghost"
|
||||
title="판매완료"
|
||||
@click="markAsSold(row.original)"
|
||||
/>
|
||||
<UButton icon="i-lucide-pencil" size="sm" color="neutral" variant="ghost" @click="openEdit(row.original)" />
|
||||
<UButton icon="i-lucide-trash-2" size="sm" color="error" variant="ghost" @click="handleDelete(row.original.id)" />
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
|
||||
<!-- Modal -->
|
||||
<UModal :open="showModal" :title="editingSale ? '판매 수정' : '판매 등록'" @update:open="showModal = $event">
|
||||
<template #body>
|
||||
<UsedSalesUsedSaleForm
|
||||
:initial="editingSale"
|
||||
@submit="handleSubmit"
|
||||
@cancel="showModal = false"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user