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 재빌드 업로드 필요)
---
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 () => {