feat: 게시판 공지 UI (관리자) — 작성 체크박스/상세 토글/목록 배지
- BoardWriteView: 관리자가 커뮤니티 새 글 작성 시 '공지로 등록' 체크박스 - BoardDetailView: 관리자 공지 등록/해제 버튼 + 제목 공지 배지 - BoardListView: 공지 배지 + 행 강조(최상단은 백엔드 정렬) - boardApi: setNotice/unsetNotice Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -53,4 +53,11 @@ export const boardApi = {
|
|||||||
unblock(id) {
|
unblock(id) {
|
||||||
return http.post(`/board/posts/${id}/unblock`)
|
return http.post(`/board/posts/${id}/unblock`)
|
||||||
},
|
},
|
||||||
|
// 공지 설정/해제 (관리자) — 목록 최상단 고정
|
||||||
|
setNotice(id) {
|
||||||
|
return http.post(`/board/posts/${id}/notice`)
|
||||||
|
},
|
||||||
|
unsetNotice(id) {
|
||||||
|
return http.post(`/board/posts/${id}/unnotice`)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,15 @@ 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
|
||||||
@@ -115,7 +124,7 @@ onMounted(load)
|
|||||||
|
|
||||||
<article v-if="post">
|
<article v-if="post">
|
||||||
<header class="post-head">
|
<header class="post-head">
|
||||||
<h1>{{ post.title }}</h1>
|
<h1><span v-if="post.notice" class="notice-badge">공지</span>{{ post.title }}</h1>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span>{{ post.authorName }}</span>
|
<span>{{ post.authorName }}</span>
|
||||||
<span>· {{ formatRelative(post.createdAt) }}</span>
|
<span>· {{ formatRelative(post.createdAt) }}</span>
|
||||||
@@ -144,6 +153,9 @@ onMounted(load)
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<IconBtn icon="back" title="목록" @click="router.push(`/board/${category}`)" />
|
<IconBtn icon="back" 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>
|
||||||
@@ -268,6 +280,20 @@ button.warn {
|
|||||||
border-color: #e67e22;
|
border-color: #e67e22;
|
||||||
color: #e67e22;
|
color: #e67e22;
|
||||||
}
|
}
|
||||||
|
button.notice-btn {
|
||||||
|
border-color: hsla(160, 100%, 37%, 1);
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
}
|
||||||
|
.notice-badge {
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: hsla(160, 100%, 37%, 1);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -163,8 +163,9 @@ onMounted(loadTags)
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="p in posts" :key="p.id" @click="router.push(`/board/${category}/${p.id}`)">
|
<tr v-for="p in posts" :key="p.id" :class="{ 'notice-row': p.notice }" @click="router.push(`/board/${category}/${p.id}`)">
|
||||||
<td>
|
<td>
|
||||||
|
<span v-if="p.notice" class="notice-badge">공지</span>
|
||||||
<template v-if="p.blocked && !isAdmin">
|
<template v-if="p.blocked && !isAdmin">
|
||||||
<span class="blocked-msg">🚫 관리자에 의해 제한된 게시글입니다.</span>
|
<span class="blocked-msg">🚫 관리자에 의해 제한된 게시글입니다.</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -318,6 +319,22 @@ button:disabled {
|
|||||||
color: #c0392b;
|
color: #c0392b;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
|
.notice-badge {
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
padding: 0.05rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: hsla(160, 100%, 37%, 1);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.notice-row {
|
||||||
|
background: hsla(160, 100%, 37%, 0.06);
|
||||||
|
}
|
||||||
|
.notice-row .title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* 하단 영역: 페이징(가운데) + 돋보기(오른쪽) 같은 행 */
|
/* 하단 영역: 페이징(가운데) + 돋보기(오른쪽) 같은 행 */
|
||||||
.list-footer {
|
.list-footer {
|
||||||
|
|||||||
@@ -2,19 +2,25 @@
|
|||||||
import { onMounted, ref, computed } from 'vue'
|
import { onMounted, ref, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { boardApi } from '@/api/boardApi'
|
import { boardApi } from '@/api/boardApi'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import MarkdownEditor from '@/components/editor/MarkdownEditor.vue'
|
import MarkdownEditor from '@/components/editor/MarkdownEditor.vue'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
import { DEFAULT_BOARD } from '@/constants/boards'
|
import { DEFAULT_BOARD } from '@/constants/boards'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
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 canNotice = computed(() => isAdmin.value && category === 'community' && !isEdit.value)
|
||||||
|
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
const content = ref('')
|
const content = ref('')
|
||||||
|
const notice = ref(false)
|
||||||
const tagGroups = ref([])
|
const tagGroups = ref([])
|
||||||
const selectedTagIds = ref([])
|
const selectedTagIds = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -65,6 +71,7 @@ async function submit() {
|
|||||||
title: title.value.trim(),
|
title: title.value.trim(),
|
||||||
content: content.value,
|
content: content.value,
|
||||||
category,
|
category,
|
||||||
|
notice: canNotice.value ? notice.value : false,
|
||||||
tagIds: selectedTagIds.value,
|
tagIds: selectedTagIds.value,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -97,6 +104,12 @@ onMounted(async () => {
|
|||||||
<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" />
|
||||||
|
|
||||||
|
<!-- 공지 등록 (관리자, 커뮤니티 새 글) -->
|
||||||
|
<label v-if="canNotice" class="notice-check">
|
||||||
|
<input v-model="notice" type="checkbox" :disabled="loading" />
|
||||||
|
📌 공지로 등록 (목록 최상단 고정)
|
||||||
|
</label>
|
||||||
|
|
||||||
<!-- 태그 선택 (DB 등록 태그를 뱃지로 토글) -->
|
<!-- 태그 선택 (DB 등록 태그를 뱃지로 토글) -->
|
||||||
<div class="tag-select">
|
<div class="tag-select">
|
||||||
<div v-if="!allTags.length" class="hint">
|
<div v-if="!allTags.length" class="hint">
|
||||||
@@ -178,6 +191,17 @@ h1 {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
|
.notice-check {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.notice-check input {
|
||||||
|
width: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
Reference in New Issue
Block a user