From 6a4d01b6e15d6fc910893aa3bfa5124765f5bf0b Mon Sep 17 00:00:00 2001 From: sb Date: Sun, 5 Jul 2026 14:09:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(account):=20=EC=9E=90=EC=97=B0=EC=96=B4=20?= =?UTF-8?q?=EB=B9=A0=EB=A5=B8=EC=9E=85=EB=A0=A5=20=EC=99=84=EC=84=B1=20?= =?UTF-8?q?=E2=80=94=20=EB=B6=84=EB=A5=98=C2=B7=EA=B3=84=EC=A2=8C=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=B6=94=EC=B2=9C=20+=20=EB=82=B4=EC=97=AD?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A7=84=EC=9E=85=EC=A0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - applyParsed의 미선언 categoryMajor 참조 버그 수정(응답 처리 실패 원인) - 빠른입력 모달 버튼 텍스트화 + 안전영역 하단 고정(가림 해결) - 클립보드 자동입력을 거래성 텍스트일 때만(로컬 판단, 불필요 AI 호출 제거) - AI 응답의 분류·결제수단을 폼에 자동 매칭(사용자 실제 목록 기준) - 내역 화면에 '✨ 빠른 입력' 텍스트 바 진입점 추가 Co-Authored-By: Claude Opus 4.8 --- ios/App/App.xcodeproj/project.pbxproj | 2 + src/api/accountApi.js | 5 +- src/components/ui/IconBtn.vue | 1 + src/views/account/AccountView.vue | 142 ++++++++++++++++++++++---- 4 files changed, 130 insertions(+), 20 deletions(-) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 5f7be39..3a9a2e3 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -297,6 +297,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = XDT9UV245W; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -319,6 +320,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = XDT9UV245W; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/src/api/accountApi.js b/src/api/accountApi.js index 75f296f..579b6bb 100644 --- a/src/api/accountApi.js +++ b/src/api/accountApi.js @@ -28,8 +28,9 @@ const realApi = { return http.delete(`/account/entries/${id}`) }, // 문자/푸시 텍스트 파싱(저장 안 함) → 추가 폼 자동채움 - parseText({ title, text } = {}) { - return http.post('/account/entries/parse', { title, text }) + // categoryHints/walletHints: AI가 이 후보들 중에서 분류·결제수단을 고르도록 사용자 목록 전달 + parseText({ title, text, categoryHints, walletHints } = {}) { + return http.post('/account/entries/parse', { title, text, categoryHints, walletHints }) }, // 환율 조회 — 외화 결제 입력 시 통화→원화 환율 자동 채움 fxRate(from, to = 'KRW') { diff --git a/src/components/ui/IconBtn.vue b/src/components/ui/IconBtn.vue index 9934a9b..8a701bf 100644 --- a/src/components/ui/IconBtn.vue +++ b/src/components/ui/IconBtn.vue @@ -35,6 +35,7 @@ const ICONS = { send: ['M22 2L11 13', 'M22 2l-7 20-4-9-9-4 20-7z'], save: ['M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z', 'M17 21v-8H7v8', 'M7 3v5h8'], lock: ['M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2z', 'M8 11V7a4 4 0 0 1 8 0v4'], + sparkle: ['M12 4l1.6 6.4L20 12l-6.4 1.6L12 20l-1.6-6.4L4 12l6.4-1.6z'], } const paths = computed(() => ICONS[props.icon] || ICONS.plus) diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index ffaa070..3142ed2 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -477,6 +477,12 @@ function openCreate() { formOpen.value = true tryClipboardOnOpen() // 문자/푸시 클립보드 자동 감지(있으면 배너) } +// 내역 화면에서 바로 빠른입력: 추가 폼을 준비해 열고, 그 위에 빠른입력 모달을 띄운다. +// 분석·채우기 후 빠른입력 모달은 닫히고, 채워진 추가 폼이 남아 확인·저장. +function openQuickInput() { + openCreate() + openPasteModal() +} function openEdit(e) { editId.value = e.id editingPending.value = !!e.pending @@ -774,51 +780,67 @@ function applyParsed(p) { if (p.amount) form.amount = Number(p.amount) if (p.merchant) form.memo = p.merchant if (p.date) form.entryDate = p.date - categoryMajor.value = '' + // AI 추천 분류(후보 목록 중에서 선택된 값). 없으면 사용자가 직접 선택 + form.category = p.category || '' + // AI 추천 결제수단(계좌) — 이름으로 실제 계좌를 찾아 종류·계좌 자동 선택 + if (p.wallet) { + const w = wallets.value.find((x) => x.name === p.wallet) + if (w) { + form.walletKind = w.type + form.walletId = w.id + } + } pasteDetected.value = null pasteModalOpen.value = false pasteText.value = '' pasteError.value = '' + formOpen.value = true // 내역 화면에서 바로 빠른입력한 경우에도 채워진 추가 폼이 보이도록 보장 } async function parseAndApply(text, { silent } = {}) { const t = (text || '').trim() if (!t) return false try { - const p = await accountApi.parseText({ text: t }) + // 사용자의 실제 분류·계좌 이름을 함께 보내 AI가 그 중에서 고르게 함 + const p = await accountApi.parseText({ + text: t, + categoryHints: allCategoryNames.value, + walletHints: wallets.value.map((w) => w.name), + }) if (p && p.recognized) { if (silent) pasteDetected.value = p else applyParsed(p) return true } - } catch { - // 무시 + } catch (e) { + // 네트워크 오류는 조용히 폴백하되, 예외는 콘솔에 남겨 원인 추적이 가능하게 한다 + console.error('[parseAndApply] 실패:', e) } return false } -// 추가 팝업 열 때 클립보드 자동 감지(권한 막히면 조용히 무시) → 버튼 배지로 안내 +// 클립보드가 카드문자/거래처럼 보이는지 로컬로만 판단(AI 호출 없음 — 비용·오작동 방지) +function looksLikeTxn(t) { + return !!t && /\d/.test(t) && /(원|₩|승인|입금|출금|결제|카드|이체)/.test(t) +} +// 추가 팝업 열 때: 클립보드에 거래성 텍스트가 있으면 버튼에 점 배지만 표시(자동 전송 안 함) async function tryClipboardOnOpen() { pasteDetected.value = null - pasteText.value = '' try { const txt = await navigator.clipboard.readText() - await parseAndApply(txt, { silent: true }) + if (looksLikeTxn(txt)) pasteDetected.value = { local: true } } catch { - // 클립보드 접근 불가 — 버튼으로 수동 + // 클립보드 접근 불가 — 무시 } } -// '문자·푸시' 버튼 → 모달 열기 + 클립보드 자동 읽기 +// '빠른 입력' 버튼 → 모달 열기. 클립보드가 카드문자처럼 보일 때만 미리 채워줌(자동 AI 전송 없음 — 사용자가 버튼으로 분석) async function openPasteModal() { pasteError.value = '' pasteModalOpen.value = true if (!pasteText.value) { try { const txt = await navigator.clipboard.readText() - if (txt) { - pasteText.value = txt - await parseAndApply(txt, { silent: true }) - } + if (looksLikeTxn(txt)) pasteText.value = txt } catch { - // 클립보드 접근 불가 — 수동 붙여넣기 + // 클립보드 접근 불가 — 수동 입력 } } } @@ -932,6 +954,16 @@ onMounted(async () => { + + +

