feat: 게시판 인기/블라인드/자기글 차단·헤더 아바타·댓글 새로고침 등
CI / build (push) Failing after 11m35s

- 헤더에 내 아바타(계정정보 링크) + 앱 시작 시 프로필 동기화(재로그인 없이 아바타/포인트)
- 목록: 인기 글 상단 배지(🔥), 비추천 20+ 블라인드 표시
- 상세: 본인 글/댓글 추천·비추천 비활성, 비추천 20+ 글/댓글 블라인드(그래도 보기)
- 댓글: 인기 댓글 배지, 본문 작성자에 작성자 뱃지, 목록 하단 새로고침 버튼(입력폼 위)
- 추천자 목록 가로 줄바꿈(아바타+이름 단위, 이름 안 잘리게)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-28 13:21:37 +09:00
parent 7699cfc55f
commit 7e38eae2b7
6 changed files with 220 additions and 24 deletions
+143 -21
View File
@@ -39,6 +39,26 @@ function canModifyComment(c) {
return auth.user.id === c.authorId || isAdmin.value
}
// 본인 글/댓글 여부 (추천/비추천 차단용)
const isMyPost = computed(() => !!post.value && auth.user?.id === post.value.authorId)
function isMyComment(c) {
return auth.user?.id === c.authorId
}
// 본문 작성자가 단 댓글인지 (작성자 뱃지)
function isAuthorComment(c) {
return !!post.value && c.authorId === post.value.authorId
}
// 비추천 20개 이상 블라인드 + 펼치기 상태
const BLIND_DOWN = 20
function isBlinded(item) {
return (item?.downCount || 0) >= BLIND_DOWN
}
const postRevealed = ref(false)
const revealedComments = ref({})
function toggleReveal(c) {
revealedComments.value[c.id] = !revealedComments.value[c.id]
}
async function load() {
loading.value = true
error.value = null
@@ -106,6 +126,21 @@ async function removeComment(c) {
}
}
// 댓글 목록만 새로고침 (다른 사람이 단 댓글·추천 반영)
const refreshingComments = ref(false)
async function refreshComments() {
if (refreshingComments.value) return
refreshingComments.value = true
try {
const fresh = await boardApi.get(postId)
post.value.comments = fresh.comments
} catch (e) {
alert(e.response?.data?.message || '댓글을 새로고침하지 못했습니다.')
} finally {
refreshingComments.value = false
}
}
// ===== 추천/비추천 =====
const voting = ref(false)
async function votePost(type) {
@@ -178,17 +213,27 @@ onMounted(load)
🚫 관리자에 의해 열람이 제한된 게시글입니다.
</div>
<!-- 비추천 많은 블라인드 (작성자·관리자는 정상 표시) -->
<div v-else-if="isBlinded(post) && !isMyPost && !isAdmin && !postRevealed" class="blinded">
🙈 비추천이 많아 블라인드 처리된 게시글입니다.
<button type="button" class="reveal-btn" @click="postRevealed = true">그래도 보기</button>
</div>
<MarkdownViewer v-else :content="post.content" class="content" />
<!-- 추천 / 비추천 -->
<!-- 추천 / 비추천 (본인 글은 비활성) -->
<div v-if="!restricted" class="vote-bar">
<button
type="button" class="vote-btn up" :class="{ active: post.myVote === 'UP' }"
:disabled="voting" @click="votePost('UP')"
:disabled="voting || isMyPost"
:title="isMyPost ? '본인 글은 추천할 수 없습니다' : ''"
@click="votePost('UP')"
>👍 추천 <b>{{ post.upCount || 0 }}</b></button>
<button
type="button" class="vote-btn down" :class="{ active: post.myVote === 'DOWN' }"
:disabled="voting" @click="votePost('DOWN')"
:disabled="voting || isMyPost"
:title="isMyPost ? '본인 글은 비추천할 수 없습니다' : ''"
@click="votePost('DOWN')"
>👎 비추천 <b>{{ post.downCount || 0 }}</b></button>
</div>
@@ -249,7 +294,7 @@ onMounted(load)
<h2>댓글 {{ post.comments.length }}</h2>
<ul class="comment-list">
<li v-for="c in post.comments" :key="c.id" class="comment">
<li v-for="c in post.comments" :key="c.id" class="comment" :class="{ 'hot-comment': c.hot }">
<div class="comment-head">
<UserAvatar
:name="c.authorName"
@@ -258,26 +303,46 @@ onMounted(load)
:size="16"
/>
<strong>{{ c.authorName }}</strong>
<span v-if="isAuthorComment(c)" class="author-badge">작성자</span>
<span v-if="c.hot" class="hot-badge">🔥 인기</span>
<span class="comment-date">{{ formatRelative(c.createdAt) }}</span>
<span v-if="canModifyComment(c)" class="comment-del">
<IconBtn icon="trash" title="댓글 삭제" variant="danger" size="sm" @click="removeComment(c)" />
</span>
</div>
<MarkdownViewer :content="c.content" class="comment-body" />
<div class="comment-votes">
<button
type="button" class="cvote up" :class="{ active: c.myVote === 'UP' }"
@click="voteComment(c, 'UP')"
>👍 {{ c.upCount || 0 }}</button>
<button
type="button" class="cvote down" :class="{ active: c.myVote === 'DOWN' }"
@click="voteComment(c, 'DOWN')"
>👎 {{ c.downCount || 0 }}</button>
<!-- 비추천 많은 댓글 블라인드 (작성자·관리자는 정상) -->
<div v-if="isBlinded(c) && !isMyComment(c) && !isAdmin && !revealedComments[c.id]" class="blinded comment-blinded">
🙈 비추천이 많아 블라인드된 댓글입니다.
<button type="button" class="reveal-btn" @click="toggleReveal(c)">그래도 보기</button>
</div>
<template v-else>
<MarkdownViewer :content="c.content" class="comment-body" />
<div class="comment-votes">
<button
type="button" class="cvote up" :class="{ active: c.myVote === 'UP' }"
:disabled="isMyComment(c)"
:title="isMyComment(c) ? '본인 댓글은 추천할 수 없습니다' : ''"
@click="voteComment(c, 'UP')"
>👍 {{ c.upCount || 0 }}</button>
<button
type="button" class="cvote down" :class="{ active: c.myVote === 'DOWN' }"
:disabled="isMyComment(c)"
:title="isMyComment(c) ? '본인 댓글은 비추천할 수 없습니다' : ''"
@click="voteComment(c, 'DOWN')"
>👎 {{ c.downCount || 0 }}</button>
</div>
</template>
</li>
</ul>
<p v-if="!post.comments.length" class="msg"> 댓글을 남겨보세요.</p>
<!-- 댓글 목록 최하단(입력폼 ) 새로고침 -->
<div class="comment-refresh">
<button type="button" class="refresh-btn" :disabled="refreshingComments" @click="refreshComments">
🔄 {{ refreshingComments ? '새로고침 ' : '댓글 새로고침' }}
</button>
</div>
<form v-if="!post.blocked" class="comment-form" @submit.prevent="addComment">
<MarkdownEditor
v-model="commentText"
@@ -393,21 +458,22 @@ h1 {
.rec-inline-list {
list-style: none;
margin: 0.4rem 0 0;
padding: 0.3rem 0.5rem;
padding: 0.55rem 0.65rem;
border: 1px solid var(--color-border);
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 0.1rem;
flex-wrap: wrap;
gap: 0.45rem 0.8rem;
}
.rec-inline-list li {
display: flex;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.4rem;
gap: 0.35rem;
white-space: nowrap; /* 이름이 잘리지 않도록 아바타+이름 단위로 줄바꿈 */
flex: none;
}
.rec-name {
font-size: 0.9rem;
font-size: 0.85rem;
}
.tags {
display: flex;
@@ -519,11 +585,67 @@ button.notice-btn {
.comment-body {
margin-top: 0.3rem;
}
.author-badge {
padding: 0.05rem 0.35rem;
border-radius: 3px;
background: hsla(160, 100%, 37%, 1);
color: #fff;
font-size: 0.68rem;
font-weight: 700;
}
.hot-comment {
background: rgba(231, 76, 60, 0.05);
border-radius: 6px;
}
.hot-badge {
padding: 0.05rem 0.35rem;
border-radius: 3px;
background: #e74c3c;
color: #fff;
font-size: 0.68rem;
font-weight: 700;
}
.comment-votes {
display: flex;
gap: 0.4rem;
margin-top: 0.4rem;
}
/* 블라인드 */
.blinded {
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
flex-wrap: wrap;
padding: 1.5rem 1rem;
border: 1px dashed var(--color-border);
border-radius: 8px;
color: var(--color-text);
opacity: 0.85;
font-size: 0.92rem;
}
.comment-blinded {
padding: 0.7rem 0.8rem;
margin-top: 0.3rem;
font-size: 0.85rem;
}
.reveal-btn {
padding: 0.3rem 0.8rem;
border-radius: 999px;
font-size: 0.82rem;
}
/* 댓글 새로고침 */
.comment-refresh {
display: flex;
justify-content: center;
margin: 0.5rem 0 0.25rem;
}
.refresh-btn {
padding: 0.35rem 1rem;
border-radius: 999px;
font-size: 0.85rem;
color: var(--color-text);
}
.cvote {
padding: 0.2rem 0.6rem;
font-size: 0.8rem;
+33
View File
@@ -13,6 +13,15 @@ const router = useRouter()
const auth = useAuthStore()
const isAdmin = computed(() => auth.user?.role === 'ADMIN')
// 비추천 20개 이상이면 블라인드 (작성자·관리자에겐 정상 표시)
const BLIND_DOWN = 20
function isBlinded(p) {
return (p.downCount || 0) >= BLIND_DOWN
}
function showBlindMsg(p) {
return isBlinded(p) && !isAdmin.value && auth.user?.id !== p.authorId
}
// 현재 게시판(카테고리)
const category = computed(() => route.params.category || DEFAULT_BOARD)
@@ -165,12 +174,17 @@ onMounted(loadTags)
<tr v-for="p in posts" :key="p.id" :class="{ 'notice-row': p.notice }" @click="router.push({ path: `/board/${category}/${p.id}`, query: route.query })">
<td>
<span v-if="p.notice" class="notice-badge">공지</span>
<span v-if="p.hot" class="hot-badge">🔥 인기</span>
<template v-if="p.blocked && !isAdmin">
<span class="blocked-msg">🚫 관리자에 의해 제한된 게시글입니다.</span>
</template>
<template v-else-if="showBlindMsg(p)">
<span class="blocked-msg">🙈 비추천이 많아 블라인드된 게시글입니다.</span>
</template>
<template v-else>
<span class="title">{{ p.title }}</span>
<span v-if="p.blocked" class="blocked-badge">제한</span>
<span v-if="isBlinded(p)" class="blind-badge">블라인드</span>
<span v-if="p.commentCount" class="comment-count">[{{ p.commentCount }}]</span>
</template>
</td>
@@ -374,6 +388,25 @@ button:disabled {
.notice-row .title {
font-weight: 600;
}
.hot-badge {
margin-right: 0.4rem;
padding: 0.05rem 0.4rem;
border-radius: 3px;
background: #e74c3c;
color: #fff;
font-size: 0.72rem;
font-weight: 700;
vertical-align: middle;
}
.blind-badge {
margin-left: 0.35rem;
padding: 0.05rem 0.35rem;
border: 1px solid var(--color-border);
border-radius: 3px;
color: var(--color-text);
opacity: 0.6;
font-size: 0.72rem;
}
/* 하단 영역: 페이징(가운데) + 돋보기(오른쪽) 같은 행 */
.list-footer {