- WalletFormView.vue 신규: /account/wallets/new · /:id/edit 라우트 - 생성 시 '계좌 종류' 선택 UI 복원(탭 제거 후 빠졌던 부분) - AccountWalletView: 모달·폼 로직 제거, +/수정 버튼은 화면 이동 - 상세 라우트는 :id(\d+)로 제한(new와 충돌 방지) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+13
-1
@@ -140,7 +140,19 @@ const router = createRouter({
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/account/wallets/:id',
|
||||
path: '/account/wallets/new',
|
||||
name: 'account-wallet-new',
|
||||
component: () => import('../views/account/WalletFormView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/account/wallets/:id(\\d+)/edit',
|
||||
name: 'account-wallet-edit',
|
||||
component: () => import('../views/account/WalletFormView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/account/wallets/:id(\\d+)',
|
||||
name: 'account-wallet-detail',
|
||||
component: () => import('../views/account/AccountWalletDetailView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Sortable from 'sortablejs'
|
||||
import { accountApi } from '@/api/accountApi'
|
||||
import { useDialog } from '@/composables/dialog'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
import AppModal from '@/components/ui/AppModal.vue'
|
||||
|
||||
const dialog = useDialog()
|
||||
const auth = useAuthStore()
|
||||
@@ -20,17 +19,6 @@ const TABS = [
|
||||
{ key: 'MINUS', label: '마이너스통장' },
|
||||
{ key: 'INVEST', label: '투자' },
|
||||
]
|
||||
const CARD_TYPES = [
|
||||
{ value: 'CREDIT', label: '신용카드' },
|
||||
{ value: 'CHECK', label: '체크카드' },
|
||||
]
|
||||
const LOAN_METHODS = [
|
||||
{ value: 'EQUAL_PAYMENT', label: '원리금균등' },
|
||||
{ value: 'EQUAL_PRINCIPAL', label: '원금균등' },
|
||||
{ value: 'BULLET', label: '만기일시' },
|
||||
{ value: '', label: '선택안함' },
|
||||
]
|
||||
|
||||
const wallets = ref([])
|
||||
const networth = ref({ totalAssets: 0, totalLiabilities: 0, netWorth: 0 })
|
||||
const loading = ref(false)
|
||||
@@ -44,35 +32,17 @@ const groups = computed(() =>
|
||||
),
|
||||
)
|
||||
|
||||
const formOpen = ref(false)
|
||||
const editId = ref(null)
|
||||
// openingBalance 는 화면 입력값(부채는 갚을 금액의 절대값). 서버 저장 시 부호 변환.
|
||||
const form = reactive({
|
||||
type: 'BANK',
|
||||
name: '',
|
||||
issuer: '',
|
||||
accountNumber: '',
|
||||
cardType: 'CREDIT',
|
||||
openingBalance: 0,
|
||||
openingDate: '',
|
||||
currentValue: null,
|
||||
// 대출(LOAN) 전용
|
||||
loanAmount: null,
|
||||
loanRate: null,
|
||||
loanMethod: '',
|
||||
loanMonths: null,
|
||||
loanStart: '',
|
||||
loanPayment: null,
|
||||
})
|
||||
const submitting = ref(false)
|
||||
const formError = ref(null)
|
||||
|
||||
const isLiability = (t) => t === 'CARD' || t === 'LOAN' || t === 'MINUS'
|
||||
|
||||
// 계좌 클릭 → 별도 상세 화면(계좌별 내역)
|
||||
function goDetail(w) {
|
||||
router.push(`/account/wallets/${w.id}`)
|
||||
}
|
||||
// 추가/수정은 별도 화면으로(모달 → 화면전환)
|
||||
function openCreate() {
|
||||
router.push('/account/wallets/new')
|
||||
}
|
||||
function openEdit(w) {
|
||||
router.push(`/account/wallets/${w.id}/edit`)
|
||||
}
|
||||
|
||||
// ===== 드래그앤드랍 순서 변경 (종류별 그룹마다 SortableJS, 터치 지원) =====
|
||||
let sortables = []
|
||||
@@ -139,22 +109,6 @@ function toggleReveal(id) {
|
||||
s.has(id) ? s.delete(id) : s.add(id)
|
||||
revealedAccts.value = s
|
||||
}
|
||||
function issuerLabel(t) {
|
||||
return t === 'BANK' || t === 'MINUS' ? '은행명' : t === 'CARD' ? '카드사' : t === 'INVEST' ? '증권사' : '대출기관'
|
||||
}
|
||||
function openingLabel(t) {
|
||||
if (t === 'BANK' || t === 'CASH') return '초기 잔액'
|
||||
if (t === 'CARD') return '초기 미결제 금액'
|
||||
if (t === 'INVEST') return '투자금(투입원금)'
|
||||
if (t === 'MINUS') return '기록 시작 시 사용 잔액'
|
||||
return '기록 시작 시 잔액'
|
||||
}
|
||||
// 투자 수익률(%) — 투입원금 대비 평가손익
|
||||
function returnPct(w) {
|
||||
if (w.type !== 'INVEST' || !w.investedAmount || w.valuationGain == null) return null
|
||||
return Math.round((w.valuationGain / w.investedAmount) * 1000) / 10
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
@@ -172,91 +126,6 @@ async function load() {
|
||||
initSortables()
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editId.value = null
|
||||
Object.assign(form, {
|
||||
type: 'BANK',
|
||||
name: '',
|
||||
issuer: '',
|
||||
accountNumber: '',
|
||||
cardType: 'CREDIT',
|
||||
openingBalance: 0,
|
||||
openingDate: '',
|
||||
currentValue: null,
|
||||
loanAmount: null,
|
||||
loanRate: null,
|
||||
loanMethod: '',
|
||||
loanMonths: null,
|
||||
loanStart: '',
|
||||
loanPayment: null,
|
||||
})
|
||||
formError.value = null
|
||||
formOpen.value = true
|
||||
}
|
||||
function openEdit(w) {
|
||||
editId.value = w.id
|
||||
Object.assign(form, {
|
||||
type: w.type,
|
||||
name: w.name,
|
||||
issuer: w.issuer || '',
|
||||
accountNumber: w.accountNumber || '',
|
||||
cardType: w.cardType || 'CREDIT',
|
||||
// 부채는 음수 저장값 → 화면엔 절대값(갚을 금액)으로
|
||||
openingBalance: isLiability(w.type) ? -(w.openingBalance || 0) : w.openingBalance || 0,
|
||||
openingDate: w.openingDate || '',
|
||||
currentValue: w.currentValue ?? null,
|
||||
loanAmount: w.loanAmount ?? null,
|
||||
loanRate: w.loanRate ?? null,
|
||||
loanMethod: w.loanMethod || '',
|
||||
loanMonths: w.loanMonths ?? null,
|
||||
loanStart: w.loanStart || '',
|
||||
loanPayment: w.loanPayment ?? null,
|
||||
})
|
||||
formError.value = null
|
||||
formOpen.value = true
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
formError.value = null
|
||||
if (!form.name.trim()) {
|
||||
formError.value = '이름을 입력하세요.'
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
const magnitude = Number(form.openingBalance) || 0
|
||||
const isLoan = form.type === 'LOAN'
|
||||
const payload = {
|
||||
type: form.type,
|
||||
name: form.name.trim(),
|
||||
issuer: form.issuer || null,
|
||||
accountNumber: form.type === 'BANK' ? form.accountNumber || null : null,
|
||||
cardType: form.type === 'CARD' ? form.cardType : null,
|
||||
// 부채는 음수로 저장 (갚을 금액)
|
||||
openingBalance: isLiability(form.type) ? -magnitude : magnitude,
|
||||
openingDate: form.openingDate || null,
|
||||
currentValue:
|
||||
form.type === 'INVEST' && form.currentValue !== '' && form.currentValue != null
|
||||
? Number(form.currentValue)
|
||||
: null,
|
||||
loanAmount: isLoan && form.loanAmount ? Number(form.loanAmount) : null,
|
||||
loanRate: isLoan && form.loanRate !== null && form.loanRate !== '' ? Number(form.loanRate) : null,
|
||||
loanMethod: isLoan && form.loanMethod ? form.loanMethod : null,
|
||||
loanMonths: isLoan && form.loanMonths ? Number(form.loanMonths) : null,
|
||||
loanStart: isLoan && form.loanStart ? form.loanStart : null,
|
||||
loanPayment: isLoan && form.loanPayment ? Number(form.loanPayment) : null,
|
||||
}
|
||||
try {
|
||||
if (editId.value) await accountApi.updateWallet(editId.value, payload)
|
||||
else await accountApi.createWallet(payload)
|
||||
formOpen.value = false
|
||||
await load()
|
||||
} catch (e) {
|
||||
formError.value = e.response?.data?.message || '저장에 실패했습니다.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(w) {
|
||||
if (!(await dialog.confirm(`'${w.name}' 을(를) 삭제할까요?`, { title: '삭제', danger: true }))) return
|
||||
try {
|
||||
@@ -362,78 +231,6 @@ onBeforeUnmount(destroySortables)
|
||||
<button type="button" class="empty-cta" @click="openCreate">+ 계좌 추가</button>
|
||||
</div>
|
||||
|
||||
<!-- 추가/수정 모달 -->
|
||||
<AppModal v-model="formOpen" :title="`${TABS.find((t) => t.key === form.type)?.label} ${editId ? '수정' : '추가'}`">
|
||||
<form class="wallet-form" @submit.prevent="submit">
|
||||
<label>이름(별칭)<input v-model="form.name" type="text" placeholder="예: 월급통장 / 메인카드 / 신용대출" :disabled="submitting" /></label>
|
||||
<label v-if="form.type !== 'CASH'">{{ issuerLabel(form.type) }}<input v-model="form.issuer" type="text" :disabled="submitting" /></label>
|
||||
<label v-if="form.type === 'BANK' || form.type === 'MINUS'">계좌번호<input v-model="form.accountNumber" type="text" placeholder="(선택)" :disabled="submitting" /></label>
|
||||
<div v-if="form.type === 'CARD'" class="field">
|
||||
<span class="field-label">카드 종류</span>
|
||||
<div class="chip-group">
|
||||
<button
|
||||
v-for="c in CARD_TYPES" :key="c.value"
|
||||
type="button" class="chip" :class="{ active: form.cardType === c.value }"
|
||||
:disabled="submitting" @click="form.cardType = c.value"
|
||||
>{{ c.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<label>{{ openingLabel(form.type) }}<input v-model.number="form.openingBalance" type="number" min="0" :disabled="submitting" /></label>
|
||||
<template v-if="form.type === 'INVEST'">
|
||||
<label>현재 평가액
|
||||
<input v-model.number="form.currentValue" type="number" min="0" placeholder="예: 현재 계좌 평가금액" :disabled="submitting" />
|
||||
</label>
|
||||
<p class="form-hint">
|
||||
<b>투자금</b>(투입원금)과 <b>현재 평가액</b>만 입력하면 손익이 자동 계산됩니다. (퇴직연금·연금·증권계좌 공통)<br />
|
||||
평가액은 위 <b>수정</b>에서 주기적으로 갱신하세요.
|
||||
</p>
|
||||
</template>
|
||||
<label>기준일<input v-model="form.openingDate" type="date" :disabled="submitting" /></label>
|
||||
|
||||
<!-- 대출 전용 -->
|
||||
<template v-if="form.type === 'LOAN'">
|
||||
<label>대출 실행 금액(원금)
|
||||
<input v-model.number="form.loanAmount" type="number" min="0"
|
||||
placeholder="처음 빌린 금액 (예: 10000000)" :disabled="submitting" />
|
||||
</label>
|
||||
<label>연이자율(%)
|
||||
<input v-model.number="form.loanRate" type="number" min="0" max="100" step="0.001"
|
||||
placeholder="예: 5.253" :disabled="submitting" />
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">상환방식</span>
|
||||
<div class="chip-group">
|
||||
<button
|
||||
v-for="m in LOAN_METHODS" :key="m.value"
|
||||
type="button" class="chip" :class="{ active: form.loanMethod === m.value }"
|
||||
:disabled="submitting" @click="form.loanMethod = m.value"
|
||||
>{{ m.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<label>대출기간(개월)
|
||||
<input v-model.number="form.loanMonths" type="number" min="1"
|
||||
placeholder="예: 120 (10년)" :disabled="submitting" />
|
||||
</label>
|
||||
<label>대출시작일
|
||||
<input v-model="form.loanStart" type="date" :disabled="submitting" />
|
||||
</label>
|
||||
<label v-if="form.loanMethod === 'EQUAL_PAYMENT' || form.loanMethod === ''">월 상환금(선택)
|
||||
<input v-model.number="form.loanPayment" type="number" min="0"
|
||||
placeholder="은행 조회 고정 월납입금 (예: 697868)" :disabled="submitting" />
|
||||
</label>
|
||||
<p v-if="form.loanMethod === 'EQUAL_PAYMENT' || form.loanMethod === ''" class="form-hint">
|
||||
원리금균등은 <b>월 상환금</b>을 입력하면 상환표의 원금·이자가 은행 조회액과 정확히 일치합니다. (미입력 시 잔액·개월수로 추정)
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<p v-if="formError" class="msg error">{{ formError }}</p>
|
||||
|
||||
<div class="buttons">
|
||||
<IconBtn icon="close" title="취소" @click="formOpen = false" />
|
||||
<IconBtn icon="save" :title="editId ? '수정' : '등록'" variant="primary" type="submit" :disabled="submitting" />
|
||||
</div>
|
||||
</form>
|
||||
</AppModal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { accountApi } from '@/api/accountApi'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const editId = route.params.id ? Number(route.params.id) : null
|
||||
const isEdit = editId != null
|
||||
|
||||
const TYPES = [
|
||||
{ key: 'BANK', label: '은행계좌' },
|
||||
{ key: 'CASH', label: '현금' },
|
||||
{ key: 'CARD', label: '신용/체크카드' },
|
||||
{ key: 'LOAN', label: '대출' },
|
||||
{ key: 'MINUS', label: '마이너스통장' },
|
||||
{ key: 'INVEST', label: '투자' },
|
||||
]
|
||||
const CARD_TYPES = [
|
||||
{ value: 'CREDIT', label: '신용카드' },
|
||||
{ value: 'CHECK', label: '체크카드' },
|
||||
]
|
||||
const LOAN_METHODS = [
|
||||
{ value: 'EQUAL_PAYMENT', label: '원리금균등' },
|
||||
{ value: 'EQUAL_PRINCIPAL', label: '원금균등' },
|
||||
{ value: 'BULLET', label: '만기일시' },
|
||||
{ value: '', label: '선택안함' },
|
||||
]
|
||||
|
||||
const isLiability = (t) => t === 'CARD' || t === 'LOAN' || t === 'MINUS'
|
||||
|
||||
const form = reactive({
|
||||
type: 'BANK',
|
||||
name: '',
|
||||
issuer: '',
|
||||
accountNumber: '',
|
||||
cardType: 'CREDIT',
|
||||
openingBalance: 0,
|
||||
openingDate: '',
|
||||
currentValue: null,
|
||||
loanAmount: null,
|
||||
loanRate: null,
|
||||
loanMethod: '',
|
||||
loanMonths: null,
|
||||
loanStart: '',
|
||||
loanPayment: null,
|
||||
})
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formError = ref(null)
|
||||
|
||||
const typeLabel = (t) => TYPES.find((x) => x.key === t)?.label || ''
|
||||
function issuerLabel(t) {
|
||||
return t === 'BANK' || t === 'MINUS' ? '은행명' : t === 'CARD' ? '카드사' : t === 'INVEST' ? '증권사' : '대출기관'
|
||||
}
|
||||
function openingLabel(t) {
|
||||
if (t === 'BANK' || t === 'CASH') return '초기 잔액'
|
||||
if (t === 'CARD') return '초기 미결제 금액'
|
||||
if (t === 'INVEST') return '투자금(투입원금)'
|
||||
if (t === 'MINUS') return '기록 시작 시 사용 잔액'
|
||||
return '기록 시작 시 잔액'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!isEdit) return
|
||||
loading.value = true
|
||||
try {
|
||||
const ws = await accountApi.wallets()
|
||||
const w = ws.find((x) => x.id === editId)
|
||||
if (!w) {
|
||||
formError.value = '계좌를 찾을 수 없습니다.'
|
||||
return
|
||||
}
|
||||
Object.assign(form, {
|
||||
type: w.type,
|
||||
name: w.name,
|
||||
issuer: w.issuer || '',
|
||||
accountNumber: w.accountNumber || '',
|
||||
cardType: w.cardType || 'CREDIT',
|
||||
// 부채는 음수 저장값 → 화면엔 절대값(갚을 금액)으로
|
||||
openingBalance: isLiability(w.type) ? -(w.openingBalance || 0) : w.openingBalance || 0,
|
||||
openingDate: w.openingDate || '',
|
||||
currentValue: w.currentValue ?? null,
|
||||
loanAmount: w.loanAmount ?? null,
|
||||
loanRate: w.loanRate ?? null,
|
||||
loanMethod: w.loanMethod || '',
|
||||
loanMonths: w.loanMonths ?? null,
|
||||
loanStart: w.loanStart || '',
|
||||
loanPayment: w.loanPayment ?? null,
|
||||
})
|
||||
} catch (e) {
|
||||
formError.value = e.response?.data?.message || '불러오지 못했습니다.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(load)
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) router.back()
|
||||
else router.replace('/account/wallets')
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
formError.value = null
|
||||
if (!form.name.trim()) {
|
||||
formError.value = '이름을 입력하세요.'
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
const magnitude = Number(form.openingBalance) || 0
|
||||
const isLoan = form.type === 'LOAN'
|
||||
const payload = {
|
||||
type: form.type,
|
||||
name: form.name.trim(),
|
||||
issuer: form.issuer || null,
|
||||
accountNumber: form.type === 'BANK' || form.type === 'MINUS' ? form.accountNumber || null : null,
|
||||
cardType: form.type === 'CARD' ? form.cardType : null,
|
||||
// 부채는 음수로 저장 (갚을 금액)
|
||||
openingBalance: isLiability(form.type) ? -magnitude : magnitude,
|
||||
openingDate: form.openingDate || null,
|
||||
currentValue:
|
||||
form.type === 'INVEST' && form.currentValue !== '' && form.currentValue != null
|
||||
? Number(form.currentValue)
|
||||
: null,
|
||||
loanAmount: isLoan && form.loanAmount ? Number(form.loanAmount) : null,
|
||||
loanRate: isLoan && form.loanRate !== null && form.loanRate !== '' ? Number(form.loanRate) : null,
|
||||
loanMethod: isLoan && form.loanMethod ? form.loanMethod : null,
|
||||
loanMonths: isLoan && form.loanMonths ? Number(form.loanMonths) : null,
|
||||
loanStart: isLoan && form.loanStart ? form.loanStart : null,
|
||||
loanPayment: isLoan && form.loanPayment ? Number(form.loanPayment) : null,
|
||||
}
|
||||
try {
|
||||
if (isEdit) await accountApi.updateWallet(editId, payload)
|
||||
else await accountApi.createWallet(payload)
|
||||
router.replace('/account/wallets')
|
||||
} catch (e) {
|
||||
formError.value = e.response?.data?.message || '저장에 실패했습니다.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="wform">
|
||||
<header class="wf-head">
|
||||
<IconBtn icon="back" title="뒤로" @click="goBack" />
|
||||
<span class="wf-title">{{ isEdit ? `${typeLabel(form.type)} 수정` : '계좌 추가' }}</span>
|
||||
</header>
|
||||
|
||||
<p v-if="loading" class="msg">불러오는 중...</p>
|
||||
|
||||
<form v-else class="wallet-form" @submit.prevent="submit">
|
||||
<!-- 종류 선택(생성 시에만) -->
|
||||
<div v-if="!isEdit" class="field">
|
||||
<span class="field-label">계좌 종류</span>
|
||||
<div class="chip-group">
|
||||
<button
|
||||
v-for="t in TYPES" :key="t.key"
|
||||
type="button" class="chip" :class="{ active: form.type === t.key }"
|
||||
:disabled="submitting" @click="form.type = t.key"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>이름(별칭)<input v-model="form.name" type="text" placeholder="예: 월급통장 / 메인카드 / 신용대출" :disabled="submitting" /></label>
|
||||
<label v-if="form.type !== 'CASH'">{{ issuerLabel(form.type) }}<input v-model="form.issuer" type="text" :disabled="submitting" /></label>
|
||||
<label v-if="form.type === 'BANK' || form.type === 'MINUS'">계좌번호<input v-model="form.accountNumber" type="text" placeholder="(선택)" :disabled="submitting" /></label>
|
||||
<div v-if="form.type === 'CARD'" class="field">
|
||||
<span class="field-label">카드 종류</span>
|
||||
<div class="chip-group">
|
||||
<button
|
||||
v-for="c in CARD_TYPES" :key="c.value"
|
||||
type="button" class="chip" :class="{ active: form.cardType === c.value }"
|
||||
:disabled="submitting" @click="form.cardType = c.value"
|
||||
>{{ c.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<label>{{ openingLabel(form.type) }}<input v-model.number="form.openingBalance" type="number" min="0" :disabled="submitting" /></label>
|
||||
<template v-if="form.type === 'INVEST'">
|
||||
<label>현재 평가액
|
||||
<input v-model.number="form.currentValue" type="number" min="0" placeholder="예: 현재 계좌 평가금액" :disabled="submitting" />
|
||||
</label>
|
||||
<p class="form-hint">
|
||||
<b>투자금</b>(투입원금)과 <b>현재 평가액</b>만 입력하면 손익이 자동 계산됩니다. (퇴직연금·연금·증권계좌 공통)<br />
|
||||
평가액은 <b>수정</b>에서 주기적으로 갱신하세요.
|
||||
</p>
|
||||
</template>
|
||||
<label>기준일<input v-model="form.openingDate" type="date" :disabled="submitting" /></label>
|
||||
|
||||
<!-- 대출 전용 -->
|
||||
<template v-if="form.type === 'LOAN'">
|
||||
<label>대출 실행 금액(원금)
|
||||
<input v-model.number="form.loanAmount" type="number" min="0"
|
||||
placeholder="처음 빌린 금액 (예: 10000000)" :disabled="submitting" />
|
||||
</label>
|
||||
<label>연이자율(%)
|
||||
<input v-model.number="form.loanRate" type="number" min="0" max="100" step="0.001"
|
||||
placeholder="예: 5.253" :disabled="submitting" />
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">상환방식</span>
|
||||
<div class="chip-group">
|
||||
<button
|
||||
v-for="m in LOAN_METHODS" :key="m.value"
|
||||
type="button" class="chip" :class="{ active: form.loanMethod === m.value }"
|
||||
:disabled="submitting" @click="form.loanMethod = m.value"
|
||||
>{{ m.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<label>대출기간(개월)
|
||||
<input v-model.number="form.loanMonths" type="number" min="1"
|
||||
placeholder="예: 120 (10년)" :disabled="submitting" />
|
||||
</label>
|
||||
<label>대출시작일
|
||||
<input v-model="form.loanStart" type="date" :disabled="submitting" />
|
||||
</label>
|
||||
<label v-if="form.loanMethod === 'EQUAL_PAYMENT' || form.loanMethod === ''">월 상환금(선택)
|
||||
<input v-model.number="form.loanPayment" type="number" min="0"
|
||||
placeholder="은행 조회 고정 월납입금 (예: 697868)" :disabled="submitting" />
|
||||
</label>
|
||||
<p v-if="form.loanMethod === 'EQUAL_PAYMENT' || form.loanMethod === ''" class="form-hint">
|
||||
원리금균등은 <b>월 상환금</b>을 입력하면 상환표의 원금·이자가 은행 조회액과 정확히 일치합니다. (미입력 시 잔액·개월수로 추정)
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<p v-if="formError" class="msg error">{{ formError }}</p>
|
||||
|
||||
<div class="buttons">
|
||||
<IconBtn icon="close" title="취소" @click="goBack" />
|
||||
<IconBtn icon="save" :title="isEdit ? '수정' : '등록'" variant="primary" type="submit" :disabled="submitting" />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wform {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.wf-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.wf-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.msg {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.msg.error {
|
||||
color: #c0392b;
|
||||
}
|
||||
.wallet-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.wallet-form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.wallet-form input {
|
||||
padding: 0.5rem 0.7rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background: var(--color-background-soft);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.field-label {
|
||||
font-size: 0.82rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.chip-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.chip {
|
||||
padding: 0.28rem 0.75rem;
|
||||
border: 1.5px solid var(--color-border);
|
||||
border-radius: 100px;
|
||||
background: var(--color-background-soft);
|
||||
color: var(--color-text);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
line-height: 1.4;
|
||||
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
||||
}
|
||||
.chip:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.chip.active {
|
||||
border-color: hsla(160, 100%, 37%, 1);
|
||||
background: hsla(160, 100%, 37%, 1);
|
||||
color: #fff;
|
||||
}
|
||||
.form-hint {
|
||||
font-size: 0.76rem;
|
||||
opacity: 0.65;
|
||||
margin: -0.2rem 0 0.1rem;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user