{{ error }}

불러오는 중...

@@ -1252,13 +1284,13 @@ onMounted(async () => {

빠른 입력

자연어로 입력하거나 문자·푸시를 붙여넣고 분석하면 구분·금액·메모·날짜가 자동 채워집니다.

-
+
감지: {{ pasteDetected.type === 'INCOME' ? '수입' : '지출' }} {{ won(pasteDetected.amount) }}

{{ pasteError }}

-
- - +
+ +
@@ -1365,6 +1397,54 @@ button.primary { gap: 0.75rem; margin-bottom: 1.25rem; } +/* 자연어 빠른 입력 바 */ +.quick-input-bar { + display: flex; + align-items: center; + gap: 0.7rem; + width: 100%; + margin-bottom: 1rem; + padding: 0.7rem 0.9rem; + border: 1px solid hsla(160, 100%, 37%, 0.45); + border-radius: 12px; + background: hsla(160, 100%, 37%, 0.08); + color: var(--color-text); + cursor: pointer; + text-align: left; + transition: background 0.15s ease; +} +.quick-input-bar:hover { + background: hsla(160, 100%, 37%, 0.14); +} +.quick-input-bar .qib-icon { + font-size: 1.25rem; + line-height: 1; +} +.quick-input-bar .qib-body { + display: flex; + flex-direction: column; + gap: 0.1rem; + flex: 1; + min-width: 0; +} +.quick-input-bar .qib-title { + font-weight: 700; + font-size: 0.95rem; + color: hsla(160, 100%, 32%, 1); +} +.quick-input-bar .qib-sub { + font-size: 0.78rem; + opacity: 0.65; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.quick-input-bar .qib-plus { + font-size: 1.3rem; + font-weight: 700; + color: hsla(160, 100%, 37%, 1); + flex: 0 0 auto; +} .card { flex: 1; border: 1px solid var(--color-border); @@ -2120,6 +2200,32 @@ button.primary { font: inherit; resize: vertical; } +/* 빠른입력 하단 버튼: 텍스트 표기 + 홈 인디케이터 위로(안전영역 밀림 방지) */ +.paste-modal .pm-buttons { + bottom: 0; + gap: 0.6rem; + padding-bottom: calc(0.2rem + env(safe-area-inset-bottom)); +} +.pm-btn { + padding: 0.85rem 1rem; + border-radius: 10px; + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; + border: 1px solid var(--color-border); +} +.pm-cancel { + flex: 0 0 auto; + min-width: 84px; + background: var(--color-background-soft); + color: var(--color-text); +} +.pm-primary { + flex: 1; + background: hsla(160, 100%, 37%, 1); + border-color: hsla(160, 100%, 37%, 1); + color: #fff; +} .to-recurring { display: block; width: 100%;