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 }}