- AppModal.vue: 전체화면 슬라이드업 공통 모달 (Teleport+Transition) - CategoryPicker.vue: 대/소분류 아코디언 칩 공통 컴포넌트 admin 모드(일반: 행 단위+수정/삭제, 순서변경: flat+SortableJS) - AccountView: 계좌종류·구분 셀렉트→뱃지, 분류→CategoryPicker - RecurringView: 구분·주기 셀렉트→뱃지, 계좌/카드→뱃지, 분류→CategoryPicker - AccountWalletView: 카드종류·상환방식 셀렉트→뱃지 - BudgetView: 비고정/고정 라디오→뱃지, 카테고리→CategoryPicker - CategoryView: 리스트 UI → CategoryPicker 칩 그리드 관리 모드 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+322
-129
@@ -3,6 +3,8 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { accountApi } from '@/api/accountApi'
|
||||
import { useDialog } from '@/composables/dialog'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
import AppModal from '@/components/ui/AppModal.vue'
|
||||
import CategoryPicker from '@/components/ui/CategoryPicker.vue'
|
||||
|
||||
const dialog = useDialog()
|
||||
|
||||
@@ -39,26 +41,6 @@ async function loadCategories() {
|
||||
categories.value = []
|
||||
}
|
||||
}
|
||||
const categoryOptions = computed(() => {
|
||||
const names = categories.value.filter((c) => c.type === 'EXPENSE').map((c) => c.name)
|
||||
if (form.category && !names.includes(form.category)) names.unshift(form.category)
|
||||
return names
|
||||
})
|
||||
// 대분류로 그룹핑 (소분류=optgroup 옵션 / 자식없는 대분류=단독 옵션)
|
||||
const categoryGroups = computed(() => {
|
||||
const list = categories.value.filter((c) => c.type === 'EXPENSE')
|
||||
const groups = []
|
||||
if (form.category && !list.some((c) => c.name === form.category)) {
|
||||
groups.push({ key: '_cur', label: null, options: [form.category] })
|
||||
}
|
||||
for (const m of list.filter((c) => c.parentId == null)) {
|
||||
const children = list.filter((c) => c.parentId === m.id)
|
||||
if (children.length) groups.push({ key: 'g' + m.id, label: m.name, options: children.map((c) => c.name) })
|
||||
else groups.push({ key: 'i' + m.id, label: null, options: [m.name] })
|
||||
}
|
||||
return groups
|
||||
})
|
||||
|
||||
const periodLabel = computed(() => `${year.value}년 ${String(month.value).padStart(2, '0')}월`)
|
||||
|
||||
function won(n) {
|
||||
@@ -273,10 +255,97 @@ async function remove(s) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 1회성 예산 =====
|
||||
const onetimes = ref([])
|
||||
const onetimeFormOpen = ref(false)
|
||||
const onetimeEditId = ref(null)
|
||||
const onetimeForm = reactive({ title: '', category: '', amount: null, startDate: '', endDate: '' })
|
||||
const onetimeSubmitting = ref(false)
|
||||
const onetimeFormError = ref(null)
|
||||
|
||||
function todayStr() {
|
||||
const d = new Date()
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
async function loadOnetime() {
|
||||
try { onetimes.value = await accountApi.onetimeBudgets() }
|
||||
catch { onetimes.value = [] }
|
||||
}
|
||||
|
||||
function statusLabel(s) {
|
||||
return s === 'UPCOMING' ? '예정' : s === 'ACTIVE' ? '진행' : '종료'
|
||||
}
|
||||
function onetimeRatio(o) {
|
||||
if (!o.amount) return 0
|
||||
return Math.min(Math.round((o.spent / o.amount) * 100), 999)
|
||||
}
|
||||
function onetimeBarClass(o) {
|
||||
const r = o.amount ? o.spent / o.amount : 0
|
||||
return r >= 1 ? 'over' : r >= 0.8 ? 'warn' : 'ok'
|
||||
}
|
||||
|
||||
function openOnetimeCreate() {
|
||||
onetimeEditId.value = null
|
||||
Object.assign(onetimeForm, { title: '', category: '', amount: null, startDate: todayStr(), endDate: '' })
|
||||
onetimeFormError.value = null
|
||||
onetimeFormOpen.value = true
|
||||
}
|
||||
function openOnetimeEdit(o) {
|
||||
onetimeEditId.value = o.id
|
||||
Object.assign(onetimeForm, {
|
||||
title: o.title,
|
||||
category: o.category || '',
|
||||
amount: o.amount,
|
||||
startDate: o.startDate,
|
||||
endDate: o.endDate,
|
||||
})
|
||||
onetimeFormError.value = null
|
||||
onetimeFormOpen.value = true
|
||||
}
|
||||
|
||||
async function submitOnetime() {
|
||||
onetimeFormError.value = null
|
||||
if (!onetimeForm.title.trim()) { onetimeFormError.value = '제목을 입력하세요.'; return }
|
||||
if (!onetimeForm.amount || onetimeForm.amount <= 0) { onetimeFormError.value = '금액을 입력하세요.'; return }
|
||||
if (!onetimeForm.startDate) { onetimeFormError.value = '시작일을 입력하세요.'; return }
|
||||
if (!onetimeForm.endDate) { onetimeFormError.value = '종료일을 입력하세요.'; return }
|
||||
if (onetimeForm.startDate > onetimeForm.endDate) { onetimeFormError.value = '종료일이 시작일보다 빠를 수 없습니다.'; return }
|
||||
const payload = {
|
||||
title: onetimeForm.title.trim(),
|
||||
category: onetimeForm.category || null,
|
||||
amount: Number(onetimeForm.amount),
|
||||
startDate: onetimeForm.startDate,
|
||||
endDate: onetimeForm.endDate,
|
||||
}
|
||||
onetimeSubmitting.value = true
|
||||
try {
|
||||
if (onetimeEditId.value) await accountApi.updateOnetimeBudget(onetimeEditId.value, payload)
|
||||
else await accountApi.createOnetimeBudget(payload)
|
||||
onetimeFormOpen.value = false
|
||||
await loadOnetime()
|
||||
} catch (e) {
|
||||
onetimeFormError.value = e.response?.data?.message || '저장에 실패했습니다.'
|
||||
} finally {
|
||||
onetimeSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOnetime(o) {
|
||||
if (!(await dialog.confirm(`'${o.title}' 1회성 예산을 삭제할까요?`, { title: '삭제', danger: true }))) return
|
||||
try {
|
||||
await accountApi.removeOnetimeBudget(o.id)
|
||||
await loadOnetime()
|
||||
} catch (e) {
|
||||
alert(e.response?.data?.message || '삭제에 실패했습니다.')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load()
|
||||
loadCategories()
|
||||
loadIncome()
|
||||
loadOnetime()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -349,70 +418,126 @@ onMounted(() => {
|
||||
</ul>
|
||||
<p v-else-if="!loading" class="msg">설정된 예산이 없습니다. "예산 추가"로 시작하세요.</p>
|
||||
|
||||
<!-- 추가/수정 모달 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="formOpen" class="modal-backdrop" @click.self="formOpen = false">
|
||||
<div class="modal" role="dialog" aria-modal="true">
|
||||
<button class="close" type="button" @click="formOpen = false">×</button>
|
||||
<h2>예산 {{ editId ? '수정' : '추가' }}</h2>
|
||||
<!-- 1회성 예산 섹션 -->
|
||||
<div class="onetime-section">
|
||||
<div class="onetime-header">
|
||||
<span class="onetime-title">1회성 예산</span>
|
||||
<IconBtn icon="plus" title="1회성 예산 추가" variant="primary" size="sm" @click="openOnetimeCreate" />
|
||||
</div>
|
||||
|
||||
<form class="budget-form" @submit.prevent="submit">
|
||||
<label>카테고리
|
||||
<select v-model="form.category" :disabled="submitting">
|
||||
<option value="">(선택)</option>
|
||||
<template v-for="g in categoryGroups" :key="g.key">
|
||||
<optgroup v-if="g.label" :label="g.label">
|
||||
<option v-for="n in g.options" :key="n" :value="n">{{ n }}</option>
|
||||
</optgroup>
|
||||
<template v-else>
|
||||
<option v-for="n in g.options" :key="n" :value="n">{{ n }}</option>
|
||||
</template>
|
||||
</template>
|
||||
</select>
|
||||
</label>
|
||||
<ul v-if="onetimes.length" class="onetime-list">
|
||||
<li v-for="o in onetimes" :key="o.id" class="onetime-row">
|
||||
<div class="ot-top">
|
||||
<span class="ot-name">{{ o.title }}</span>
|
||||
<span class="ot-status" :class="o.status.toLowerCase()">{{ statusLabel(o.status) }}</span>
|
||||
<span class="ot-amounts">
|
||||
<span :class="o.remaining < 0 ? 'neg' : ''">{{ won(o.spent) }}</span>
|
||||
<span class="ot-sep">/</span>
|
||||
{{ won(o.amount) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="bar">
|
||||
<div class="bar-fill" :class="onetimeBarClass(o)" :style="{ width: Math.min(onetimeRatio(o), 100) + '%' }"></div>
|
||||
</div>
|
||||
<div class="ot-bottom">
|
||||
<span class="ot-date">{{ o.startDate }} ~ {{ o.endDate }}</span>
|
||||
<span v-if="o.category" class="ot-cat">{{ o.category }}</span>
|
||||
<span class="ot-remain" :class="o.remaining < 0 ? 'neg' : ''">
|
||||
{{ o.remaining >= 0 ? `잔여 ${won(o.remaining)}` : `초과 ${won(-o.remaining)}` }}
|
||||
</span>
|
||||
<span class="acts">
|
||||
<IconBtn icon="edit" title="수정" size="sm" @click="openOnetimeEdit(o)" />
|
||||
<IconBtn icon="trash" title="삭제" variant="danger" size="sm" @click="removeOnetime(o)" />
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="msg ot-empty">등록된 1회성 예산이 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="mode">
|
||||
<label class="radio"><input type="radio" :value="false" v-model="form.fixed" /> 비고정(기간별 직접)</label>
|
||||
<label class="radio"><input type="radio" :value="true" v-model="form.fixed" /> 고정(자동 환산)</label>
|
||||
</div>
|
||||
<!-- 1회성 예산 추가/수정 모달 -->
|
||||
<AppModal v-model="onetimeFormOpen" :title="`1회성 예산 ${onetimeEditId ? '수정' : '추가'}`">
|
||||
<form class="budget-form" @submit.prevent="submitOnetime">
|
||||
<label>제목<input v-model="onetimeForm.title" type="text" placeholder="예: 7월 여행, 추석 준비" :disabled="onetimeSubmitting" /></label>
|
||||
<div class="field">
|
||||
<span class="field-label">카테고리 <span class="field-hint">(선택 — 비우면 전체 지출)</span></span>
|
||||
<CategoryPicker
|
||||
v-model="onetimeForm.category"
|
||||
type="EXPENSE"
|
||||
:categories="categories"
|
||||
:disabled="onetimeSubmitting"
|
||||
@category-added="loadCategories"
|
||||
/>
|
||||
</div>
|
||||
<label>예산 금액<input v-model.number="onetimeForm.amount" type="number" min="1" placeholder="원" :disabled="onetimeSubmitting" /></label>
|
||||
<div class="date-range">
|
||||
<label>시작일<input v-model="onetimeForm.startDate" type="date" :disabled="onetimeSubmitting" /></label>
|
||||
<span class="range-sep">~</span>
|
||||
<label>종료일<input v-model="onetimeForm.endDate" type="date" :disabled="onetimeSubmitting" /></label>
|
||||
</div>
|
||||
<p v-if="onetimeFormError" class="msg error">{{ onetimeFormError }}</p>
|
||||
<div class="buttons">
|
||||
<IconBtn icon="close" title="취소" @click="onetimeFormOpen = false" />
|
||||
<IconBtn icon="save" :title="onetimeEditId ? '수정' : '등록'" variant="primary" type="submit" :disabled="onetimeSubmitting" />
|
||||
</div>
|
||||
</form>
|
||||
</AppModal>
|
||||
|
||||
<!-- 고정 -->
|
||||
<template v-if="form.fixed">
|
||||
<div class="fixed-base">
|
||||
<select v-model="form.baseUnit" :disabled="submitting">
|
||||
<option value="DAY">일</option>
|
||||
<option value="WEEK">주</option>
|
||||
<option value="MONTH">월</option>
|
||||
</select>
|
||||
<input v-model.number="form.baseAmount" type="number" min="0" placeholder="기준 금액(원)" :disabled="submitting" />
|
||||
</div>
|
||||
<p v-if="fixedPreview" class="preview">
|
||||
환산 — 주 {{ won(fixedPreview.weekly) }} · 월 {{ won(fixedPreview.monthly) }} · 년 {{ won(fixedPreview.yearly) }}
|
||||
<span class="note">(이번 달 {{ daysInMonth(year, month) }}일 / 올해 {{ daysInYear(year) }}일 기준)</span>
|
||||
</p>
|
||||
</template>
|
||||
<!-- 추가/수정 모달 (월별) -->
|
||||
<AppModal v-model="formOpen" :title="`예산 ${editId ? '수정' : '추가'}`">
|
||||
<form class="budget-form" @submit.prevent="submit">
|
||||
<div class="field">
|
||||
<span class="field-label">카테고리</span>
|
||||
<CategoryPicker
|
||||
v-model="form.category"
|
||||
type="EXPENSE"
|
||||
:categories="categories"
|
||||
:disabled="submitting"
|
||||
@category-added="loadCategories"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 비고정 -->
|
||||
<template v-else>
|
||||
<label>일 예산<input v-model.number="form.daily" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||
<label>주 예산<input v-model.number="form.weekly" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||
<label>월 예산<input v-model.number="form.monthly" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||
<label>년 예산<input v-model.number="form.yearly" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||
<p class="note">예산 대비 지출은 '월 예산' 기준으로 비교됩니다.</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>
|
||||
<div class="field">
|
||||
<span class="field-label">예산 방식</span>
|
||||
<div class="chip-group">
|
||||
<button type="button" class="chip" :class="{ active: !form.fixed }" :disabled="submitting" @click="form.fixed = false">비고정 (기간별 직접)</button>
|
||||
<button type="button" class="chip" :class="{ active: form.fixed }" :disabled="submitting" @click="form.fixed = true">고정 (자동 환산)</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- 고정 -->
|
||||
<template v-if="form.fixed">
|
||||
<div class="fixed-base">
|
||||
<select v-model="form.baseUnit" :disabled="submitting">
|
||||
<option value="DAY">일</option>
|
||||
<option value="WEEK">주</option>
|
||||
<option value="MONTH">월</option>
|
||||
</select>
|
||||
<input v-model.number="form.baseAmount" type="number" min="0" placeholder="기준 금액(원)" :disabled="submitting" />
|
||||
</div>
|
||||
<p v-if="fixedPreview" class="preview">
|
||||
환산 — 주 {{ won(fixedPreview.weekly) }} · 월 {{ won(fixedPreview.monthly) }} · 년 {{ won(fixedPreview.yearly) }}
|
||||
<span class="note">(이번 달 {{ daysInMonth(year, month) }}일 / 올해 {{ daysInYear(year) }}일 기준)</span>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- 비고정 -->
|
||||
<template v-else>
|
||||
<label>일 예산<input v-model.number="form.daily" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||
<label>주 예산<input v-model.number="form.weekly" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||
<label>월 예산<input v-model.number="form.monthly" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||
<label>년 예산<input v-model.number="form.yearly" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
|
||||
<p class="note">예산 대비 지출은 '월 예산' 기준으로 비교됩니다.</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>
|
||||
|
||||
@@ -647,43 +772,6 @@ button.primary {
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
/* 모달 */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 1rem;
|
||||
}
|
||||
.modal {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
padding: 1.75rem 1.5rem 1.5rem;
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.modal .close {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.6rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.modal h2 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.budget-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -703,17 +791,42 @@ button.primary {
|
||||
background: var(--color-background-soft);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.mode {
|
||||
/* 배지형 선택 */
|
||||
.field {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.radio {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
gap: 0.3rem !important;
|
||||
.field-label {
|
||||
font-size: 0.82rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.field-hint {
|
||||
font-size: 0.75rem;
|
||||
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;
|
||||
}
|
||||
.fixed-base {
|
||||
display: flex;
|
||||
@@ -741,12 +854,92 @@ button.primary {
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.18s ease;
|
||||
/* 1회성 예산 */
|
||||
.onetime-section {
|
||||
margin-top: 2rem;
|
||||
border-top: 2px solid var(--color-border);
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
.onetime-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.onetime-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.onetime-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.onetime-row {
|
||||
padding: 0.85rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.ot-top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.ot-name {
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ot-status {
|
||||
font-size: 0.7rem;
|
||||
border-radius: 3px;
|
||||
padding: 0.05rem 0.35rem;
|
||||
border: 1px solid;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ot-status.active { color: #2e7d32; border-color: #2e7d32; }
|
||||
.ot-status.upcoming { color: #1565c0; border-color: #1565c0; }
|
||||
.ot-status.ended { opacity: 0.5; }
|
||||
.ot-amounts {
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ot-amounts .neg { color: #c0392b; font-weight: 600; }
|
||||
.ot-sep { opacity: 0.5; margin: 0 0.2rem; }
|
||||
.ot-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ot-date { opacity: 0.65; }
|
||||
.ot-cat {
|
||||
background: var(--color-background-mute);
|
||||
border-radius: 3px;
|
||||
padding: 0.05rem 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.ot-remain { opacity: 0.85; }
|
||||
.ot-remain.neg { color: #c0392b; opacity: 1; }
|
||||
.ot-empty { opacity: 0.6; }
|
||||
|
||||
/* 날짜 범위 입력 */
|
||||
.date-range {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.date-range label {
|
||||
flex: 1;
|
||||
}
|
||||
.range-sep {
|
||||
padding-bottom: 0.55rem;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user