diff --git a/src/api/authApi.js b/src/api/authApi.js index 08ceda7..976cd30 100644 --- a/src/api/authApi.js +++ b/src/api/authApi.js @@ -39,4 +39,8 @@ export const authApi = { updateProfileImage(image) { return http.put('/auth/profile-image', { image }) }, + // 현재 사용자 활동 포인트 (최신값) + points() { + return http.get('/auth/points') + }, } diff --git a/src/api/boardApi.js b/src/api/boardApi.js index 0b97b62..45646d6 100644 --- a/src/api/boardApi.js +++ b/src/api/boardApi.js @@ -47,6 +47,17 @@ export const boardApi = { removeComment(commentId) { return http.delete(`/board/comments/${commentId}`) }, + // 추천/비추천 (토글) — type: 'UP' | 'DOWN' + votePost(id, type) { + return http.post(`/board/posts/${id}/vote`, { type }) + }, + voteComment(commentId, type) { + return http.post(`/board/comments/${commentId}/vote`, { type }) + }, + // 추천(👍)한 사람 목록 + recommenders(id) { + return http.get(`/board/posts/${id}/recommenders`) + }, block(id, reason) { return http.post(`/board/posts/${id}/block`, { reason }) }, diff --git a/src/components/UserAvatar.vue b/src/components/UserAvatar.vue new file mode 100644 index 0000000..3f5021a --- /dev/null +++ b/src/components/UserAvatar.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/views/board/BoardDetailView.vue b/src/views/board/BoardDetailView.vue index 3e4e854..83cbb4a 100644 --- a/src/views/board/BoardDetailView.vue +++ b/src/views/board/BoardDetailView.vue @@ -8,6 +8,7 @@ import { formatRelative } from '@/utils/datetime' import MarkdownViewer from '@/components/editor/MarkdownViewer.vue' import MarkdownEditor from '@/components/editor/MarkdownEditor.vue' import IconBtn from '@/components/ui/IconBtn.vue' +import UserAvatar from '@/components/UserAvatar.vue' import { DEFAULT_BOARD } from '@/constants/boards' const route = useRoute() @@ -105,6 +106,45 @@ async function removeComment(c) { } } +// ===== 추천/비추천 ===== +const voting = ref(false) +async function votePost(type) { + if (voting.value) return + voting.value = true + try { + const res = await boardApi.votePost(postId, type) + post.value.upCount = res.upCount + post.value.downCount = res.downCount + post.value.myVote = res.myVote + // 추천 변동 → 추천자 목록 갱신 + post.value.recommenders = await boardApi.recommenders(postId) + } catch (e) { + alert(e.response?.data?.message || '처리에 실패했습니다.') + } finally { + voting.value = false + } +} + +async function voteComment(c, type) { + try { + const res = await boardApi.voteComment(c.id, type) + c.upCount = res.upCount + c.downCount = res.downCount + c.myVote = res.myVote + } catch (e) { + alert(e.response?.data?.message || '처리에 실패했습니다.') + } +} + +// ===== 추천자 목록 ===== +const recommenders = computed(() => post.value?.recommenders || []) +const previewRecommenders = computed(() => recommenders.value.slice(0, 10)) +const extraRecommenders = computed(() => Math.max(0, (post.value?.upCount || 0) - 10)) +const showRecommenders = ref(false) +function openRecommenders() { + if (post.value?.upCount) showRecommenders.value = true +} + onMounted(load) @@ -117,7 +157,13 @@ onMounted(load)

공지{{ post.title }}

- {{ post.authorName }} + + {{ post.authorName }} · {{ formatRelative(post.createdAt) }} · 조회 {{ post.viewCount }}
@@ -136,6 +182,37 @@ onMounted(load) + +
+ + +
+ + +
+
+ +
+ + + + +
+
{{ t }} @@ -162,6 +239,12 @@ onMounted(load)
  • + {{ c.authorName }} {{ formatRelative(c.createdAt) }} @@ -169,6 +252,16 @@ onMounted(load)
    +
    + + +

첫 댓글을 남겨보세요.

@@ -186,6 +279,29 @@ onMounted(load) + + + +
+ +
+
@@ -208,11 +324,74 @@ h1 { .meta { font-size: 0.85rem; color: var(--color-text); - opacity: 0.75; + opacity: 0.85; display: flex; + align-items: center; gap: 0.4rem; flex-wrap: wrap; } +.meta-author { + font-weight: 600; +} +/* 추천/비추천 */ +.vote-bar { + display: flex; + justify-content: center; + gap: 0.75rem; + padding: 0.5rem 0 1rem; +} +.vote-btn { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.5rem 1.4rem; + border-radius: 999px; + font-size: 0.92rem; +} +.vote-btn b { + font-weight: 700; +} +.vote-btn.up.active { + border-color: hsla(160, 100%, 37%, 1); + color: hsla(160, 100%, 37%, 1); + background: hsla(160, 100%, 37%, 0.08); +} +.vote-btn.down.active { + border-color: #e67e22; + color: #e67e22; + background: rgba(230, 126, 34, 0.08); +} +/* 추천자 */ +.recommenders { + display: flex; + align-items: center; + gap: 0.6rem; + flex-wrap: wrap; + padding: 0.6rem 0.8rem; + border: 1px solid var(--color-border); + border-radius: 8px; + background: var(--color-background-soft); + cursor: pointer; + margin-bottom: 0.5rem; +} +.recommenders:hover { + background: var(--color-background-mute); +} +.rec-avatars { + display: flex; +} +.rec-avatar { + margin-left: -6px; + box-shadow: 0 0 0 2px var(--color-background-soft); +} +.rec-avatar:first-child { + margin-left: 0; +} +.rec-label { + font-size: 0.85rem; + color: var(--color-text); + opacity: 0.85; +} .tags { display: flex; gap: 0.4rem; @@ -323,6 +502,68 @@ button.notice-btn { .comment-body { margin-top: 0.3rem; } +.comment-votes { + display: flex; + gap: 0.4rem; + margin-top: 0.4rem; +} +.cvote { + padding: 0.2rem 0.6rem; + font-size: 0.8rem; + border-radius: 999px; +} +.cvote.up.active { + border-color: hsla(160, 100%, 37%, 1); + color: hsla(160, 100%, 37%, 1); +} +.cvote.down.active { + border-color: #e67e22; + color: #e67e22; +} +/* 추천자 모달 */ +.rec-modal-backdrop { + position: fixed; + inset: 0; + z-index: 2000; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} +.rec-modal { + width: 100%; + max-width: 360px; + max-height: 70vh; + display: flex; + flex-direction: column; + background: var(--color-background); + border: 1px solid var(--color-border); + border-radius: 10px; + overflow: hidden; +} +.rec-modal-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.85rem 1rem; + border-bottom: 1px solid var(--color-border); +} +.rec-modal-list { + list-style: none; + margin: 0; + padding: 0.5rem 0; + overflow-y: auto; +} +.rec-modal-list li { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.5rem 1rem; +} +.rec-name { + font-size: 0.95rem; +} .comment-form { margin-top: 1rem; } diff --git a/src/views/board/BoardListView.vue b/src/views/board/BoardListView.vue index 3b1f83d..2e857ff 100644 --- a/src/views/board/BoardListView.vue +++ b/src/views/board/BoardListView.vue @@ -6,6 +6,7 @@ import { formatRelative } from '@/utils/datetime' import { useAuthStore } from '@/stores/auth' import { DEFAULT_BOARD } from '@/constants/boards' import IconBtn from '@/components/ui/IconBtn.vue' +import UserAvatar from '@/components/UserAvatar.vue' const route = useRoute() const router = useRouter() @@ -170,9 +171,20 @@ onMounted(loadTags) {{ p.title }} 제한 [{{ p.commentCount }}] + 👍 {{ p.upCount }} - {{ p.authorName }} + + + + {{ p.authorName }} + + {{ p.viewCount }} {{ formatRelative(p.createdAt) }} @@ -307,7 +319,18 @@ button:disabled { text-align: center; } .col-author { - width: 100px; + width: 130px; +} +.author-cell { + display: inline-flex; + align-items: center; + gap: 0.4rem; + min-width: 0; +} +.author-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .col-date { width: 110px; @@ -318,6 +341,12 @@ button:disabled { color: hsla(160, 100%, 37%, 1); font-size: 0.85rem; } +.recommend-count { + margin-left: 0.35rem; + color: var(--color-text); + opacity: 0.7; + font-size: 0.82rem; +} .blocked-msg { color: #c0392b; font-size: 0.9rem; diff --git a/src/views/settings/AccountInfoView.vue b/src/views/settings/AccountInfoView.vue index 03754bf..e252a5e 100644 --- a/src/views/settings/AccountInfoView.vue +++ b/src/views/settings/AccountInfoView.vue @@ -1,5 +1,5 @@