- AppModal 제거, 목록/폼 v-if·v-else 전환(헤더·하단바 유지 + 뒤로가기) - ?budget 라우트 바인딩으로 뒤로가기 시 닫힘 - 분류 선택은 트리거 → 바텀시트(CategoryPicker) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
import { useDialog } from '@/composables/dialog'
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
import AppModal from '@/components/ui/AppModal.vue'
|
|
||||||
import CategoryPicker from '@/components/ui/CategoryPicker.vue'
|
import CategoryPicker from '@/components/ui/CategoryPicker.vue'
|
||||||
|
import BottomSheet from '@/components/ui/BottomSheet.vue'
|
||||||
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const year = ref(now.getFullYear())
|
const year = ref(now.getFullYear())
|
||||||
@@ -17,7 +20,18 @@ const budgetsRaw = ref([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
|
// 폼 — 라우트(?budget) 바인딩으로 뒤로가기 시 닫힘(페이지 전환처럼)
|
||||||
const formOpen = ref(false)
|
const formOpen = ref(false)
|
||||||
|
function openForm() {
|
||||||
|
formOpen.value = true
|
||||||
|
if (route.query.budget == null) router.push({ query: { ...route.query, budget: '1' } })
|
||||||
|
}
|
||||||
|
function closeForm() {
|
||||||
|
formOpen.value = false
|
||||||
|
if (route.query.budget != null) router.back()
|
||||||
|
}
|
||||||
|
watch(() => route.query.budget, (v) => { if (v == null) formOpen.value = false })
|
||||||
|
|
||||||
const editId = ref(null)
|
const editId = ref(null)
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
category: '',
|
category: '',
|
||||||
@@ -25,6 +39,9 @@ const form = reactive({
|
|||||||
})
|
})
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const formError = ref(null)
|
const formError = ref(null)
|
||||||
|
// 분류 선택 바텀시트 — 최종 분류 정해지면 닫힘
|
||||||
|
const catSheet = ref(false)
|
||||||
|
watch(() => form.category, (v) => { if (v) catSheet.value = false })
|
||||||
|
|
||||||
// 분류(지출) 드롭다운
|
// 분류(지출) 드롭다운
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
@@ -152,7 +169,7 @@ function openCreate() {
|
|||||||
editId.value = null
|
editId.value = null
|
||||||
Object.assign(form, { category: '', monthly: null })
|
Object.assign(form, { category: '', monthly: null })
|
||||||
formError.value = null
|
formError.value = null
|
||||||
formOpen.value = true
|
openForm()
|
||||||
}
|
}
|
||||||
function openEdit(s) {
|
function openEdit(s) {
|
||||||
const b = budgetsRaw.value.find((x) => x.id === s.id)
|
const b = budgetsRaw.value.find((x) => x.id === s.id)
|
||||||
@@ -160,7 +177,7 @@ function openEdit(s) {
|
|||||||
editId.value = b.id
|
editId.value = b.id
|
||||||
Object.assign(form, { category: b.category, monthly: b.monthly })
|
Object.assign(form, { category: b.category, monthly: b.monthly })
|
||||||
formError.value = null
|
formError.value = null
|
||||||
formOpen.value = true
|
openForm()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -185,7 +202,7 @@ async function submit() {
|
|||||||
try {
|
try {
|
||||||
if (editId.value) await accountApi.updateBudget(editId.value, payload)
|
if (editId.value) await accountApi.updateBudget(editId.value, payload)
|
||||||
else await accountApi.createBudget(payload, { year: year.value, month: month.value })
|
else await accountApi.createBudget(payload, { year: year.value, month: month.value })
|
||||||
formOpen.value = false
|
closeForm()
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
formError.value = e.response?.data?.message || '저장에 실패했습니다.'
|
formError.value = e.response?.data?.message || '저장에 실패했습니다.'
|
||||||
@@ -213,6 +230,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="budget">
|
<section class="budget">
|
||||||
|
<template v-if="!formOpen">
|
||||||
<div class="month-nav">
|
<div class="month-nav">
|
||||||
<span class="mn-spacer"></span>
|
<span class="mn-spacer"></span>
|
||||||
<IconBtn icon="chevronLeft" title="이전 달" size="sm" @click="prevMonth" />
|
<IconBtn icon="chevronLeft" title="이전 달" size="sm" @click="prevMonth" />
|
||||||
@@ -279,12 +297,26 @@ onMounted(() => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else-if="!loading" class="msg">설정된 예산이 없습니다. "예산 추가"로 시작하세요.</p>
|
<p v-else-if="!loading" class="msg">설정된 예산이 없습니다. "예산 추가"로 시작하세요.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 추가/수정 모달 (월별) -->
|
<!-- 예산 추가/수정 (인라인 페이지 — 헤더·하단바 유지, 뒤로가기로 닫힘) -->
|
||||||
<AppModal v-model="formOpen" :title="`예산 ${editId ? '수정' : '추가'}`">
|
<div v-else class="budget-page">
|
||||||
|
<header class="ep-head">
|
||||||
|
<IconBtn icon="back" title="뒤로" @click="closeForm()" />
|
||||||
|
<span class="ep-title">예산 {{ editId ? '수정' : '추가' }}</span>
|
||||||
|
</header>
|
||||||
<form class="budget-form" @submit.prevent="submit">
|
<form class="budget-form" @submit.prevent="submit">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="field-label">카테고리</span>
|
<span class="field-label">카테고리</span>
|
||||||
|
<button
|
||||||
|
type="button" class="sheet-trigger" :class="{ empty: !form.category }"
|
||||||
|
:disabled="submitting" @click="catSheet = true"
|
||||||
|
>
|
||||||
|
<span class="st-label">{{ form.category || '분류를 선택하세요' }}</span>
|
||||||
|
<span class="st-caret">▾</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<BottomSheet v-model="catSheet" title="분류 선택">
|
||||||
<CategoryPicker
|
<CategoryPicker
|
||||||
v-model="form.category"
|
v-model="form.category"
|
||||||
type="EXPENSE"
|
type="EXPENSE"
|
||||||
@@ -292,7 +324,7 @@ onMounted(() => {
|
|||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
@category-added="loadCategories"
|
@category-added="loadCategories"
|
||||||
/>
|
/>
|
||||||
</div>
|
</BottomSheet>
|
||||||
|
|
||||||
<label>월 예산<input v-model.number="form.monthly" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
<label>월 예산<input v-model.number="form.monthly" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||||
<p class="note">설정한 월 예산 기준으로 이번 달 지출과 비교됩니다.</p>
|
<p class="note">설정한 월 예산 기준으로 이번 달 지출과 비교됩니다.</p>
|
||||||
@@ -300,11 +332,11 @@ onMounted(() => {
|
|||||||
<p v-if="formError" class="msg error">{{ formError }}</p>
|
<p v-if="formError" class="msg error">{{ formError }}</p>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<IconBtn icon="close" title="취소" @click="formOpen = false" />
|
<IconBtn icon="close" title="취소" @click="closeForm()" />
|
||||||
<IconBtn icon="save" :title="editId ? '수정' : '등록'" variant="primary" type="submit" :disabled="submitting" />
|
<IconBtn icon="save" :title="editId ? '수정' : '등록'" variant="primary" type="submit" :disabled="submitting" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppModal>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -313,6 +345,52 @@ onMounted(() => {
|
|||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
.budget-page .budget-form {
|
||||||
|
max-width: 460px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.ep-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
.ep-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.sheet-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.sheet-trigger:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.sheet-trigger.empty .st-label {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.st-label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.st-caret {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.head {
|
.head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user