feat: 홈 일별 캘린더·시세 갱신·게시판 카테고리·약관동의·계좌 마스킹

- 홈: 6월 예산 아래 일별 수입/지출 캘린더(마우스오버/탭 시 내역 목록)
- 투자: 시세 자동조회 버튼/진입 시 갱신, 보유종목 2줄 표기, 평가액 직접입력
- 게시판: 커뮤니티/짠테크 수다방/재테크 팁 분리, 사이드바 영역 구분선
- 회원가입: 이용약관·개인정보 수집 동의(필수 체크)
- 보안: 계좌번호 화면 마스킹(끝 4자리+눈 토글)
- 정기→고정지출, 내역/정기 라디오·sticky 저장, 태그 드래그 정렬
- fix: 모바일웹 새로고침 시 로그인 팝업(restore localStorage 우선)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-04 05:01:33 +09:00
parent 67aa635dd8
commit 4e6a89fd7a
17 changed files with 1022 additions and 136 deletions
+76 -24
View File
@@ -363,7 +363,7 @@ function todayStr() {
function openCreate() {
editId.value = null
editingPending.value = false
Object.assign(form, { entryDate: todayStr(), type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: '', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, installmentMonths: '' })
Object.assign(form, { entryDate: todayStr(), type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: 'BANK', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, installmentMonths: '' })
selectedTagIds.value = []
cancelAddCategory()
formError.value = null
@@ -378,7 +378,7 @@ function openEdit(e) {
category: e.category || '',
amount: e.amount,
memo: e.memo || '',
walletKind: walletKindOf(e.walletId),
walletKind: e.walletId ? walletKindOf(e.walletId) : (e.type === 'TRANSFER' ? '' : 'CASH'),
walletId: e.walletId || '',
toWalletKind: walletKindOf(e.toWalletId),
toWalletId: e.toWalletId || '',
@@ -653,21 +653,27 @@ onMounted(async () => {
<option v-if="!editId" value="REPAYMENT">상환/납부</option>
</select>
</label>
<!-- 계좌 종류 먼저 선택 해당 종류만 목록 -->
<label>계좌 종류
<select v-model="form.walletKind" :disabled="submitting" @change="onWalletKindChange">
<option value="">{{ form.type === 'INCOME' || form.type === 'EXPENSE' ? '(선택 안 함)' : '(선택)' }}</option>
<option v-for="k in WALLET_KINDS" :key="k.value" :value="k.value">{{ k.label }}</option>
</select>
</label>
<label v-if="form.walletKind">{{ form.type === 'INCOME' || form.type === 'EXPENSE' ? '계좌/카드' : '출금 계좌' }}
<select v-model="form.walletId" :disabled="submitting">
<option value="">(선택)</option>
<!-- 계좌 종류 먼저 선택(라디오) 해당 종류만 셀렉트 -->
<div class="field">
<div class="field-row">
<span class="field-label">계좌 종류</span>
<div class="wallet-radios">
<label v-for="k in WALLET_KINDS" :key="k.value" class="radio">
<input type="radio" :value="k.value" v-model="form.walletKind" :disabled="submitting" @change="onWalletKindChange" />
{{ k.label }}
</label>
<label v-if="form.type !== 'TRANSFER'" class="radio">
<input type="radio" value="CASH" v-model="form.walletKind" :disabled="submitting" @change="onWalletKindChange" /> 현금
</label>
</div>
</div>
<select v-if="form.walletKind && form.walletKind !== 'CASH'" v-model="form.walletId" :disabled="submitting">
<option value="">{{ form.type === 'INCOME' || form.type === 'EXPENSE' ? '계좌/카드 선택' : '출금 계좌 선택' }}</option>
<option v-for="w in walletsOfKind" :key="w.id" :value="w.id">
{{ w.name }}{{ w.issuer ? ` (${w.issuer})` : '' }}
</option>
</select>
</label>
</div>
<!-- 카드 지출: 할부 개월수 (일시불 또는 2~24개월) -->
<label v-if="showInstallment">할부
<select v-model="form.installmentMonths" :disabled="submitting">
@@ -677,20 +683,23 @@ onMounted(async () => {
<small v-if="installmentMonthly" class="install-hint"> {{ won(installmentMonthly) }}</small>
</label>
<template v-if="form.type === 'TRANSFER'">
<label>입금 종류
<select v-model="form.toWalletKind" :disabled="submitting" @change="onToWalletKindChange">
<option value="">(선택)</option>
<option v-for="k in WALLET_KINDS" :key="k.value" :value="k.value">{{ k.label }}</option>
</select>
</label>
<label v-if="form.toWalletKind">입금 계좌
<select v-model="form.toWalletId" :disabled="submitting">
<option value="">(선택)</option>
<div class="field">
<div class="field-row">
<span class="field-label">입금 종류</span>
<div class="wallet-radios">
<label v-for="k in WALLET_KINDS" :key="k.value" class="radio">
<input type="radio" :value="k.value" v-model="form.toWalletKind" :disabled="submitting" @change="onToWalletKindChange" />
{{ k.label }}
</label>
</div>
</div>
<select v-if="form.toWalletKind" v-model="form.toWalletId" :disabled="submitting">
<option value="">입금 계좌 선택</option>
<option v-for="w in toWalletsOfKind" :key="w.id" :value="w.id">
{{ w.name }}{{ w.issuer ? ` (${w.issuer})` : '' }}
</option>
</select>
</label>
</div>
</template>
<!-- 상환/납부: 대상(대출/카드) + 원금 + 이자 -->
@@ -1071,7 +1080,10 @@ button.primary {
position: relative;
width: 100%;
max-width: 360px;
padding: 1.75rem 1.5rem 1.5rem;
/* 폼이 길어도 모달 안에서 스크롤되도록 + 하단 소프트키/제스처바 안전영역 확보 */
max-height: calc(100vh - 2rem);
overflow-y: auto;
padding: 1.75rem 1.5rem calc(1.5rem + env(safe-area-inset-bottom));
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
@@ -1207,6 +1219,41 @@ button.primary {
background: var(--color-background-soft);
color: var(--color-text);
}
/* 계좌 종류 라디오 (라벨+라디오 한 줄, 선택 종류는 셀렉트로 아래) */
.field {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.field-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.field-label {
font-size: 0.82rem;
flex-shrink: 0;
}
.wallet-radios {
display: flex;
flex: 1;
min-width: 0;
flex-wrap: wrap;
gap: 0.3rem 0.5rem;
}
.wallet-radios .radio {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.15rem;
font-size: 0.82rem;
cursor: pointer;
}
.wallet-radios .radio input {
padding: 0;
width: auto;
margin: 0;
}
.cat-input {
display: flex;
gap: 0.4rem;
@@ -1287,6 +1334,11 @@ button.primary {
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
/* 폼이 길 때도 저장/취소 버튼이 항상 보이도록 하단 고정 */
position: sticky;
bottom: calc(-1.5rem - env(safe-area-inset-bottom));
padding: 0.6rem 0 0.2rem;
background: var(--color-background);
}
.fade-enter-active,
.fade-leave-active {