diff --git a/src/api/accountApi.js b/src/api/accountApi.js index 2611b27..26b2123 100644 --- a/src/api/accountApi.js +++ b/src/api/accountApi.js @@ -26,6 +26,21 @@ export const accountApi = { remove(id) { return http.delete(`/account/entries/${id}`) }, + // 문자/푸시 텍스트 파싱(저장 안 함) → 추가 폼 자동채움 + parseText({ title, text } = {}) { + return http.post('/account/entries/parse', { title, text }) + }, + + // 자주 쓰는 내역(빠른 등록 템플릿) + quickEntries() { + return http.get('/account/quick-entries') + }, + createQuickEntry(payload) { + return http.post('/account/quick-entries', payload) + }, + removeQuickEntry(id) { + return http.delete(`/account/quick-entries/${id}`) + }, // 계좌/카드 (사용자별) wallets() { diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index 054f3f3..d88a5e1 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -445,6 +445,7 @@ function openCreate() { cancelAddCategory() formError.value = null formOpen.value = true + tryClipboardOnOpen() // 문자/푸시 클립보드 자동 감지(있으면 배너) } function openEdit(e) { editId.value = e.id @@ -473,6 +474,8 @@ function openEdit(e) { selectedTagIds.value = (e.tags || []).map((n) => nameToId[n]).filter(Boolean) syncCategoryMajor() cancelAddCategory() + pasteDetected.value = null + pasteOpen.value = false formError.value = null formOpen.value = true } @@ -644,6 +647,130 @@ async function doRegisterRecurring() { } } +// ===== 자주 쓰는 내역(빠른 등록 템플릿) ===== +const quickEntries = ref([]) +const quickEditMode = ref(false) +async function loadQuick() { + try { + quickEntries.value = await accountApi.quickEntries() + } catch { + quickEntries.value = [] + } +} +function quickLabel(q) { + return q.label || q.category || q.memo || (q.type === 'INCOME' ? '수입' : '지출') +} +// 칩 탭 → 오늘 날짜로 즉시 등록 +async function useQuick(q) { + if (quickEditMode.value) return + try { + await accountApi.create({ + entryDate: todayStr(), + type: q.type, + category: q.category || null, + amount: Number(q.amount), + memo: q.memo || null, + walletId: q.walletId || null, + toWalletId: null, + installmentMonths: null, + tagIds: [], + }) + await load() + await loadPendingCount() + flash(`'${quickLabel(q)}' ${won(q.amount)} 등록했습니다.`) + } catch (e) { + flash(e.response?.data?.message || '빠른 등록에 실패했습니다.') + } +} +async function removeQuick(q) { + try { + await accountApi.removeQuickEntry(q.id) + await loadQuick() + } catch (e) { + flash(e.response?.data?.message || '삭제에 실패했습니다.') + } +} +// 현재 폼 값을 자주 쓰는 내역으로 저장 +async function saveAsQuick() { + if (form.type !== 'INCOME' && form.type !== 'EXPENSE') return + const amount = Number(form.amount) + if (!amount || amount <= 0) { + formError.value = '금액을 올바르게 입력하세요.' + return + } + try { + await accountApi.createQuickEntry({ + label: (form.memo || form.category || '').trim() || null, + type: form.type, + category: form.category || null, + amount, + memo: form.memo || null, + walletId: form.walletId || null, + }) + await loadQuick() + flash('자주 쓰는 내역으로 저장했습니다.') + } catch (e) { + formError.value = e.response?.data?.message || '저장에 실패했습니다.' + } +} + +// ===== 문자/푸시 붙여넣기 → 파싱 자동채움 ===== +const pasteDetected = ref(null) // 클립보드 자동감지 결과(배너) +const pasteOpen = ref(false) // 수동 붙여넣기 입력칸 +const pasteText = ref('') +function applyParsed(p) { + if (!p || !p.recognized) return + form.type = p.type === 'INCOME' ? 'INCOME' : 'EXPENSE' + if (p.amount) form.amount = Number(p.amount) + if (p.merchant) form.memo = p.merchant + if (p.date) form.entryDate = p.date + categoryMajor.value = '' + pasteDetected.value = null + pasteOpen.value = false + pasteText.value = '' +} +async function parseAndApply(text, { silent } = {}) { + const t = (text || '').trim() + if (!t) return false + try { + const p = await accountApi.parseText({ text: t }) + if (p && p.recognized) { + if (silent) pasteDetected.value = p + else applyParsed(p) + return true + } + } catch { + // 무시 + } + return false +} +// 추가 팝업 열 때 클립보드 자동 감지(권한 막히면 조용히 무시) +async function tryClipboardOnOpen() { + pasteDetected.value = null + pasteOpen.value = false + pasteText.value = '' + try { + const txt = await navigator.clipboard.readText() + await parseAndApply(txt, { silent: true }) + } catch { + // 클립보드 접근 불가 — 버튼으로 수동 + } +} +// '붙여넣기' 버튼: 클립보드 읽어 바로 적용, 실패 시 수동 입력칸 +async function pasteFromClipboard() { + try { + const txt = await navigator.clipboard.readText() + const ok = await parseAndApply(txt, { silent: false }) + if (!ok) pasteOpen.value = true + } catch { + pasteOpen.value = true + } +} +async function analyzePasted() { + const ok = await parseAndApply(pasteText.value, { silent: false }) + if (!ok) formError.value = '거래 내용을 인식하지 못했습니다.' +} + onMounted(async () => { // 진입 시 밀린 정기 거래를 먼저 반영한 뒤 목록을 불러온다 (중복 없이) try { @@ -656,6 +783,7 @@ onMounted(async () => { loadWallets() loadCategories() loadPendingCount() + loadQuick() checkNotifPermission() }) @@ -668,6 +796,19 @@ onMounted(async () => {

{{ flashMsg }}

+ +
+ + +
+
💳 카드 결제 알림을 자동으로 가계부에 등록하려면 알림 접근 권한이 필요합니다. @@ -806,6 +947,19 @@ onMounted(async () => {
+ +
+ +
+ 감지: {{ pasteDetected.type === 'INCOME' ? '수입' : '지출' }} {{ won(pasteDetected.amount) }} + +
+
+ + +
+
+