From 6784786262acdfe5276c649779ac5832f78a7bd4 Mon Sep 17 00:00:00 2001 From: hyeonggil <> Date: Sun, 8 Mar 2026 21:25:29 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B5=AC=EB=A7=A4=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=97=90=20=EC=97=91=EC=85=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=B0=8F=20=EC=A4=91=EA=B3=A0=20=ED=8C=90?= =?UTF-8?q?=EB=A7=A4=20=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - usePurchases: user_id 필터링으로 타 사용자 데이터 접근 차단 - usePurchases: bulkCreatePurchases() 일괄 저장 메서드 추가 - PurchaseModal: submit 시 중복 닫힘 방지 (부모에서 제어) - purchases/index: 엑셀 업로드 버튼 및 모달 연동 - purchases/index: 중고 판매 등록(태그 아이콘) 버튼 및 모달 연동 - purchases/index: 판매 상태 뱃지를 장비명 옆에 표시 - PurchaseExcelUpload: xlsx 파일 파싱 후 일괄 저장 컴포넌트 추가 - SellFromPurchaseModal: 구매 장비에서 중고 판매 등록 모달 추가 - xlsx 패키지 추가 --- .../purchases/PurchaseExcelUpload.vue | 381 ++++++++++++++++++ app/components/purchases/PurchaseModal.vue | 2 +- .../purchases/SellFromPurchaseModal.vue | 104 +++++ app/composables/usePurchases.ts | 28 +- app/pages/purchases/index.vue | 92 ++++- package.json | 1 + pnpm-lock.yaml | 65 +++ 7 files changed, 653 insertions(+), 20 deletions(-) create mode 100644 app/components/purchases/PurchaseExcelUpload.vue create mode 100644 app/components/purchases/SellFromPurchaseModal.vue diff --git a/app/components/purchases/PurchaseExcelUpload.vue b/app/components/purchases/PurchaseExcelUpload.vue new file mode 100644 index 0000000..bcac469 --- /dev/null +++ b/app/components/purchases/PurchaseExcelUpload.vue @@ -0,0 +1,381 @@ + + + + + + + + + + + + {{ file ? file.name : '파일을 클릭하거나 드래그하여 업로드' }} + + .xlsx, .xls 파일 지원 + + + + + + + 처음 사용하시나요? + + + + + + + + + + + + + 총 {{ rows.length }}행 + + + 유효 {{ validRows.length }}행 + + + 오류 {{ errorRows.length }}행 + + + + + + + + {{ row.original._index }} + + + + {{ row.original.status === 'valid' ? '유효' : '오류' }} + + + + {{ row.original.data.name }} + + + {{ row.original.data.category }} + + + {{ formatPrice(row.original.data.price) }} + + + {{ row.original.data.purchase_date }} + + + + + {{ err }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/components/purchases/PurchaseModal.vue b/app/components/purchases/PurchaseModal.vue index bed62ee..b2c3243 100644 --- a/app/components/purchases/PurchaseModal.vue +++ b/app/components/purchases/PurchaseModal.vue @@ -14,8 +14,8 @@ const emit = defineEmits<{ const title = computed(() => props.initial ? '장비 수정' : '장비 추가') function handleSubmit(data: PurchaseInsert) { + // 부모의 비동기 핸들러가 완료된 후 모달을 닫으므로 여기서 닫지 않음 emit('submit', data) - emit('update:open', false) } diff --git a/app/components/purchases/SellFromPurchaseModal.vue b/app/components/purchases/SellFromPurchaseModal.vue new file mode 100644 index 0000000..c2be3e3 --- /dev/null +++ b/app/components/purchases/SellFromPurchaseModal.vue @@ -0,0 +1,104 @@ + + + + + + + + + + 구매 장비 {{ purchase.name }} 와 연결됩니다 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/composables/usePurchases.ts b/app/composables/usePurchases.ts index ef439c0..0e0a3d4 100644 --- a/app/composables/usePurchases.ts +++ b/app/composables/usePurchases.ts @@ -28,6 +28,7 @@ export function usePurchases() { const { data, error: err } = await client .from('purchases') .select('*') + .eq('user_id', user.value.id) .order('purchase_date', { ascending: false }) if (err) throw err purchases.value = data as Purchase[] @@ -59,6 +60,7 @@ export function usePurchases() { } async function updatePurchase(id: string, payload: Partial) { + if (!user.value) return loading.value = true error.value = null try { @@ -66,6 +68,7 @@ export function usePurchases() { .from('purchases') .update(payload) .eq('id', id) + .eq('user_id', user.value.id) .select() .single() if (err) throw err @@ -80,6 +83,7 @@ export function usePurchases() { } async function deletePurchase(id: string) { + if (!user.value) return loading.value = true error.value = null try { @@ -87,6 +91,7 @@ export function usePurchases() { .from('purchases') .delete() .eq('id', id) + .eq('user_id', user.value.id) if (err) throw err purchases.value = purchases.value.filter(p => p.id !== id) } catch (e: unknown) { @@ -97,11 +102,13 @@ export function usePurchases() { } async function getPurchase(id: string): Promise { + if (!user.value) return null try { const { data, error: err } = await client .from('purchases') .select('*') .eq('id', id) + .eq('user_id', user.value.id) .single() if (err) throw err return data as Purchase @@ -110,6 +117,24 @@ export function usePurchases() { } } + async function bulkCreatePurchases(payloads: PurchaseInsert[]): Promise { + if (!user.value || payloads.length === 0) return 0 + loading.value = true + error.value = null + try { + const rows = payloads.map(p => ({ ...p, user_id: user.value!.id })) + const { data, error: err } = await client.from('purchases').insert(rows).select() + if (err) throw err + purchases.value.unshift(...(data as Purchase[])) + return data.length + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : '일괄 저장에 실패했습니다' + return 0 + } finally { + loading.value = false + } + } + return { purchases: readonly(purchases), loading: readonly(loading), @@ -120,6 +145,7 @@ export function usePurchases() { createPurchase, updatePurchase, deletePurchase, - getPurchase + getPurchase, + bulkCreatePurchases } } diff --git a/app/pages/purchases/index.vue b/app/pages/purchases/index.vue index 3585e6f..4d95ff6 100644 --- a/app/pages/purchases/index.vue +++ b/app/pages/purchases/index.vue @@ -1,46 +1,76 @@
+ {{ file ? file.name : '파일을 클릭하거나 드래그하여 업로드' }} +
.xlsx, .xls 파일 지원