- 분류 관리: 대분류 선택·소분류 들여쓰기(드래그 정렬 유지) - 입력/예산/고정지출 분류 드롭다운 대분류 optgroup 그룹핑(값은 소분류명) - 대시보드: 파이는 소분류 기준 + 대분류 합계 롤업 표시 - 릴리스 노트 33 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,13 @@
|
|||||||
## 32. 계좌 관리 — 탭 한 줄
|
## 32. 계좌 관리 — 탭 한 줄
|
||||||
- 계좌 종류 탭(은행/현금/카드/대출/투자)이 두 줄로 줄바꿈되던 것 → **한 줄(넘치면 가로 스크롤)**. 버튼 글자 줄바꿈 방지.
|
- 계좌 종류 탭(은행/현금/카드/대출/투자)이 두 줄로 줄바꿈되던 것 → **한 줄(넘치면 가로 스크롤)**. 버튼 글자 줄바꿈 방지.
|
||||||
|
|
||||||
|
## 33. 분류 — 대분류/소분류(2단계)
|
||||||
|
- **분류를 대분류 아래 소분류로 묶기**(2단계). `account_category.parent_id` 추가(멱등 마이그레이션). 기존 분류는 모두 대분류로 시작 → 분류 관리에서 소분류로 재배치.
|
||||||
|
- **소분류 이름 기반 유지**(내역·예산·고정지출·통계 무변경) — 같은 소분류 이름 중복은 불가, 예산·통계는 **소분류 기준**.
|
||||||
|
- 분류 관리: 행별 대분류 선택·소분류 들여쓰기, 드래그 정렬 유지. 자식 있는 대분류는 소분류화·삭제 차단.
|
||||||
|
- 입력/예산/고정지출 드롭다운: **대분류 optgroup** 그룹핑(값은 소분류명).
|
||||||
|
- 대시보드 통계: 파이는 소분류 기준 + 아래 **대분류 합계** 롤업 표시.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 운영 반영 시 체크
|
## 운영 반영 시 체크
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const barData = ref([]) // [{bucket, budget, expense}]
|
|||||||
const monthlyStats = ref([]) // MONTH unit of year (income/expense)
|
const monthlyStats = ref([]) // MONTH unit of year (income/expense)
|
||||||
const catType = ref('EXPENSE') // 분류별 파이차트 구분
|
const catType = ref('EXPENSE') // 분류별 파이차트 구분
|
||||||
const catData = ref([]) // [{category, total}]
|
const catData = ref([]) // [{category, total}]
|
||||||
|
const categoryList = ref([]) // 분류 목록(대/소분류 매핑용)
|
||||||
const trendData = ref([]) // [{month, netWorth}]
|
const trendData = ref([]) // [{month, netWorth}]
|
||||||
|
|
||||||
const periodLabel = computed(() => `${year.value}년 ${String(month.value).padStart(2, '0')}월`)
|
const periodLabel = computed(() => `${year.value}년 ${String(month.value).padStart(2, '0')}월`)
|
||||||
@@ -188,6 +189,33 @@ async function loadCat() {
|
|||||||
catData.value = []
|
catData.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function loadCategoryList() {
|
||||||
|
try {
|
||||||
|
categoryList.value = await accountApi.categories()
|
||||||
|
} catch {
|
||||||
|
categoryList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 소분류명 → 대분류명 매핑(현재 구분). 소분류가 아니면 자기 자신.
|
||||||
|
const majorRollup = computed(() => {
|
||||||
|
const list = categoryList.value.filter((c) => c.type === catType.value)
|
||||||
|
const byId = {}
|
||||||
|
list.forEach((c) => (byId[c.id] = c))
|
||||||
|
const toMajor = (name) => {
|
||||||
|
const c = list.find((x) => x.name === name)
|
||||||
|
if (c && c.parentId != null && byId[c.parentId]) return byId[c.parentId].name
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
const map = {}
|
||||||
|
for (const d of catData.value) {
|
||||||
|
const major = toMajor(d.category)
|
||||||
|
map[major] = (map[major] || 0) + d.total
|
||||||
|
}
|
||||||
|
const rows = Object.entries(map).map(([category, total]) => ({ category, total })).sort((a, b) => b.total - a.total)
|
||||||
|
// 실제로 묶인 게 있을 때만(소분류 존재) 노출
|
||||||
|
const hasSub = list.some((c) => c.parentId != null) && rows.length < catData.value.length
|
||||||
|
return hasSub ? rows : []
|
||||||
|
})
|
||||||
function setCatType(t) {
|
function setCatType(t) {
|
||||||
catType.value = t
|
catType.value = t
|
||||||
loadCat()
|
loadCat()
|
||||||
@@ -290,6 +318,7 @@ onMounted(async () => {
|
|||||||
load()
|
load()
|
||||||
loadMonthly()
|
loadMonthly()
|
||||||
loadTrend()
|
loadTrend()
|
||||||
|
loadCategoryList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -393,6 +422,15 @@ onMounted(async () => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="majorRollup.length" class="major-rollup">
|
||||||
|
<div class="mr-head">대분류 합계</div>
|
||||||
|
<ul class="mr-list">
|
||||||
|
<li v-for="m in majorRollup" :key="m.category">
|
||||||
|
<span class="mr-cat">{{ m.category }}</span>
|
||||||
|
<span class="mr-amt">{{ won(m.total) }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<p v-else class="empty">{{ catType === 'EXPENSE' ? '지출' : '수입' }} 내역이 없습니다.</p>
|
<p v-else class="empty">{{ catType === 'EXPENSE' ? '지출' : '수입' }} 내역이 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -760,6 +798,33 @@ h2 {
|
|||||||
fill: var(--color-text);
|
fill: var(--color-text);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
.major-rollup {
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
padding-top: 0.6rem;
|
||||||
|
border-top: 1px dashed var(--color-border);
|
||||||
|
}
|
||||||
|
.mr-head {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
.mr-list {
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 0.1rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.mr-list li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.1rem 0;
|
||||||
|
}
|
||||||
|
.mr-cat {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.pie-legend {
|
.pie-legend {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -234,6 +234,23 @@ const categoryOptions = computed(() => {
|
|||||||
if (form.category && !names.includes(form.category)) names.unshift(form.category)
|
if (form.category && !names.includes(form.category)) names.unshift(form.category)
|
||||||
return names
|
return names
|
||||||
})
|
})
|
||||||
|
// 대분류로 그룹핑(소분류 = optgroup 옵션 / 자식없는 대분류 = 단독 옵션). 값은 분류명(소분류) 그대로.
|
||||||
|
const categoryGroups = computed(() => {
|
||||||
|
const list = categories.value.filter((c) => c.type === form.type)
|
||||||
|
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 allCategoryNames = computed(() => [...new Set(categories.value.map((c) => c.name))])
|
const allCategoryNames = computed(() => [...new Set(categories.value.map((c) => c.name))])
|
||||||
|
|
||||||
@@ -811,7 +828,14 @@ onMounted(async () => {
|
|||||||
<div v-if="!addingCategory" class="cat-input">
|
<div v-if="!addingCategory" class="cat-input">
|
||||||
<select v-model="form.category" :disabled="submitting">
|
<select v-model="form.category" :disabled="submitting">
|
||||||
<option value="">(선택)</option>
|
<option value="">(선택)</option>
|
||||||
<option v-for="c in categoryOptions" :key="c" :value="c">{{ c }}</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>
|
</select>
|
||||||
<button type="button" class="cat-add-btn" :disabled="submitting" @click="startAddCategory">+ 추가</button>
|
<button type="button" class="cat-add-btn" :disabled="submitting" @click="startAddCategory">+ 추가</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,6 +44,20 @@ const categoryOptions = computed(() => {
|
|||||||
if (form.category && !names.includes(form.category)) names.unshift(form.category)
|
if (form.category && !names.includes(form.category)) names.unshift(form.category)
|
||||||
return names
|
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')}월`)
|
const periodLabel = computed(() => `${year.value}년 ${String(month.value).padStart(2, '0')}월`)
|
||||||
|
|
||||||
@@ -305,7 +319,14 @@ onMounted(() => {
|
|||||||
<label>카테고리
|
<label>카테고리
|
||||||
<select v-model="form.category" :disabled="submitting">
|
<select v-model="form.category" :disabled="submitting">
|
||||||
<option value="">(선택)</option>
|
<option value="">(선택)</option>
|
||||||
<option v-for="c in categoryOptions" :key="c" :value="c">{{ c }}</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>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
@@ -13,6 +13,7 @@ const loading = ref(false)
|
|||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
const activeType = ref('EXPENSE') // EXPENSE / INCOME
|
const activeType = ref('EXPENSE') // EXPENSE / INCOME
|
||||||
const newName = ref('')
|
const newName = ref('')
|
||||||
|
const newParent = ref('') // 추가 시 대분류('' = 대분류로 추가)
|
||||||
|
|
||||||
// categories/activeType 변경 시 현재 탭 목록 동기화 (백엔드가 sort_order 순 정렬)
|
// categories/activeType 변경 시 현재 탭 목록 동기화 (백엔드가 sort_order 순 정렬)
|
||||||
function syncRows() {
|
function syncRows() {
|
||||||
@@ -20,6 +21,13 @@ function syncRows() {
|
|||||||
}
|
}
|
||||||
watch([categories, activeType], syncRows, { immediate: true })
|
watch([categories, activeType], syncRows, { immediate: true })
|
||||||
|
|
||||||
|
// 현재 탭의 대분류(parent 없는 것) 목록 — 부모 선택 옵션용
|
||||||
|
const majors = computed(() => rows.value.filter((c) => c.parentId == null))
|
||||||
|
// 부모(대분류) 선택 옵션: 자기 자신 제외
|
||||||
|
function parentOptions(c) {
|
||||||
|
return majors.value.filter((m) => m.id !== c.id)
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
@@ -67,7 +75,7 @@ async function addCategory() {
|
|||||||
const name = newName.value.trim()
|
const name = newName.value.trim()
|
||||||
if (!name) return
|
if (!name) return
|
||||||
try {
|
try {
|
||||||
await accountApi.createCategory({ type: activeType.value, name })
|
await accountApi.createCategory({ type: activeType.value, name, parentId: newParent.value || null })
|
||||||
newName.value = ''
|
newName.value = ''
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -79,7 +87,8 @@ async function saveCategory(c) {
|
|||||||
const name = (c.name || '').trim()
|
const name = (c.name || '').trim()
|
||||||
if (!name) return
|
if (!name) return
|
||||||
try {
|
try {
|
||||||
await accountApi.updateCategory(c.id, { type: c.type, name })
|
// parentId 는 항상 현재값을 함께 전송(미전송 시 대분류로 풀림)
|
||||||
|
await accountApi.updateCategory(c.id, { type: c.type, name, parentId: c.parentId || null })
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e.response?.data?.message || '수정에 실패했습니다.')
|
alert(e.response?.data?.message || '수정에 실패했습니다.')
|
||||||
@@ -122,7 +131,7 @@ onBeforeUnmount(() => sortable?.destroy())
|
|||||||
<IconBtn icon="back" title="가계부로" @click="router.push('/account')" />
|
<IconBtn icon="back" title="가계부로" @click="router.push('/account')" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<p class="hint">내역·예산에서 선택할 분류를 관리합니다. 분류 이름을 바꾸면 기존 내역·예산에도 반영됩니다.</p>
|
<p class="hint">내역·예산에서 선택할 분류를 관리합니다. <b>대분류 아래 소분류</b>로 묶을 수 있고(2단계), 예산·통계는 소분류 기준입니다. 이름을 바꾸면 기존 내역·예산에도 반영됩니다.</p>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button type="button" :class="{ active: activeType === 'EXPENSE' }" @click="activeType = 'EXPENSE'">지출 분류</button>
|
<button type="button" :class="{ active: activeType === 'EXPENSE' }" @click="activeType = 'EXPENSE'">지출 분류</button>
|
||||||
@@ -130,18 +139,27 @@ onBeforeUnmount(() => sortable?.destroy())
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="new-cat" @submit.prevent="addCategory">
|
<form class="new-cat" @submit.prevent="addCategory">
|
||||||
|
<select v-model="newParent" class="parent-sel" title="대분류 선택">
|
||||||
|
<option value="">대분류로 추가</option>
|
||||||
|
<option v-for="m in majors" :key="m.id" :value="m.id">{{ m.name }}의 소분류</option>
|
||||||
|
</select>
|
||||||
<input v-model="newName" type="text" :placeholder="activeType === 'EXPENSE' ? '예: 식비, 교통' : '예: 급여, 용돈'" />
|
<input v-model="newName" type="text" :placeholder="activeType === 'EXPENSE' ? '예: 식비, 교통' : '예: 급여, 용돈'" />
|
||||||
<IconBtn icon="plus" title="추가" variant="primary" type="submit" />
|
<IconBtn icon="plus" title="추가" variant="primary" type="submit" />
|
||||||
</form>
|
</form>
|
||||||
<p class="hint sm">≡ 손잡이를 끌어 순서를 바꾸면 내역 추가 화면에도 그 순서로 나옵니다.</p>
|
<p class="hint sm">≡ 손잡이를 끌어 순서 변경. 각 행의 대분류를 바꾸면 소분류로 묶입니다.</p>
|
||||||
|
|
||||||
<p v-if="error" class="msg error">{{ error }}</p>
|
<p v-if="error" class="msg error">{{ error }}</p>
|
||||||
<p v-if="loading" class="msg">불러오는 중...</p>
|
<p v-if="loading" class="msg">불러오는 중...</p>
|
||||||
|
|
||||||
<ul v-show="!loading && rows.length" ref="listEl" class="cat-list">
|
<ul v-show="!loading && rows.length" ref="listEl" class="cat-list">
|
||||||
<li v-for="c in rows" :key="c.id" :data-id="c.id" class="cat-row">
|
<li v-for="c in rows" :key="c.id" :data-id="c.id" class="cat-row" :class="{ child: c.parentId != null }">
|
||||||
<span class="drag-handle" title="드래그하여 순서 변경">≡</span>
|
<span class="drag-handle" title="드래그하여 순서 변경">≡</span>
|
||||||
|
<span v-if="c.parentId != null" class="sub-mark" title="소분류">↳</span>
|
||||||
<input v-model="c.name" class="cat-name" @keyup.enter="saveCategory(c)" />
|
<input v-model="c.name" class="cat-name" @keyup.enter="saveCategory(c)" />
|
||||||
|
<select v-model="c.parentId" class="parent-sel" title="대분류" @change="saveCategory(c)">
|
||||||
|
<option :value="null">(대분류)</option>
|
||||||
|
<option v-for="m in parentOptions(c)" :key="m.id" :value="m.id">{{ m.name }}</option>
|
||||||
|
</select>
|
||||||
<IconBtn icon="check" title="저장" @click="saveCategory(c)" />
|
<IconBtn icon="check" title="저장" @click="saveCategory(c)" />
|
||||||
<IconBtn icon="trash" title="삭제" variant="danger" @click="removeCategory(c)" />
|
<IconBtn icon="trash" title="삭제" variant="danger" @click="removeCategory(c)" />
|
||||||
</li>
|
</li>
|
||||||
@@ -245,6 +263,23 @@ button.danger {
|
|||||||
}
|
}
|
||||||
.cat-name {
|
.cat-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.parent-sel {
|
||||||
|
max-width: 40%;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.new-cat .parent-sel {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.cat-row.child {
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
}
|
||||||
|
.cat-row.child .cat-name {
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
}
|
||||||
|
.sub-mark {
|
||||||
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
.hint.sm {
|
.hint.sm {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
|
|||||||
@@ -41,6 +41,20 @@ const categoryOptions = computed(() => {
|
|||||||
if (form.category && !names.includes(form.category)) names.unshift(form.category)
|
if (form.category && !names.includes(form.category)) names.unshift(form.category)
|
||||||
return names
|
return names
|
||||||
})
|
})
|
||||||
|
// 대분류로 그룹핑 (소분류=optgroup 옵션 / 자식없는 대분류=단독 옵션)
|
||||||
|
const categoryGroups = computed(() => {
|
||||||
|
const list = categories.value.filter((c) => c.type === form.type)
|
||||||
|
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 WALLET_KINDS = [
|
const WALLET_KINDS = [
|
||||||
{ value: 'BANK', label: '계좌' },
|
{ value: 'BANK', label: '계좌' },
|
||||||
@@ -326,7 +340,14 @@ onMounted(load)
|
|||||||
<label v-if="form.type !== 'TRANSFER'">분류
|
<label v-if="form.type !== 'TRANSFER'">분류
|
||||||
<select v-model="form.category" :disabled="submitting">
|
<select v-model="form.category" :disabled="submitting">
|
||||||
<option value="">(선택)</option>
|
<option value="">(선택)</option>
|
||||||
<option v-for="c in categoryOptions" :key="c" :value="c">{{ c }}</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>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>메모<input v-model="form.memo" type="text" placeholder="(선택)" :disabled="submitting" /></label>
|
<label>메모<input v-model="form.memo" type="text" placeholder="(선택)" :disabled="submitting" /></label>
|
||||||
|
|||||||
Reference in New Issue
Block a user