From caa591a16de3a1adf4f9ac5a7030a979e9b4ac14 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Mon, 29 Jun 2026 21:42:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20versionCode=202=20/=201.0.1=20(v2?= =?UTF-8?q?=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C)=20+=20=EB=B0=B1=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 4 ++-- brand/LAUNCH-PROGRESS.md | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0c8aa49..d1981c7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "kr.sblog.slimbudget" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0" + versionCode 2 + versionName "1.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/brand/LAUNCH-PROGRESS.md b/brand/LAUNCH-PROGRESS.md index d5b68a9..7e31235 100644 --- a/brand/LAUNCH-PROGRESS.md +++ b/brand/LAUNCH-PROGRESS.md @@ -109,7 +109,9 @@ 2) 게시글 등록 **키워드 필터링** 3) **추천 +5 / 비추 -1** (일일 최대 적립 50포인트) 4) 다른 사용자 글에 **추천 누르면 +1** (일일 10 제한), 추천 **취소 시 -1** -5. **분류 화면 순서변경 보호** — 스크롤하다 실수로 순서 바뀌는 것 방지: 기본은 drag&drop 비활성, **"순서변경" 버튼 클릭 시에만** 드래그 가능 +5. ✅ **[수정완료] 분류 화면 순서변경 보호** — 기본 drag 잠금 + '순서변경' 토글로만 가능. (v2 포함) + +> **v2 빌드 완료(versionCode 2, versionName 1.0.1, 2026-06-29)** — #5·#6 + 모든 다듬기 포함. 비공개 테스트 트랙에 업로드 대기. (업로드해도 14일 카운트 리셋 안 됨) 6. ✅ **[버그·수정완료] 앱 잠금이 카메라/갤러리 복귀 시 오작동** — appLock.suppressLock() 추가, 파일선택은 App.vue 전역 리스너, 카메라/공유는 호출부에서 suppress. (코드 배포됨 — 테스터에게 반영하려면 versionCode 2로 AAB 재빌드 업로드 필요) --- From dab6dc3411222ecfcc493f48f3b2eb19056cf5e0 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Tue, 30 Jun 2026 21:37:24 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EC=99=B8=ED=99=94=20=EC=9E=85/?= =?UTF-8?q?=EC=A7=80=EC=B6=9C=20=EC=9E=85=EB=A0=A5=20=E2=80=94=20=ED=86=B5?= =?UTF-8?q?=ED=99=94=20=EC=84=A0=ED=83=9D=20+=20=ED=99=98=EC=9C=A8=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=A1=B0=ED=9A=8C=20=E2=86=92=20=EC=9B=90?= =?UTF-8?q?=ED=99=94=20=ED=99=98=EC=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입력폼: 통화 드롭다운(KRW 기본 + 15개), 외화면 외화금액+환율 입력 → 환산 원화 자동 계산(amount). 환율은 /fx/rate 자동조회('↻ 자동') + 수정 가능 - 저장: 외화면 currency/originalAmount/exchangeRate 전송, amount=환산 원화 - 목록 상세에 'USD 10.00 @1,350' 외화 뱃지 표시 - accountApi.fxRate 추가 Co-Authored-By: Claude Opus 4.8 --- src/api/accountApi.js | 4 + src/views/account/AccountView.vue | 121 +++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/src/api/accountApi.js b/src/api/accountApi.js index a461886..b25b5b4 100644 --- a/src/api/accountApi.js +++ b/src/api/accountApi.js @@ -31,6 +31,10 @@ const realApi = { parseText({ title, text } = {}) { return http.post('/account/entries/parse', { title, text }) }, + // 환율 조회 — 외화 결제 입력 시 통화→원화 환율 자동 채움 + fxRate(from, to = 'KRW') { + return http.get('/fx/rate', { params: { from, to } }) + }, // 자주 쓰는 내역(빠른 등록 템플릿) quickEntries() { diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index 85bd044..901ed30 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -84,7 +84,42 @@ function resetFilters() { // 추가/수정 모달 const formOpen = ref(false) const editId = ref(null) -const form = reactive({ entryDate: '', type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: '', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, annualFee: null, installmentMonths: '' }) +const form = reactive({ entryDate: '', type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: '', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, annualFee: null, installmentMonths: '', currency: 'KRW', foreignAmount: null, rate: null }) + +// ===== 외화 결제 ===== +const CURRENCIES = ['KRW', 'USD', 'JPY', 'EUR', 'CNY', 'GBP', 'AUD', 'CAD', 'HKD', 'SGD', 'THB', 'VND', 'TWD', 'PHP', 'MYR'] +const isForeign = computed(() => form.currency && form.currency !== 'KRW') +// JPY 등은 원본이 정수 위주라 100단위 표기가 흔하지만, 환율은 1단위 기준으로 통일 +const convertedKrw = computed(() => + isForeign.value ? Math.round((Number(form.foreignAmount) || 0) * (Number(form.rate) || 0)) : null, +) +// 외화면 환산 원화를 amount 에 반영(저장·검증·표시는 원화 기준) +watch([() => form.currency, () => form.foreignAmount, () => form.rate], () => { + if (isForeign.value) form.amount = convertedKrw.value +}) +const fxLoading = ref(false) +async function fetchRate() { + if (!isForeign.value || fxLoading.value) return + fxLoading.value = true + try { + const res = await accountApi.fxRate(form.currency, 'KRW') + if (res?.rate != null) form.rate = Number(res.rate) + else formInfo.value = '환율을 가져오지 못했어요. 직접 입력해 주세요.' + } catch { + formInfo.value = '환율 조회에 실패했어요. 직접 입력해 주세요.' + } finally { + fxLoading.value = false + } +} +function onCurrencyChange() { + if (isForeign.value) { + if (form.foreignAmount == null && form.amount != null) form.foreignAmount = form.amount // 원화→외화 전환 시 초기값 + fetchRate() + } else { + form.foreignAmount = null + form.rate = null + } +} const isRepayment = computed(() => form.type === 'REPAYMENT') // 상환 대상이 카드면 연회비 입력 노출(대출은 연회비 없음) const repayTargetIsCard = computed(() => { @@ -452,7 +487,7 @@ function todayStr() { function openCreate() { editId.value = null editingPending.value = false - Object.assign(form, { entryDate: todayStr(), type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: 'BANK', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, annualFee: null, installmentMonths: '' }) + Object.assign(form, { entryDate: todayStr(), type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: 'BANK', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, annualFee: null, installmentMonths: '', currency: 'KRW', foreignAmount: null, rate: null }) selectedTagIds.value = [] syncCategoryMajor() cancelAddCategory() @@ -481,6 +516,9 @@ function openEdit(e) { interest: null, annualFee: null, installmentMonths: e.installmentMonths || '', + currency: e.currency && e.currency !== 'KRW' ? e.currency : 'KRW', + foreignAmount: e.currency && e.currency !== 'KRW' ? Number(e.originalAmount) : null, + rate: e.currency && e.currency !== 'KRW' ? Number(e.exchangeRate) : null, }) // 태그 이름 → id 매핑 (현재 태그 목록 기준) const nameToId = {} @@ -559,6 +597,7 @@ async function submit() { } } submitting.value = true + const foreign = isForeign.value const payload = { entryDate: form.entryDate, type: form.type, @@ -569,6 +608,10 @@ async function submit() { toWalletId: form.type === 'TRANSFER' ? form.toWalletId || null : null, installmentMonths: showInstallment.value && Number(form.installmentMonths) >= 2 ? Number(form.installmentMonths) : null, tagIds: selectedTagIds.value, + // 외화 결제: amount 는 환산 원화, 원본은 별도 보존 + currency: foreign ? form.currency : null, + originalAmount: foreign ? Number(form.foreignAmount) : null, + exchangeRate: foreign ? Number(form.rate) : null, } try { if (editId.value) { @@ -947,7 +990,8 @@ onMounted(async () => { -
+
+ {{ e.currency }} {{ Number(e.originalAmount).toLocaleString('ko-KR') }} @{{ Number(e.exchangeRate).toLocaleString('ko-KR') }} {{ e.installmentMonths }}개월 할부 · 월 {{ won(Math.round(e.amount / e.installmentMonths)) }} {{ e.memo }} {{ t }} @@ -1095,7 +1139,23 @@ onMounted(async () => {
- + +
+ + + = ₩{{ (convertedKrw || 0).toLocaleString('ko-KR') }} +
@@ -1853,6 +1913,59 @@ button.primary { color: hsla(160, 100%, 37%, 1); white-space: nowrap; } +/* 외화 표기(목록 상세) */ +.ei-fx { + font-size: 0.74rem; + padding: 0.05rem 0.4rem; + border: 1px solid hsla(40, 90%, 50%, 0.6); + border-radius: 3px; + color: #b8860b; + white-space: nowrap; +} +/* 입력폼 금액/환율 */ +.amount-row { + display: flex; + gap: 0.4rem; +} +.cur-sel { + flex: none; + width: 5rem; +} +.amount-row input { + flex: 1; + min-width: 0; +} +.fx-row { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + margin: -0.3rem 0 0.2rem; +} +.fx-rate { + display: flex; + flex-direction: column; + font-size: 0.8rem; +} +.fx-rate input { + width: 7rem; +} +.fx-auto { + align-self: flex-end; + padding: 0.4rem 0.7rem; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-background-soft); + color: var(--color-text); + font-size: 0.82rem; + cursor: pointer; +} +.fx-krw { + align-self: flex-end; + font-weight: 700; + color: #b8860b; + font-size: 0.9rem; +} .row-wallet { margin-right: 0.35rem; font-size: 0.75rem;