- 환율 자동조회 후 '방금 업데이트 / N분 전 / N시간 전' 표시 - 30초 간격 ticker로 레이블 자동 갱신 - 폼 열기/통화 변경 시 rateUpdatedAt 초기화 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<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 { accountApi } from '@/api/accountApi'
|
||||||
import { reminders } from '@/native/reminders'
|
import { reminders } from '@/native/reminders'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
@@ -99,14 +99,31 @@ watch([() => form.currency, () => form.foreignAmount, () => form.rate], () => {
|
|||||||
if (isForeign.value) form.amount = convertedKrw.value
|
if (isForeign.value) form.amount = convertedKrw.value
|
||||||
})
|
})
|
||||||
const fxLoading = ref(false)
|
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() {
|
async function fetchRate() {
|
||||||
if (!isForeign.value || fxLoading.value) return
|
if (!isForeign.value || fxLoading.value) return
|
||||||
fxLoading.value = true
|
fxLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await accountApi.fxRate(form.currency, 'KRW')
|
const res = await accountApi.fxRate(form.currency, 'KRW')
|
||||||
// 환율은 소수점 4자리로 통일(입력폼 step·저장 정밀도 일치)
|
// 환율은 소수점 4자리로 통일(입력폼 step·저장 정밀도 일치)
|
||||||
if (res?.rate != null) form.rate = Math.round(Number(res.rate) * 10000) / 10000
|
if (res?.rate != null) {
|
||||||
else formInfo.value = '환율을 가져오지 못했어요. 직접 입력해 주세요.'
|
form.rate = Math.round(Number(res.rate) * 10000) / 10000
|
||||||
|
rateUpdatedAt.value = res.updatedAt ? new Date(res.updatedAt).getTime() : Date.now()
|
||||||
|
} else {
|
||||||
|
formInfo.value = '환율을 가져오지 못했어요. 직접 입력해 주세요.'
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
formInfo.value = '환율 조회에 실패했어요. 직접 입력해 주세요.'
|
formInfo.value = '환율 조회에 실패했어요. 직접 입력해 주세요.'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -120,6 +137,7 @@ function onCurrencyChange() {
|
|||||||
} else {
|
} else {
|
||||||
form.foreignAmount = null
|
form.foreignAmount = null
|
||||||
form.rate = null
|
form.rate = null
|
||||||
|
rateUpdatedAt.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 계좌/카드 (repayTargetIsCard·repayTargetLoanWallet computed가 참조하므로 먼저 선언)
|
// 계좌/카드 (repayTargetIsCard·repayTargetLoanWallet computed가 참조하므로 먼저 선언)
|
||||||
@@ -500,6 +518,7 @@ function openCreate() {
|
|||||||
editId.value = null
|
editId.value = null
|
||||||
editingPending.value = false
|
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 })
|
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 = []
|
selectedTagIds.value = []
|
||||||
formError.value = null
|
formError.value = null
|
||||||
formInfo.value = ''
|
formInfo.value = ''
|
||||||
@@ -540,6 +559,7 @@ function openEdit(e) {
|
|||||||
const nameToId = {}
|
const nameToId = {}
|
||||||
tagOptions.value.forEach((t) => (nameToId[t.name] = t.id))
|
tagOptions.value.forEach((t) => (nameToId[t.name] = t.id))
|
||||||
selectedTagIds.value = (e.tags || []).map((n) => nameToId[n]).filter(Boolean)
|
selectedTagIds.value = (e.tags || []).map((n) => nameToId[n]).filter(Boolean)
|
||||||
|
rateUpdatedAt.value = null
|
||||||
pasteDetected.value = null
|
pasteDetected.value = null
|
||||||
pasteModalOpen.value = false
|
pasteModalOpen.value = false
|
||||||
pasteText.value = ''
|
pasteText.value = ''
|
||||||
@@ -1212,6 +1232,7 @@ onMounted(async () => {
|
|||||||
<input v-model.number="form.rate" type="number" min="0" step="0.0001" placeholder="원" :disabled="submitting" />
|
<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>
|
<button type="button" class="fx-auto" :disabled="submitting || fxLoading" @click="fetchRate">{{ fxLoading ? '…' : '↻ 자동' }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-if="rateAgeLabel" class="fx-age">{{ rateAgeLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="fx-krw">= ₩{{ (convertedKrw || 0).toLocaleString('ko-KR') }}</span>
|
<span class="fx-krw">= ₩{{ (convertedKrw || 0).toLocaleString('ko-KR') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -2130,6 +2151,11 @@ button.primary {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding-bottom: 0.45rem;
|
padding-bottom: 0.45rem;
|
||||||
}
|
}
|
||||||
|
.fx-age {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--color-text-soft, #999);
|
||||||
|
margin-top: 0.18rem;
|
||||||
|
}
|
||||||
.row-wallet {
|
.row-wallet {
|
||||||
margin-right: 0.35rem;
|
margin-right: 0.35rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user