- utils/backup.js: 가계부 전체(내역·계좌·분류·고정지출·예산·태그·자주내역)를 이름 기준 참조로 .xlsx 다중 시트 생성. xlsx 동적 import(별도 청크) - 웹/PC=다운로드, 앱(네이티브)=Filesystem 저장 + Share - SettingsView: '데이터 백업' 카드(엑셀로 내보내기 동작 / 가져오기 버튼=준비중) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+134
-1
@@ -13,7 +13,9 @@
|
|||||||
"@capacitor/camera": "^8.2.0",
|
"@capacitor/camera": "^8.2.0",
|
||||||
"@capacitor/cli": "^8.3.4",
|
"@capacitor/cli": "^8.3.4",
|
||||||
"@capacitor/core": "^8.3.4",
|
"@capacitor/core": "^8.3.4",
|
||||||
|
"@capacitor/filesystem": "^8.1.2",
|
||||||
"@capacitor/preferences": "^8.0.1",
|
"@capacitor/preferences": "^8.0.1",
|
||||||
|
"@capacitor/share": "^8.0.1",
|
||||||
"@capawesome/capacitor-google-sign-in": "^0.1.2",
|
"@capawesome/capacitor-google-sign-in": "^0.1.2",
|
||||||
"@toast-ui/editor": "^3.2.2",
|
"@toast-ui/editor": "^3.2.2",
|
||||||
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
|
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
|
||||||
@@ -22,7 +24,8 @@
|
|||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
@@ -1187,6 +1190,18 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor/filesystem": {
|
||||||
|
"version": "8.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-8.1.2.tgz",
|
||||||
|
"integrity": "sha512-doaaMfGoFR2hWU6aV6u83I+5ZsGyJVq+Gz4r9lMpJzUKMm1eMu0hLnFdV1aXZlU9FlK/RndFrVD8oRZfNOqWgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@capacitor/synapse": "^1.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@capacitor/preferences": {
|
"node_modules/@capacitor/preferences": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz",
|
||||||
@@ -1196,6 +1211,21 @@
|
|||||||
"@capacitor/core": ">=8.0.0"
|
"@capacitor/core": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor/share": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/share/-/share-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-3cSBKBCJVon54rKDROP2rqGyeGks4pBh9TbaEk9S375Kbek/ZHe72N50zIa0Vn9Eac/SuhwgehO/mmA4CsUOiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@capacitor/synapse": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/@capawesome/capacitor-google-sign-in": {
|
"node_modules/@capawesome/capacitor-google-sign-in": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-google-sign-in/-/capacitor-google-sign-in-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-google-sign-in/-/capacitor-google-sign-in-0.1.2.tgz",
|
||||||
@@ -4015,6 +4045,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@@ -5182,6 +5221,19 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chai": {
|
"node_modules/chai": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
@@ -5378,6 +5430,15 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
@@ -5782,6 +5843,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/create-require": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
@@ -7479,6 +7552,15 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs-constants": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
@@ -12540,6 +12622,18 @@
|
|||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ssri": {
|
"node_modules/ssri": {
|
||||||
"version": "12.0.0",
|
"version": "12.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
|
||||||
@@ -14175,6 +14269,24 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@@ -14297,6 +14409,27 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-js": {
|
"node_modules/xml-js": {
|
||||||
"version": "1.6.11",
|
"version": "1.6.11",
|
||||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||||
|
|||||||
+4
-1
@@ -46,7 +46,9 @@
|
|||||||
"@capacitor/camera": "^8.2.0",
|
"@capacitor/camera": "^8.2.0",
|
||||||
"@capacitor/cli": "^8.3.4",
|
"@capacitor/cli": "^8.3.4",
|
||||||
"@capacitor/core": "^8.3.4",
|
"@capacitor/core": "^8.3.4",
|
||||||
|
"@capacitor/filesystem": "^8.1.2",
|
||||||
"@capacitor/preferences": "^8.0.1",
|
"@capacitor/preferences": "^8.0.1",
|
||||||
|
"@capacitor/share": "^8.0.1",
|
||||||
"@capawesome/capacitor-google-sign-in": "^0.1.2",
|
"@capawesome/capacitor-google-sign-in": "^0.1.2",
|
||||||
"@toast-ui/editor": "^3.2.2",
|
"@toast-ui/editor": "^3.2.2",
|
||||||
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
|
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
|
||||||
@@ -55,7 +57,8 @@
|
|||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
|
|||||||
+26
-5
@@ -94,15 +94,36 @@ const TAGS = [
|
|||||||
{ id: 403, name: '경조사' },
|
{ id: 403, name: '경조사' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 요약은 내역에서 계산해 일관성 보장
|
// 해당 연·월 내역만 (월 이동 반영)
|
||||||
const SUMMARY = (() => {
|
function inMonth(e, year, month) {
|
||||||
|
if (!year || !month) return true
|
||||||
|
return e.entryDate.startsWith(`${year}-${String(month).padStart(2, '0')}`)
|
||||||
|
}
|
||||||
|
// 검색·필터 적용
|
||||||
|
function filterEntries(p = {}) {
|
||||||
|
let rows = ENTRIES.filter((e) => inMonth(e, p.year, p.month))
|
||||||
|
if (p.type) rows = rows.filter((e) => e.type === p.type)
|
||||||
|
if (p.category) rows = rows.filter((e) => e.category === p.category)
|
||||||
|
if (p.walletId) rows = rows.filter((e) => String(e.walletId) === String(p.walletId))
|
||||||
|
if (p.keyword) {
|
||||||
|
const k = String(p.keyword).trim()
|
||||||
|
rows = rows.filter((e) => (e.memo || '').includes(k) || (e.category || '').includes(k))
|
||||||
|
}
|
||||||
|
if (p.tagId) {
|
||||||
|
const t = TAGS.find((x) => String(x.id) === String(p.tagId))
|
||||||
|
if (t) rows = rows.filter((e) => (e.tags || []).includes(t.name))
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
function summaryOf(p = {}) {
|
||||||
let income = 0, expense = 0
|
let income = 0, expense = 0
|
||||||
for (const e of ENTRIES) {
|
for (const e of ENTRIES) {
|
||||||
|
if (!inMonth(e, p.year, p.month)) continue
|
||||||
if (e.type === 'INCOME') income += e.amount
|
if (e.type === 'INCOME') income += e.amount
|
||||||
else if (e.type === 'EXPENSE') expense += e.amount
|
else if (e.type === 'EXPENSE') expense += e.amount
|
||||||
}
|
}
|
||||||
return { totalIncome: income, totalExpense: expense, balance: income - expense }
|
return { totalIncome: income, totalExpense: expense, balance: income - expense }
|
||||||
})()
|
}
|
||||||
const NETWORTH = { totalAssets: 15860000, totalLiabilities: 340000, netWorth: 15520000 }
|
const NETWORTH = { totalAssets: 15860000, totalLiabilities: 340000, netWorth: 15520000 }
|
||||||
|
|
||||||
function ok(data) {
|
function ok(data) {
|
||||||
@@ -115,8 +136,8 @@ function block() {
|
|||||||
|
|
||||||
// 읽기 응답 / 쓰기 차단
|
// 읽기 응답 / 쓰기 차단
|
||||||
export const demoApi = {
|
export const demoApi = {
|
||||||
list: () => ok(ENTRIES),
|
list: (params) => ok(filterEntries(params)),
|
||||||
summary: () => ok(SUMMARY),
|
summary: (params) => ok(summaryOf(params)),
|
||||||
wallets: () => ok(WALLETS),
|
wallets: () => ok(WALLETS),
|
||||||
netWorth: () => ok(NETWORTH),
|
netWorth: () => ok(NETWORTH),
|
||||||
categories: () => ok(CATEGORIES),
|
categories: () => ok(CATEGORIES),
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// 데이터 백업 — 가계부 전체 데이터를 엑셀(.xlsx)로 내보내기.
|
||||||
|
// 참조는 이름 기준(계좌명·분류명·태그명)으로 저장해 추후 '가져오기(복구)' 시 재매핑이 단순하도록 한다.
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
import { accountApi } from '@/api/accountApi'
|
||||||
|
|
||||||
|
async function safe(promise) {
|
||||||
|
try {
|
||||||
|
return await promise
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileName() {
|
||||||
|
const d = new Date()
|
||||||
|
const p = (n) => String(n).padStart(2, '0')
|
||||||
|
return `돈돼지가계부_백업_${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}.xlsx`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 워크북을 플랫폼에 맞게 저장: 웹/PC=다운로드, 앱(네이티브)=파일 저장 후 공유
|
||||||
|
async function saveWorkbook(XLSX, wb, name) {
|
||||||
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
const base64 = XLSX.write(wb, { type: 'base64', bookType: 'xlsx' })
|
||||||
|
const { Filesystem, Directory } = await import('@capacitor/filesystem')
|
||||||
|
const { Share } = await import('@capacitor/share')
|
||||||
|
const res = await Filesystem.writeFile({ path: name, data: base64, directory: Directory.Cache })
|
||||||
|
try {
|
||||||
|
await Share.share({ title: name, text: '가계부 백업 파일', url: res.uri })
|
||||||
|
} catch {
|
||||||
|
/* 공유 취소 — 파일은 캐시에 저장됨 */
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
XLSX.writeFile(wb, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportBackup() {
|
||||||
|
const XLSX = await import('xlsx')
|
||||||
|
|
||||||
|
const [entries, wallets, categories, recurrings, budgets, tags, quick] = await Promise.all([
|
||||||
|
safe(accountApi.list({})),
|
||||||
|
safe(accountApi.wallets()),
|
||||||
|
safe(accountApi.categories()),
|
||||||
|
safe(accountApi.recurrings()),
|
||||||
|
safe(accountApi.budgets()),
|
||||||
|
safe(accountApi.tags()),
|
||||||
|
safe(accountApi.quickEntries()),
|
||||||
|
])
|
||||||
|
|
||||||
|
const catNameById = {}
|
||||||
|
categories.forEach((c) => (catNameById[c.id] = c.name))
|
||||||
|
|
||||||
|
const wb = XLSX.utils.book_new()
|
||||||
|
const add = (name, rows) => XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(rows), name)
|
||||||
|
|
||||||
|
add('내역', entries.map((e) => ({
|
||||||
|
날짜: e.entryDate, 구분: e.type, 분류: e.category, 금액: e.amount, 메모: e.memo,
|
||||||
|
계좌: e.walletName, 입금계좌: e.toWalletName, 할부개월: e.installmentMonths,
|
||||||
|
태그: (e.tags || []).join(','),
|
||||||
|
})))
|
||||||
|
add('계좌', wallets.map((w) => ({
|
||||||
|
종류: w.type, 이름: w.name, '은행/카드사': w.issuer, 계좌번호: w.accountNumber,
|
||||||
|
카드유형: w.cardType, 개시잔액: w.openingBalance, 개시일: w.openingDate, '평가액(수동)': w.currentValue,
|
||||||
|
})))
|
||||||
|
add('분류', categories.map((c) => ({
|
||||||
|
구분: c.type, 이름: c.name, 대분류: c.parentId ? catNameById[c.parentId] || '' : '',
|
||||||
|
})))
|
||||||
|
add('고정지출', recurrings.map((r) => ({
|
||||||
|
제목: r.title, 구분: r.type, 금액: r.amount, 분류: r.category, 메모: r.memo,
|
||||||
|
계좌: r.walletName, 입금계좌: r.toWalletName, 주기: r.frequency,
|
||||||
|
일: r.dayOfMonth, 요일: r.dayOfWeek, 월: r.monthOfYear,
|
||||||
|
시작일: r.startDate, 종료일: r.endDate, 활성: r.active,
|
||||||
|
})))
|
||||||
|
add('예산', budgets.map((b) => ({
|
||||||
|
분류: b.category, 고정: b.fixed, 기준단위: b.baseUnit, 기준금액: b.baseAmount,
|
||||||
|
일: b.daily, 주: b.weekly, 월: b.monthly, 년: b.yearly,
|
||||||
|
})))
|
||||||
|
add('태그', tags.map((t) => ({ 이름: t.name })))
|
||||||
|
add('자주쓰는내역', quick.map((q) => ({
|
||||||
|
라벨: q.label, 구분: q.type, 분류: q.category, 금액: q.amount, 메모: q.memo, 계좌: q.walletName,
|
||||||
|
})))
|
||||||
|
|
||||||
|
await saveWorkbook(XLSX, wb, fileName())
|
||||||
|
}
|
||||||
@@ -700,10 +700,15 @@ onMounted(load)
|
|||||||
}
|
}
|
||||||
.cta {
|
.cta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
justify-content: center;
|
|
||||||
margin-top: 1.4rem;
|
margin-top: 1.4rem;
|
||||||
}
|
}
|
||||||
|
.cta .btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 260px;
|
||||||
|
}
|
||||||
.btn {
|
.btn {
|
||||||
padding: 0.6rem 1.4rem;
|
padding: 0.6rem 1.4rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|||||||
@@ -4,9 +4,26 @@ import { RouterLink } from 'vue-router'
|
|||||||
import { Preferences } from '@capacitor/preferences'
|
import { Preferences } from '@capacitor/preferences'
|
||||||
import { useDialog } from '@/composables/dialog'
|
import { useDialog } from '@/composables/dialog'
|
||||||
import { useUiStore } from '@/stores/ui'
|
import { useUiStore } from '@/stores/ui'
|
||||||
|
import { exportBackup } from '@/utils/backup'
|
||||||
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const ui = useUiStore()
|
const ui = useUiStore()
|
||||||
|
|
||||||
|
const exporting = ref(false)
|
||||||
|
async function doExport() {
|
||||||
|
if (exporting.value) return
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
await exportBackup()
|
||||||
|
} catch (e) {
|
||||||
|
window.alert(e?.message || '내보내기에 실패했습니다.')
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function importBackup() {
|
||||||
|
dialog.alert('가져오기(복구)는 준비 중입니다. 곧 제공됩니다.')
|
||||||
|
}
|
||||||
const THEMES = [
|
const THEMES = [
|
||||||
{ value: 'system', label: '시스템' },
|
{ value: 'system', label: '시스템' },
|
||||||
{ value: 'light', label: '라이트' },
|
{ value: 'light', label: '라이트' },
|
||||||
@@ -92,6 +109,24 @@ async function clearAppData() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 데이터 백업 -->
|
||||||
|
<section class="card">
|
||||||
|
<button type="button" class="row row-btn" :disabled="exporting" @click="doExport">
|
||||||
|
<div class="row-main">
|
||||||
|
<span class="row-label">엑셀로 내보내기</span>
|
||||||
|
<span class="row-sub">가계부 전체 데이터를 .xlsx 파일로 저장</span>
|
||||||
|
</div>
|
||||||
|
<span class="row-value">{{ exporting ? '내보내는 중…' : '내보내기' }}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="row row-btn" @click="importBackup">
|
||||||
|
<div class="row-main">
|
||||||
|
<span class="row-label">엑셀에서 가져오기</span>
|
||||||
|
<span class="row-sub">백업 파일로 데이터 복구</span>
|
||||||
|
</div>
|
||||||
|
<span class="row-value">가져오기</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<RouterLink to="/settings/account" class="row row-link">
|
<RouterLink to="/settings/account" class="row row-link">
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
|
|||||||
Reference in New Issue
Block a user