v1.12.24 멤버 작성 이력 통합 표 및 종류 필터 추가
This commit is contained in:
@@ -1,70 +1,85 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
memberId: { type: String, required: true },
|
||||
kind: { type: String, required: true }
|
||||
})
|
||||
const isComments = computed(() => props.kind === 'comments')
|
||||
const props = defineProps({ memberId: { type: String, required: true } })
|
||||
const searchInput = ref('')
|
||||
const filters = ref({ q: '', page: 1 })
|
||||
const query = computed(() => ({
|
||||
...filters.value, pageSize: 10,
|
||||
...(isComments.value ? { memberId: props.memberId } : {})
|
||||
}))
|
||||
const filters = ref({ q: '', type: 'all', page: 1 })
|
||||
const query = computed(() => ({ ...filters.value, pageSize: 10 }))
|
||||
const { data, status, error, refresh } = await useFetch(
|
||||
() => isComments.value ? '/admin/api/comments' : `/admin/api/members/${props.memberId}/posts`,
|
||||
() => `/admin/api/members/${props.memberId}/activity`,
|
||||
{ query, default: () => ({ items: [], pagination: { page: 1, pageCount: 1, total: 0 } }) }
|
||||
)
|
||||
const statusLabels = { published: '공개', draft: '초안', members: '멤버십', private: '비공개', pending: '승인 대기', blocked: '차단됨' }
|
||||
|
||||
/**
|
||||
* 검색어와 페이지를 함께 적용해 검색 결과 첫 페이지를 조회한다.
|
||||
* 검색어를 적용하고 첫 페이지를 조회한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const applySearch = () => { filters.value = { q: searchInput.value.trim(), page: 1 } }
|
||||
const applySearch = () => { filters.value = { ...filters.value, q: searchInput.value.trim(), page: 1 } }
|
||||
|
||||
/**
|
||||
* 전체 검색 결과에서 선택한 페이지를 조회한다.
|
||||
* 작성 종류를 바꾸고 첫 페이지에서 다시 조회한다.
|
||||
* @param {Event} event - 종류 선택 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const changeType = (event) => { filters.value = { q: searchInput.value.trim(), type: event.target.value, page: 1 } }
|
||||
|
||||
/**
|
||||
* 통합 검색 결과에서 선택한 페이지를 조회한다.
|
||||
* @param {number} page - 페이지 번호
|
||||
* @returns {void}
|
||||
*/
|
||||
const changePage = (page) => { filters.value = { ...filters.value, page } }
|
||||
|
||||
/**
|
||||
* 활동의 작성 시각을 한국어로 표시한다.
|
||||
* 작성 시각을 짧은 한국어 날짜로 표시한다.
|
||||
* @param {string} value - 작성 시각
|
||||
* @returns {string} 날짜·시각
|
||||
*/
|
||||
const formatCreatedAt = (value) => new Intl.DateTimeFormat('ko-KR', {
|
||||
dateStyle: 'medium', timeStyle: 'short', timeZone: 'Asia/Seoul'
|
||||
year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Seoul'
|
||||
}).format(new Date(value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-member-activity border border-line bg-white p-5" :aria-busy="status === 'pending'">
|
||||
<div class="admin-member-activity__toolbar flex flex-wrap items-center justify-between gap-4">
|
||||
<h2 class="admin-member-activity__heading text-base font-semibold">{{ isComments ? '작성한 댓글' : '작성한 글' }} <span class="text-xs font-normal text-muted">{{ data?.pagination.total || 0 }}개 · 최신순</span></h2>
|
||||
<form class="admin-member-activity__search flex w-full max-w-full items-center gap-2 sm:w-auto" role="search" @submit.prevent="applySearch">
|
||||
<AdminSearchInput root-class="min-w-0 flex-1" width-class="w-full sm:w-64" :value="searchInput" :label="isComments ? '작성한 댓글 검색' : '작성한 글 검색'" :placeholder="isComments ? '댓글 내용·게시물 제목 검색' : '제목·주소·본문 검색'" @input="searchInput = $event.target.value" />
|
||||
<section class="admin-member-activity border border-line bg-white p-4" :aria-busy="status === 'pending'">
|
||||
<div class="admin-member-activity__toolbar flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 class="admin-member-activity__heading text-sm font-semibold">작성한 글 <span class="text-xs font-normal text-muted">{{ data?.pagination.total || 0 }}개 · 최신순</span></h2>
|
||||
<form class="admin-member-activity__search flex w-full flex-wrap items-center gap-2 sm:w-auto" role="search" @submit.prevent="applySearch">
|
||||
<select class="admin-member-activity__type h-10 rounded border border-line bg-white px-3 text-sm" aria-label="작성 종류" :value="filters.type" @change="changeType">
|
||||
<option value="all">전체</option>
|
||||
<option value="comment">댓글</option>
|
||||
<option value="post">게시글</option>
|
||||
</select>
|
||||
<AdminSearchInput root-class="min-w-0 flex-1" width-class="w-full sm:w-64" :value="searchInput" label="작성한 글 검색" placeholder="게시글 제목·내용 검색" @input="searchInput = $event.target.value" />
|
||||
<button type="submit" class="admin-member-activity__search-button h-10 shrink-0 rounded border border-line px-3 text-sm">검색</button>
|
||||
</form>
|
||||
</div>
|
||||
<p v-if="error" class="admin-member-activity__error py-6 text-sm text-red-700" role="alert">목록을 불러오지 못했습니다. <button type="button" class="underline" @click="refresh()">다시 시도</button></p>
|
||||
<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 ? '검색 결과가 없습니다.' : isComments ? '작성한 댓글이 없습니다.' : '작성한 글이 없습니다.' }}</p>
|
||||
<ul v-else class="admin-member-activity__items mt-4 divide-y divide-line">
|
||||
<li v-for="item in data.items" :key="item.id" class="admin-member-activity__item py-4 text-sm">
|
||||
<div class="admin-member-activity__meta flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||
<time :datetime="item.createdAt">{{ formatCreatedAt(item.createdAt) }}</time>
|
||||
<span v-if="isComments && item.parentId">답글</span>
|
||||
<span>{{ statusLabels[item.status] || item.status }}</span>
|
||||
</div>
|
||||
<p v-if="isComments" class="admin-member-activity__body my-2 whitespace-pre-wrap break-words">{{ item.body }}</p>
|
||||
<div class="admin-member-activity__post mt-2 flex flex-wrap items-center gap-3">
|
||||
<NuxtLink :to="`/admin/posts/${isComments ? item.postId : item.id}`" class="break-words font-medium hover:underline">{{ (isComments ? item.postTitle : item.title) || '(제목 없음)' }}</NuxtLink>
|
||||
<NuxtLink v-if="['published', 'members'].includes(isComments ? item.postStatus : item.status)" :to="`/post/${encodeURIComponent(isComments ? item.postSlug : item.slug)}${isComments ? '#comments' : ''}`" target="_blank" rel="noopener noreferrer" class="shrink-0 text-xs text-muted underline">게시물 보기</NuxtLink>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<AdminPagination v-if="status !== 'pending' && !error" :page="data?.pagination.page" :page-count="data?.pagination.pageCount" :label="isComments ? '작성한 댓글 페이지' : '작성한 글 페이지'" @update:page="changePage" />
|
||||
<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">
|
||||
<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>
|
||||
</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<AdminPagination v-if="status !== 'pending' && !error" :page="data?.pagination.page" :page-count="data?.pagination.pageCount" label="작성한 글 페이지" @update:page="changePage" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -24,8 +24,7 @@ const { data: adminSession } = await useFetch('/admin/api/auth/me', {
|
||||
const selectedMemberTab = ref('info')
|
||||
const memberTabs = [
|
||||
{ value: 'info', label: '정보' },
|
||||
{ value: 'posts', label: '작성한 글' },
|
||||
{ value: 'comments', label: '작성한 댓글' }
|
||||
{ value: 'posts', label: '작성한 글' }
|
||||
]
|
||||
|
||||
const isNewMember = computed(() => props.mode === 'new')
|
||||
@@ -762,7 +761,7 @@ watch(() => props.member, () => {
|
||||
</nav>
|
||||
<AdminMemberActivityList
|
||||
v-if="!isNewMember && selectedMemberTab !== 'info'"
|
||||
:key="`${member.id}-${selectedMemberTab}`" :member-id="member.id" :kind="selectedMemberTab"
|
||||
:key="`${member.id}-${selectedMemberTab}`" :member-id="member.id"
|
||||
class="admin-member-form__content-tab mt-6"
|
||||
/>
|
||||
<div v-show="selectedMemberTab === 'info'" class="admin-member-form__body grid gap-8 py-8 xl:grid-cols-3">
|
||||
|
||||
Reference in New Issue
Block a user