Merge branch 'dev'
This commit is contained in:
@@ -27,6 +27,7 @@ const ICONS = {
|
|||||||
refresh: ['M23 4v6h-6', 'M1 20v-6h6', 'M3.51 9a9 9 0 0 1 14.85-3.36L23 10', 'M1 14l4.64 4.36A9 9 0 0 0 20.49 15'],
|
refresh: ['M23 4v6h-6', 'M1 20v-6h6', 'M3.51 9a9 9 0 0 1 14.85-3.36L23 10', 'M1 14l4.64 4.36A9 9 0 0 0 20.49 15'],
|
||||||
filter: ['M22 3H2l8 9.46V19l4 2v-8.54L22 3z'],
|
filter: ['M22 3H2l8 9.46V19l4 2v-8.54L22 3z'],
|
||||||
menu: ['M3 12h18', 'M3 6h18', 'M3 18h18'],
|
menu: ['M3 12h18', 'M3 6h18', 'M3 18h18'],
|
||||||
|
list: ['M8 6h13', 'M8 12h13', 'M8 18h13', 'M3 6h.01', 'M3 12h.01', 'M3 18h.01'],
|
||||||
back: ['M19 12H5', 'M12 19l-7-7 7-7'],
|
back: ['M19 12H5', 'M12 19l-7-7 7-7'],
|
||||||
logout: ['M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4', 'M16 17l5-5-5-5', 'M21 12H9'],
|
logout: ['M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4', 'M16 17l5-5-5-5', 'M21 12H9'],
|
||||||
login: ['M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4', 'M10 17l5-5-5-5', 'M15 12H3'],
|
login: ['M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4', 'M10 17l5-5-5-5', 'M15 12H3'],
|
||||||
|
|||||||
@@ -108,8 +108,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="members">
|
<main class="members">
|
||||||
<header class="head">
|
<header class="head" style="justify-content: flex-end">
|
||||||
<h1>회원 관리</h1>
|
|
||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
<input v-model="keyword" type="search" class="search" placeholder="아이디·이름·이메일 검색" />
|
<input v-model="keyword" type="search" class="search" placeholder="아이디·이름·이메일 검색" />
|
||||||
<IconBtn icon="refresh" title="새로고침" size="sm" :disabled="loading" @click="load" />
|
<IconBtn icon="refresh" title="새로고침" size="sm" :disabled="loading" @click="load" />
|
||||||
|
|||||||
@@ -324,9 +324,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="dash">
|
<section class="dash">
|
||||||
<header class="head">
|
|
||||||
<h1>통계</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="month-nav">
|
<div class="month-nav">
|
||||||
<IconBtn icon="chevronLeft" title="이전 달" size="sm" @click="prevMonth" />
|
<IconBtn icon="chevronLeft" title="이전 달" size="sm" @click="prevMonth" />
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
|
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
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'
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
const tags = ref([])
|
const tags = ref([])
|
||||||
@@ -100,9 +98,6 @@ onBeforeUnmount(() => sortable?.destroy())
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="tag-admin">
|
<section class="tag-admin">
|
||||||
<header class="head">
|
|
||||||
<h1>가계부 태그 관리</h1>
|
|
||||||
</header>
|
|
||||||
<p class="hint">내가 등록한 태그는 나의 가계부 내역에서만 사용됩니다.</p>
|
<p class="hint">내가 등록한 태그는 나의 가계부 내역에서만 사용됩니다.</p>
|
||||||
|
|
||||||
<form class="new-tag" @submit.prevent="addTag">
|
<form class="new-tag" @submit.prevent="addTag">
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<section class="account">
|
<section class="account">
|
||||||
<header class="account-head">
|
<header class="account-head">
|
||||||
<h1>가계부<span v-if="pendingCount" class="pending-count">확인 필요 {{ pendingCount }}건</span></h1>
|
<span class="pending-wrap"><span v-if="pendingCount" class="pending-count">확인 필요 {{ pendingCount }}건</span></span>
|
||||||
<IconBtn icon="plus" title="내역 추가" variant="primary" @click="openCreate" />
|
<IconBtn icon="plus" title="내역 추가" variant="primary" @click="openCreate" />
|
||||||
</header>
|
</header>
|
||||||
<Transition name="fade"><p v-if="flashMsg" class="flash">{{ flashMsg }}</p></Transition>
|
<Transition name="fade"><p v-if="flashMsg" class="flash">{{ flashMsg }}</p></Transition>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
import { onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
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'
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
@@ -310,9 +308,6 @@ onBeforeUnmount(() => sortable?.destroy())
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="wallet">
|
<section class="wallet">
|
||||||
<header class="head">
|
|
||||||
<h1>계좌 관리</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- 순자산 요약 -->
|
<!-- 순자산 요약 -->
|
||||||
<div class="networth">
|
<div class="networth">
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { 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'
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -244,8 +242,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="budget">
|
<section class="budget">
|
||||||
<header class="head">
|
<header class="head" style="justify-content: flex-end">
|
||||||
<h1>예산 설정</h1>
|
|
||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
<IconBtn icon="plus" title="예산 추가" variant="primary" @click="openCreate" />
|
<IconBtn icon="plus" title="예산 추가" variant="primary" @click="openCreate" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
const rows = ref([]) // 현재 탭(activeType)의 분류 — 드래그 정렬 대상
|
const rows = ref([]) // 현재 탭(activeType)의 분류 — 드래그 정렬 대상
|
||||||
@@ -184,8 +182,7 @@ onBeforeUnmount(destroySortables)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="cat">
|
<section class="cat">
|
||||||
<header class="head">
|
<header class="head" style="justify-content: flex-end">
|
||||||
<h1>분류 관리</h1>
|
|
||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
<button type="button" class="text-btn" @click="importDefaults">기본 분류 불러오기</button>
|
<button type="button" class="text-btn" @click="importDefaults">기본 분류 불러오기</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { 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'
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
const recurrings = ref([])
|
const recurrings = ref([])
|
||||||
@@ -240,8 +238,7 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="recur">
|
<section class="recur">
|
||||||
<header class="head">
|
<header class="head" style="justify-content: flex-end">
|
||||||
<h1>고정 지출</h1>
|
|
||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
<IconBtn icon="refresh" title="지금 반영" @click="runNow" />
|
<IconBtn icon="refresh" title="지금 반영" @click="runNow" />
|
||||||
<IconBtn icon="plus" title="고정 지출 추가" variant="primary" @click="openCreate" />
|
<IconBtn icon="plus" title="고정 지출 추가" variant="primary" @click="openCreate" />
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
import { adminApi } from '@/api/adminApi'
|
import { adminApi } from '@/api/adminApi'
|
||||||
import { useDialog } from '@/composables/dialog'
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
@@ -150,9 +148,6 @@ onBeforeUnmount(destroySortables)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="cat">
|
<section class="cat">
|
||||||
<header class="head">
|
|
||||||
<h1>기본 분류 관리</h1>
|
|
||||||
</header>
|
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
전체 사용자 공용 <b>기본 분류</b> 템플릿입니다. 사용자가 분류 관리에서 <b>‘기본 분류 불러오기’</b>로 자기 분류에 복사해 씁니다.
|
전체 사용자 공용 <b>기본 분류</b> 템플릿입니다. 사용자가 분류 관리에서 <b>‘기본 분류 불러오기’</b>로 자기 분류에 복사해 씁니다.
|
||||||
<b>대분류 아래 소분류</b>로 묶을 수 있습니다(2단계).
|
<b>대분류 아래 소분류</b>로 묶을 수 있습니다(2단계).
|
||||||
|
|||||||
@@ -100,8 +100,6 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="tag-admin">
|
<section class="tag-admin">
|
||||||
<h1>태그 관리</h1>
|
|
||||||
|
|
||||||
<div class="board-setting">
|
<div class="board-setting">
|
||||||
<label>게시판 사용 카테고리</label>
|
<label>게시판 사용 카테고리</label>
|
||||||
<select v-model="boardCategoryId">
|
<select v-model="boardCategoryId">
|
||||||
|
|||||||
@@ -80,15 +80,6 @@ async function unblockPost() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setNoticePost(on) {
|
|
||||||
try {
|
|
||||||
await (on ? boardApi.setNotice(postId) : boardApi.unsetNotice(postId))
|
|
||||||
await load()
|
|
||||||
} catch (e) {
|
|
||||||
alert(e.response?.data?.message || '처리에 실패했습니다.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addComment() {
|
async function addComment() {
|
||||||
const content = commentText.value.trim()
|
const content = commentText.value.trim()
|
||||||
if (!content) return
|
if (!content) return
|
||||||
@@ -151,10 +142,8 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
<IconBtn icon="list" title="목록" @click="router.push(`/board/${category}`)" />
|
||||||
<div class="right-actions">
|
<div class="right-actions">
|
||||||
<!-- 관리자 공지 설정/해제 (커뮤니티) -->
|
|
||||||
<button v-if="isAdmin && category === 'community' && !post.notice" type="button" class="notice-btn" @click="setNoticePost(true)">공지 등록</button>
|
|
||||||
<button v-if="isAdmin && category === 'community' && post.notice" type="button" class="notice-btn" @click="setNoticePost(false)">공지 해제</button>
|
|
||||||
<!-- 관리자 제한/해제 -->
|
<!-- 관리자 제한/해제 -->
|
||||||
<button v-if="isAdmin && !post.blocked" type="button" class="warn" @click="blockPost">열람 제한</button>
|
<button v-if="isAdmin && !post.blocked" type="button" class="warn" @click="blockPost">열람 제한</button>
|
||||||
<button v-if="isAdmin && post.blocked" type="button" class="warn" @click="unblockPost">제한 해제</button>
|
<button v-if="isAdmin && post.blocked" type="button" class="warn" @click="unblockPost">제한 해제</button>
|
||||||
@@ -295,7 +284,7 @@ button.notice-btn {
|
|||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,8 +128,7 @@ onMounted(loadTags)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="board">
|
<section class="board">
|
||||||
<header class="board-head">
|
<header class="board-head" style="justify-content: flex-end">
|
||||||
<h1>{{ boardTitle }}</h1>
|
|
||||||
<IconBtn icon="edit" title="글쓰기" variant="primary" @click="router.push(`/board/${category}/write`)" />
|
<IconBtn icon="edit" title="글쓰기" variant="primary" @click="router.push(`/board/${category}/write`)" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const auth = useAuthStore()
|
|||||||
const category = route.params.category || DEFAULT_BOARD
|
const category = route.params.category || DEFAULT_BOARD
|
||||||
const editId = route.params.id || null
|
const editId = route.params.id || null
|
||||||
const isEdit = computed(() => !!editId)
|
const isEdit = computed(() => !!editId)
|
||||||
// 공지: 관리자가 커뮤니티 게시판에 새 글 작성할 때만 설정 가능
|
// 공지: 관리자가 커뮤니티 게시판 글을 작성/수정할 때 체크박스로 설정
|
||||||
const isAdmin = computed(() => auth.user?.role === 'ADMIN')
|
const isAdmin = computed(() => auth.user?.role === 'ADMIN')
|
||||||
const canNotice = computed(() => isAdmin.value && category === 'community' && !isEdit.value)
|
const canNotice = computed(() => isAdmin.value && category === 'community')
|
||||||
|
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
const content = ref('')
|
const content = ref('')
|
||||||
@@ -49,6 +49,7 @@ async function loadForEdit() {
|
|||||||
const p = await boardApi.get(editId)
|
const p = await boardApi.get(editId)
|
||||||
title.value = p.title
|
title.value = p.title
|
||||||
content.value = p.content
|
content.value = p.content
|
||||||
|
notice.value = !!p.notice
|
||||||
// 게시글의 태그(이름) → 현재 태그 목록에서 id 매핑
|
// 게시글의 태그(이름) → 현재 태그 목록에서 id 매핑
|
||||||
const nameToId = {}
|
const nameToId = {}
|
||||||
tagGroups.value.forEach((c) => c.tags.forEach((t) => (nameToId[t.name] = t.id)))
|
tagGroups.value.forEach((c) => c.tags.forEach((t) => (nameToId[t.name] = t.id)))
|
||||||
@@ -99,7 +100,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="write">
|
<section class="write">
|
||||||
<h1>{{ isEdit ? '글 수정' : '글쓰기' }}</h1>
|
|
||||||
|
|
||||||
<form class="write-form" @submit.prevent="submit">
|
<form class="write-form" @submit.prevent="submit">
|
||||||
<input v-model="title" type="text" placeholder="제목" maxlength="200" :disabled="loading" />
|
<input v-model="title" type="text" placeholder="제목" maxlength="200" :disabled="loading" />
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ function goEdit() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="account-info">
|
<div class="account-info">
|
||||||
<h1 class="page-title">계정정보</h1>
|
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -93,14 +93,6 @@ function cancel() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="profile-edit">
|
<div class="profile-edit">
|
||||||
<button type="button" class="back" @click="cancel">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M19 12H5" /><path d="M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
<span>계정정보</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<h1 class="page-title">가입정보 변경</h1>
|
|
||||||
|
|
||||||
<!-- 1단계: 비밀번호 재인증 -->
|
<!-- 1단계: 비밀번호 재인증 -->
|
||||||
<form v-if="step === 'verify'" class="card form" @submit.prevent="verify">
|
<form v-if="step === 'verify'" class="card form" @submit.prevent="verify">
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ async function clearAppData() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
<h1 class="page-title">설정</h1>
|
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
Reference in New Issue
Block a user