v1.12.29 접속자 요약과 회원 작성 이력 및 댓글 표시 정리

This commit is contained in:
2026-10-21 18:17:03 +09:00
parent 184a971863
commit 51d5de148f
15 changed files with 114 additions and 47 deletions

View File

@@ -26,12 +26,12 @@ const formatCreatedAt = (value) => new Intl.DateTimeFormat('ko-KR', {
</p>
<p v-else-if="!data?.items.length" class="admin-comment-list__empty py-1 text-xs text-muted">작성한 댓글이 없습니다.</p>
<ul v-else class="admin-comment-list__items divide-y divide-line">
<li v-for="comment in data.items" :key="comment.id" class="admin-comment-list__item grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-0.5 py-1.5 text-xs md:grid-cols-[minmax(0,2fr)_7rem_8rem_minmax(0,3fr)]">
<li v-for="comment in data.items" :key="comment.id" class="admin-comment-list__item grid min-w-0 grid-cols-2 items-center gap-x-3 gap-y-0.5 py-1.5 text-xs md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)_7rem_8rem]">
<NuxtLink :to="`/post/${encodeURIComponent(comment.postSlug)}#comments`" v-if="['published', 'members'].includes(comment.postStatus)" target="_blank" rel="noopener noreferrer" class="admin-comment-list__post truncate font-medium hover:underline" :title="comment.postTitle">{{ comment.postTitle || '(제목 없음)' }}</NuxtLink>
<NuxtLink v-else :to="`/admin/posts/${comment.postId}`" class="admin-comment-list__post truncate font-medium hover:underline" :title="comment.postTitle">{{ comment.postTitle || '(제목 없음)' }}</NuxtLink>
<NuxtLink :to="`/admin/members/${comment.userId}`" class="admin-comment-list__author truncate text-muted hover:underline" :title="comment.username">{{ comment.username }}</NuxtLink>
<time :datetime="comment.createdAt" class="admin-comment-list__time text-muted">{{ formatCreatedAt(comment.createdAt) }}</time>
<p class="admin-comment-list__body truncate text-muted" :title="comment.body">{{ comment.body }}</p>
<NuxtLink :to="`/admin/members/${comment.userId}`" class="admin-comment-list__author truncate text-muted hover:underline" :title="comment.username">{{ comment.username }}</NuxtLink>
<time :datetime="comment.createdAt" class="admin-comment-list__time whitespace-nowrap text-muted">{{ formatCreatedAt(comment.createdAt) }}</time>
</li>
</ul>
</section>

View File

@@ -57,26 +57,22 @@ const formatCreatedAt = (value) => new Intl.DateTimeFormat('ko-KR', {
<p v-else-if="status === 'pending'" class="admin-member-activity__loading py-6 text-sm text-muted">목록을 불러오는 중입니다.</p>
<p v-else-if="!data?.items.length" class="admin-member-activity__empty py-6 text-sm text-muted">{{ filters.q ? '검색 결과가 없습니다.' : '작성한 글이 없습니다.' }}</p>
<div v-else class="admin-member-activity__table-wrap mt-3 overflow-x-auto">
<table class="admin-member-activity__table w-full min-w-[760px] table-fixed text-left text-xs">
<table class="admin-member-activity__table w-full min-w-[600px] table-fixed text-left text-xs">
<caption class="sr-only">회원이 작성한 게시글과 댓글 목록</caption>
<thead class="border-b border-line text-muted">
<tr>
<th scope="col" class="w-16 py-2 pr-3 font-medium">구분</th>
<th scope="col" class="w-[28%] py-2 pr-3 font-medium">게시글</th>
<th scope="col" class="w-28 py-2 pr-3 font-medium">작성자</th>
<th scope="col" class="w-44 py-2 pr-3 font-medium">작성일</th>
<th scope="col" class="py-2 font-medium">내용</th>
<th scope="col" class="w-[32%] py-2 pr-3 font-medium">게시글</th>
<th scope="col" class="py-2 pr-3 font-medium">내용</th>
<th scope="col" class="w-44 py-2 font-medium">작성일</th>
</tr>
</thead>
<tbody class="divide-y divide-line">
<tr v-for="item in data.items" :key="`${item.kind}-${item.id}`" class="admin-member-activity__item">
<td class="admin-member-activity__kind whitespace-nowrap py-2 pr-3" :class="item.kind === 'post' ? 'text-blue-700' : 'text-muted'">{{ item.kind === 'post' ? '게시글' : '댓글' }}</td>
<td class="py-2 pr-3">
<NuxtLink :to="item.publicUrl || `/admin/posts/${item.postId}`" :target="item.publicUrl ? '_blank' : undefined" :rel="item.publicUrl ? 'noopener noreferrer' : undefined" class="admin-member-activity__post block truncate font-medium hover:underline" :title="item.postTitle">{{ item.postTitle || '(제목 없음)' }}</NuxtLink>
</td>
<td class="py-2 pr-3"><span class="admin-member-activity__author block truncate text-muted" :title="item.username">{{ item.username }}</span></td>
<td class="whitespace-nowrap py-2 pr-3 text-muted"><time :datetime="item.createdAt">{{ formatCreatedAt(item.createdAt) }}</time></td>
<td class="py-2"><p class="admin-member-activity__body truncate text-muted" :title="item.body">{{ item.body || '—' }}</p></td>
<td class="py-2 pr-3"><p class="admin-member-activity__body truncate text-muted" :title="item.body">{{ item.body || '—' }}</p></td>
<td class="whitespace-nowrap py-2 text-muted"><time :datetime="item.createdAt">{{ formatCreatedAt(item.createdAt) }}</time></td>
</tr>
</tbody>
</table>

View File

@@ -39,6 +39,7 @@ const cancelClose = () => { clearTimeout(closeTimer) }
* @returns {Promise<void>} 위치 반영
*/
const openPages = async () => {
if (pages.value.length <= 1) return
cancelClose()
isOpen.value = true
await nextTick()
@@ -103,6 +104,10 @@ const handleKeydown = (event) => {
}
}
watch(() => pages.value.length, (count) => {
if (count <= 1) closePages()
})
onMounted(() => {
document.addEventListener('pointerdown', handleOutside)
document.addEventListener('keydown', handleKeydown)
@@ -120,12 +125,14 @@ onBeforeUnmount(() => {
<template>
<div class="admin-realtime-pages min-w-0">
<button ref="triggerRef" type="button" class="admin-realtime-pages__trigger flex w-full min-w-0 items-center gap-1 text-left text-xs text-muted hover:text-ink" :aria-expanded="isOpen" :aria-controls="popoverId" aria-label="열린 페이지 목록" @mouseenter="openPages" @mouseleave="scheduleClose" @focus="openPages" @blur="scheduleClose" @click="togglePinned">
<button v-if="pages.length > 1" ref="triggerRef" type="button" class="admin-realtime-pages__trigger flex w-full min-w-0 items-center gap-1 text-left text-xs text-muted hover:text-ink" :aria-expanded="isOpen" :aria-controls="popoverId" aria-label="열린 페이지 목록" @mouseenter="openPages" @mouseleave="scheduleClose" @focus="openPages" @blur="scheduleClose" @click="togglePinned">
<span class="admin-realtime-pages__latest min-w-0 truncate">{{ getActivePageTitle(pages[0]) }}</span>
<span v-if="pages.length > 1" class="admin-realtime-pages__extra shrink-0 font-medium">외 {{ formatAnalyticsCount(pages.length - 1) }}개</span>
<span v-if="pages.length > 1" class="admin-realtime-pages__extra shrink-0 font-medium text-[#2563eb]">외 <strong class="admin-realtime-pages__extra-number font-bold">{{ formatAnalyticsCount(pages.length - 1) }}</strong>개</span>
</button>
<a v-else-if="getActivePageHref(pages[0])" :href="getActivePageHref(pages[0])" target="_blank" rel="noopener noreferrer" class="admin-realtime-pages__single block truncate text-xs text-muted hover:text-ink hover:underline">{{ getActivePageTitle(pages[0]) }}</a>
<span v-else class="admin-realtime-pages__single block truncate text-xs text-muted">{{ getActivePageTitle(pages[0]) }}</span>
<Teleport to="body">
<AdminPopoverSurface v-if="isOpen" :id="popoverId" root-class="admin-realtime-pages__popover admin-layout--light-controls fixed z-[80] overflow-y-auto rounded-lg border-line p-3 text-xs text-[#15171a]" :surface-style="popoverStyle" @mouseenter="cancelClose" @mouseleave="scheduleClose" @focusin="cancelClose" @focusout="scheduleClose">
<AdminPopoverSurface v-if="isOpen && pages.length > 1" :id="popoverId" root-class="admin-realtime-pages__popover admin-layout--light-controls fixed z-[80] overflow-y-auto rounded-lg border-line p-3 text-xs text-[#15171a]" :surface-style="popoverStyle" @mouseenter="cancelClose" @mouseleave="scheduleClose" @focusin="cancelClose" @focusout="scheduleClose">
<div ref="panelRef">
<div class="mb-2 flex items-center justify-between gap-3">
<p class="font-semibold">열린 페이지 {{ formatAnalyticsCount(pages.length) }}개 · 최근 확인순</p>