feat(fx): 환율 갱신 시각 표시 및 자동 업데이트 레이블
Deploy / deploy (push) Failing after 13m33s

- 환율 자동조회 후 '방금 업데이트 / N분 전 / N시간 전' 표시
- 30초 간격 ticker로 레이블 자동 갱신
- 폼 열기/통화 변경 시 rateUpdatedAt 초기화

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-07-05 14:52:00 +09:00
parent e23a694675
commit c069e70de4
+29 -3
View File
@@ -1,5 +1,5 @@
<script setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { accountApi } from '@/api/accountApi'
import { reminders } from '@/native/reminders'
import { useAuthStore } from '@/stores/auth'
@@ -99,14 +99,31 @@ watch([() => form.currency, () => form.foreignAmount, () => form.rate], () => {
if (isForeign.value) form.amount = convertedKrw.value
})
const fxLoading = ref(false)
const rateUpdatedAt = ref(null) // 환율 마지막 조회 시각(ms)
const tickNow = ref(Date.now())
let _fxTick = null
onMounted(() => { _fxTick = setInterval(() => { tickNow.value = Date.now() }, 30_000) })
onUnmounted(() => clearInterval(_fxTick))
const rateAgeLabel = computed(() => {
if (!rateUpdatedAt.value || !isForeign.value) return null
const diff = Math.floor((tickNow.value - rateUpdatedAt.value) / 1000)
if (diff < 60) return '방금 업데이트'
if (diff < 3600) return `${Math.floor(diff / 60)}분 전`
if (diff < 86400) return `${Math.floor(diff / 3600)}시간 전`
return `${Math.floor(diff / 86400)}일 전`
})
async function fetchRate() {
if (!isForeign.value || fxLoading.value) return
fxLoading.value = true
try {
const res = await accountApi.fxRate(form.currency, 'KRW')
// 환율은 소수점 4자리로 통일(입력폼 step·저장 정밀도 일치)
if (res?.rate != null) form.rate = Math.round(Number(res.rate) * 10000) / 10000
else formInfo.value = '환율을 가져오지 못했어요. 직접 입력해 주세요.'
if (res?.rate != null) {
form.rate = Math.round(Number(res.rate) * 10000) / 10000
rateUpdatedAt.value = res.updatedAt ? new Date(res.updatedAt).getTime() : Date.now()
} else {
formInfo.value = '환율을 가져오지 못했어요. 직접 입력해 주세요.'
}
} catch {
formInfo.value = '환율 조회에 실패했어요. 직접 입력해 주세요.'
} finally {
@@ -120,6 +137,7 @@ function onCurrencyChange() {
} else {
form.foreignAmount = null
form.rate = null
rateUpdatedAt.value = null
}
}
// 계좌/카드 (repayTargetIsCard·repayTargetLoanWallet computed가 참조하므로 먼저 선언)
@@ -500,6 +518,7 @@ 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: '', currency: 'KRW', foreignAmount: null, rate: null })
rateUpdatedAt.value = null
selectedTagIds.value = []
formError.value = null
formInfo.value = ''
@@ -540,6 +559,7 @@ function openEdit(e) {
const nameToId = {}
tagOptions.value.forEach((t) => (nameToId[t.name] = t.id))
selectedTagIds.value = (e.tags || []).map((n) => nameToId[n]).filter(Boolean)
rateUpdatedAt.value = null
pasteDetected.value = null
pasteModalOpen.value = false
pasteText.value = ''
@@ -1212,6 +1232,7 @@ onMounted(async () => {
<input v-model.number="form.rate" type="number" min="0" step="0.0001" placeholder="원" :disabled="submitting" />
<button type="button" class="fx-auto" :disabled="submitting || fxLoading" @click="fetchRate">{{ fxLoading ? '' : ' 자동' }}</button>
</div>
<span v-if="rateAgeLabel" class="fx-age">{{ rateAgeLabel }}</span>
</div>
<span class="fx-krw">= {{ (convertedKrw || 0).toLocaleString('ko-KR') }}</span>
</div>
@@ -2130,6 +2151,11 @@ button.primary {
font-size: 0.9rem;
padding-bottom: 0.45rem;
}
.fx-age {
font-size: 0.7rem;
color: var(--color-text-soft, #999);
margin-top: 0.18rem;
}
.row-wallet {
margin-right: 0.35rem;
font-size: 0.75rem;