diff --git a/app/components/purchases/PurchaseExcelUpload.vue b/app/components/purchases/PurchaseExcelUpload.vue index bcac469..d05a49f 100644 --- a/app/components/purchases/PurchaseExcelUpload.vue +++ b/app/components/purchases/PurchaseExcelUpload.vue @@ -1,6 +1,7 @@ @@ -275,7 +316,7 @@ function formatPrice(price: number) { color="neutral" variant="soft" title="엑셀 파일 형식 안내" - :description="`필수 컬럼: 장비명, 카테고리, 가격, 구매일 / 카테고리 허용값: 텐트, 침낭/매트, 취사도구, 조명, 의류, 배낭, 가구, 안전장비, 전자기기, 기타`" + description="필수 컬럼: 장비명, 카테고리, 단가, 구매일 / 수량 미입력 시 1로 처리 / 카테고리: 텐트, 침낭/매트, 취사도구, 조명, 의류, 배낭, 가구, 안전장비, 전자기기, 기타" /> @@ -283,12 +324,8 @@ function formatPrice(price: number) { - - 총 {{ rows.length }}행 - - - 유효 {{ validRows.length }}행 - + 총 {{ rows.length }}행 + 유효 {{ validRows.length }}행 오류 {{ errorRows.length }}행 @@ -300,6 +337,7 @@ function formatPrice(price: number) { {{ row.original._index }} + + {{ row.original.data.name }} + + - {{ row.original.data.category }} + + + + {{ (row.original.data.quantity ?? 1).toLocaleString() }} + + - {{ formatPrice(row.original.data.price) }} + {{ formatPrice(row.original.data.price) }} + + + + {{ formatPrice(row.original.data.price * (row.original.data.quantity ?? 1)) }} + + + {{ row.original.data.purchase_date }} + {{ err }} @@ -338,25 +398,35 @@ function formatPrice(price: number) { - + + + + - + - + -const state = reactive({ +const state = reactive<{ + name: string + category: EquipmentCategory | undefined + brand: string + price: number + quantity: number + purchase_date: string + store: string + warranty_until: string + notes: string +}>({ name: props.initial?.name ?? '', - category: props.initial?.category ?? '', + category: props.initial?.category, brand: props.initial?.brand ?? '', price: props.initial?.price ?? 0, - purchase_date: props.initial?.purchase_date ?? new Date().toISOString().split('T')[0], + quantity: props.initial?.quantity ?? 1, + purchase_date: props.initial?.purchase_date ?? new Date().toISOString().slice(0, 10), store: props.initial?.store ?? '', warranty_until: props.initial?.warranty_until ?? '', notes: props.initial?.notes ?? '' }) +// 가격 텍스트 입력 상태 (세미콜론 변환용) +const priceRaw = ref(String(props.initial?.price ?? '')) + +// priceRaw 변경 시 숫자만 추출해서 state.price 동기화 +watch(priceRaw, (v) => { + const n = parseInt(v.replace(/[^0-9]/g, ''), 10) + state.price = isNaN(n) ? 0 : n +}) + +// `;` 입력 시 현재 값 × 1000 (예: 50; → 50000) +function handlePriceKeydown(e: KeyboardEvent) { + if (e.key === ';') { + e.preventDefault() + const n = parseInt(priceRaw.value.replace(/[^0-9]/g, ''), 10) + if (!isNaN(n) && n > 0) { + priceRaw.value = String(n * 1000) + state.price = n * 1000 + } + } +} + +// 숫자 이외 문자 입력 방지 +function handlePriceInput(e: Event) { + const input = e.target as HTMLInputElement + const cleaned = input.value.replace(/[^0-9]/g, '') + if (cleaned !== input.value) { + priceRaw.value = cleaned + } +} + +// 단가 × 수량 합계 +const totalPrice = computed(() => state.price * state.quantity) + +function formatPrice(n: number) { + return n.toLocaleString('ko-KR') + '원' +} + async function onSubmit(event: FormSubmitEvent) { const data: PurchaseInsert = { name: event.data.name, category: event.data.category as PurchaseInsert['category'], brand: event.data.brand || undefined, price: event.data.price, + quantity: event.data.quantity, purchase_date: event.data.purchase_date, store: event.data.store || undefined, warranty_until: event.data.warranty_until || undefined, @@ -69,25 +119,49 @@ async function onSubmit(event: FormSubmitEvent) { - - + + - - + + + + + + {{ formatPrice(state.price) }} × {{ state.quantity }}개 + + + 합계 {{ formatPrice(totalPrice) }} + + + + + + + - - - - + + + + diff --git a/app/composables/usePurchases.ts b/app/composables/usePurchases.ts index 0e0a3d4..c034e05 100644 --- a/app/composables/usePurchases.ts +++ b/app/composables/usePurchases.ts @@ -1,5 +1,12 @@ import type { Purchase, PurchaseInsert, EquipmentCategory } from '~/types/purchase' +// Supabase PostgrestError는 instanceof Error가 아니므로 message 프로퍼티를 직접 추출 +function extractErrorMessage(e: unknown, fallback: string): string { + if (e instanceof Error) return e.message + if (e && typeof e === 'object' && 'message' in e) return String((e as { message: unknown }).message) + return fallback +} + export function usePurchases() { const client = useSupabaseClient() const user = useSupabaseUser() @@ -9,13 +16,13 @@ export function usePurchases() { const error = ref(null) const totalSpent = computed(() => - purchases.value.reduce((sum, p) => sum + p.price, 0) + purchases.value.reduce((sum, p) => sum + p.price * (p.quantity ?? 1), 0) ) const categoryBreakdown = computed(() => { const breakdown: Record = {} for (const p of purchases.value) { - breakdown[p.category] = (breakdown[p.category] ?? 0) + p.price + breakdown[p.category] = (breakdown[p.category] ?? 0) + p.price * (p.quantity ?? 1) } return breakdown }) @@ -33,7 +40,7 @@ export function usePurchases() { if (err) throw err purchases.value = data as Purchase[] } catch (e: unknown) { - error.value = e instanceof Error ? e.message : '오류가 발생했습니다' + error.value = extractErrorMessage(e, '오류가 발생했습니다') } finally { loading.value = false } @@ -53,7 +60,7 @@ export function usePurchases() { purchases.value.unshift(data as Purchase) return data as Purchase } catch (e: unknown) { - error.value = e instanceof Error ? e.message : '저장에 실패했습니다' + error.value = extractErrorMessage(e, '저장에 실패했습니다') } finally { loading.value = false } @@ -76,7 +83,7 @@ export function usePurchases() { if (idx !== -1) purchases.value[idx] = data as Purchase return data as Purchase } catch (e: unknown) { - error.value = e instanceof Error ? e.message : '수정에 실패했습니다' + error.value = extractErrorMessage(e, '수정에 실패했습니다') } finally { loading.value = false } @@ -95,7 +102,7 @@ export function usePurchases() { if (err) throw err purchases.value = purchases.value.filter(p => p.id !== id) } catch (e: unknown) { - error.value = e instanceof Error ? e.message : '삭제에 실패했습니다' + error.value = extractErrorMessage(e, '삭제에 실패했습니다') } finally { loading.value = false } @@ -128,7 +135,7 @@ export function usePurchases() { purchases.value.unshift(...(data as Purchase[])) return data.length } catch (e: unknown) { - error.value = e instanceof Error ? e.message : '일괄 저장에 실패했습니다' + error.value = extractErrorMessage(e, '일괄 저장에 실패했습니다') return 0 } finally { loading.value = false diff --git a/app/pages/purchases/index.vue b/app/pages/purchases/index.vue index 4d95ff6..d59c81a 100644 --- a/app/pages/purchases/index.vue +++ b/app/pages/purchases/index.vue @@ -68,6 +68,7 @@ const columns: TableColumn[] = [ { accessorKey: 'name', header: '장비명' }, { accessorKey: 'category', header: '카테고리' }, { accessorKey: 'brand', header: '브랜드' }, + { accessorKey: 'quantity', header: () => h('div', { class: 'text-center' }, '수량') }, { accessorKey: 'price', header: () => h('div', { class: 'text-right' }, '가격') }, { accessorKey: 'purchase_date', header: '구매일' }, { id: 'actions', header: '' } @@ -161,8 +162,18 @@ function formatPrice(price: number) { {{ CATEGORY_LABELS[row.original.category as EquipmentCategory] }} + + + {{ (row.original.quantity ?? 1).toLocaleString() }} + + - {{ formatPrice(row.original.price) }} + + {{ formatPrice(row.original.price * (row.original.quantity ?? 1)) }} + + {{ formatPrice(row.original.price) }} × {{ row.original.quantity }} + + diff --git a/app/types/purchase.ts b/app/types/purchase.ts index 8d77ead..d97b801 100644 --- a/app/types/purchase.ts +++ b/app/types/purchase.ts @@ -35,6 +35,7 @@ export interface Purchase { category: EquipmentCategory brand?: string price: number + quantity: number purchase_date: string store?: string warranty_until?: string @@ -48,6 +49,7 @@ export interface PurchaseInsert { category: EquipmentCategory brand?: string price: number + quantity: number purchase_date: string store?: string warranty_until?: string