From a4e0b38b2f784a0483edcfeb34ffdcda92009706 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Thu, 9 Jul 2026 22:39:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=EA=B0=80=EA=B3=84=EB=B6=80?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=E2=80=94=20=EC=97=91=EC=85=80=20=EB=B0=B1?= =?UTF-8?q?=EC=97=85=20=EC=95=88=EB=82=B4=C2=B7=EC=84=A0=ED=83=9D=C2=B7?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=83=81=ED=83=9C=EB=B0=94=C2=B7=EC=99=84?= =?UTF-8?q?=EB=A3=8C=ED=86=A0=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/api/accountApi.js | 7 +- src/views/settings/SettingsView.vue | 208 ++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/src/api/accountApi.js b/src/api/accountApi.js index a2f4f22..8b2e2b3 100644 --- a/src/api/accountApi.js +++ b/src/api/accountApi.js @@ -220,6 +220,11 @@ const realApi = { return http.post('/account/restore', payload) }, + // 가계부 데이터 전체 삭제 (내역·계좌·분류·태그·예산·고정지출·투자 전부) + deleteAllData() { + return http.delete('/account/all') + }, + // 영수증 OCR (백엔드가 Google Vision 호출 → 전체 텍스트 반환) — AI 폴백용 // suppressNetError: runOcr 이 자체 formError 로 안내하므로 전역 배너 억제 ocrReceipt(blob) { @@ -266,7 +271,7 @@ const DEMO_WRITES = new Set([ 'createRecurring', 'updateRecurring', 'removeRecurring', 'runRecurrings', 'createWallet', 'updateWallet', 'removeWallet', 'reorderWallets', 'createCategory', 'updateCategory', 'removeCategory', 'reorderCategories', 'importCategories', - 'createTag', 'updateTag', 'removeTag', 'reorderTags', 'confirmEntry', 'ocrReceipt', 'parseReceiptAi', 'aiComment', 'restore', + 'createTag', 'updateTag', 'removeTag', 'reorderTags', 'confirmEntry', 'ocrReceipt', 'parseReceiptAi', 'aiComment', 'restore', 'deleteAllData', 'createBudget', 'updateBudget', 'removeBudget', 'setExpectedIncome', 'createHolding', 'updateHolding', 'removeHolding', 'addTrade', 'updateTrade', 'removeTrade', 'refreshPrices', 'refreshAllPrices', diff --git a/src/views/settings/SettingsView.vue b/src/views/settings/SettingsView.vue index 3a2def8..a6ef23a 100644 --- a/src/views/settings/SettingsView.vue +++ b/src/views/settings/SettingsView.vue @@ -9,6 +9,7 @@ import { exportBackup, importBackup } from '@/utils/backup' import { reminders } from '@/native/reminders' import { appLock, PIN_LENGTH } from '@/composables/appLock' import PinPad from '@/components/PinPad.vue' +import { accountApi } from '@/api/accountApi' const dialog = useDialog() const ui = useUiStore() @@ -169,6 +170,55 @@ async function setDim(dim, value) { window.desktop?.setWindowSize(w, h) } +// ===== 가계부 데이터 전체 삭제 ===== +const resetModalOpen = ref(false) +const resetting = ref(false) +const resetToast = ref(false) +let resetToastTimer = null + +function openResetModal() { + if (resetting.value) return + resetModalOpen.value = true +} + +async function doResetWithExport() { + resetModalOpen.value = false + resetting.value = true + try { + await exportBackup() + await accountApi.deleteAllData() + showResetToast() + } catch (e) { + window.alert(e?.message || '처리에 실패했습니다.') + } finally { + resetting.value = false + } +} + +async function doResetOnly() { + const ok = await dialog.confirm( + '엑셀 백업 없이 모든 데이터를 삭제합니다. 삭제된 데이터는 복구할 수 없습니다. 계속하시겠습니까?', + { title: '데이터 삭제 확인', okText: '삭제', danger: true }, + ) + if (!ok) return + resetModalOpen.value = false + resetting.value = true + try { + await accountApi.deleteAllData() + showResetToast() + } catch (e) { + window.alert(e?.response?.data?.message || e?.message || '삭제에 실패했습니다.') + } finally { + resetting.value = false + } +} + +function showResetToast() { + resetToast.value = true + clearTimeout(resetToastTimer) + resetToastTimer = setTimeout(() => { resetToast.value = false }, 3000) +} + async function clearAppData() { if (clearing.value) return const ok = await dialog.confirm( @@ -342,6 +392,14 @@ async function clearAppData() { v{{ appVersion }} + + + + + + + + + + + +
+
+ 데이터를 삭제하는 중입니다… +
+
+ + + +
데이터 초기화가 완료되었습니다.
+
+
@@ -564,4 +660,116 @@ async function clearAppData() { font-size: 0.78rem; opacity: 0.5; } + +/* 데이터 초기화 모달 */ +.reset-backdrop { + position: fixed; + inset: 0; + z-index: 2500; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + background: rgba(0, 0, 0, 0.55); +} +.reset-modal { + width: 100%; + max-width: 360px; + padding: 1.6rem 1.4rem 1.4rem; + border-radius: 14px; + background: var(--color-background); + border: 1px solid var(--color-border); +} +.reset-title { + font-size: 1.05rem; + font-weight: 700; + color: #c0392b; + margin-bottom: 0.9rem; +} +.reset-warn { + font-size: 0.88rem; + line-height: 1.65; + opacity: 0.85; + margin-bottom: 1.3rem; +} +.reset-actions { + display: flex; + flex-direction: column; + gap: 0.55rem; +} +.reset-btn { + width: 100%; + padding: 0.7rem 1rem; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + border: 1px solid var(--color-border); + background: var(--color-background-soft); + color: var(--color-text); + text-align: center; +} +.reset-btn.export { + border-color: hsla(160, 100%, 37%, 1); + color: hsla(160, 100%, 37%, 1); +} +.reset-btn.danger { + border-color: #c0392b; + color: #c0392b; +} +.reset-btn.cancel { + opacity: 0.6; +} + +/* 삭제 진행 상태바 */ +.reset-progress-wrap { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 3000; + background: var(--color-background-soft); + border-bottom: 1px solid var(--color-border); + padding: 0.6rem 1rem 0.5rem; + display: flex; + flex-direction: column; + gap: 0.4rem; +} +.reset-progress-bar { + height: 3px; + border-radius: 2px; + background: linear-gradient(90deg, hsla(160,100%,37%,1) 0%, hsla(160,100%,37%,0.3) 100%); + background-size: 200% 100%; + animation: progress-slide 1.2s linear infinite; +} +@keyframes progress-slide { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} +.reset-progress-label { + font-size: 0.82rem; + opacity: 0.75; +} + +/* 완료 토스트 */ +.reset-toast { + position: fixed; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + z-index: 3000; + padding: 0.65rem 1.3rem; + border-radius: 999px; + background: #2ecc71; + color: #fff; + font-size: 0.88rem; + font-weight: 600; + white-space: nowrap; + box-shadow: 0 4px 16px rgba(0,0,0,0.18); + animation: toast-in 0.2s ease; +} +@keyframes toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(8px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } +}