관리자 인기 지표와 회원 핵심 지표를 보강한다
This commit is contained in:
@@ -159,6 +159,7 @@ const props = defineProps({
|
||||
<div class="tierAdminCard__stats">
|
||||
<span class="pill" :class="tierList.isPublic ? 'pill--public' : 'pill--private'">{{ props.tierListVisibilityLabel(tierList) }}</span>
|
||||
<span v-if="tierList.isFeatured" class="pill pill--accent">추천 노출중</span>
|
||||
<span class="pill pill--soft">즐겨찾기 {{ tierList.favoriteCount || 0 }}개</span>
|
||||
<span class="pill">전체 아이템 {{ tierList.itemCount }}개</span>
|
||||
<span class="pill" :class="{ 'pill--accent': tierList.extraItemCount > 0 }">추가 아이템 {{ tierList.extraItemCount }}개</span>
|
||||
</div>
|
||||
|
||||
@@ -17,14 +17,11 @@ const props = defineProps({
|
||||
removeUserAvatar: { type: Function, required: true },
|
||||
canEditUserAvatar: { type: Function, required: true },
|
||||
canEditUserInfo: { type: Function, required: true },
|
||||
canResetUserPassword: { type: Function, required: true },
|
||||
canDeleteUser: { type: Function, required: true },
|
||||
roleLabelOf: { type: Function, required: true },
|
||||
fmt: { type: Function, required: true },
|
||||
openUserPasswordModal: { type: Function, required: true },
|
||||
openUserDeleteModal: { type: Function, required: true },
|
||||
openUserEditModal: { type: Function, required: true },
|
||||
lockResetIcon: { type: String, required: true },
|
||||
deleteIcon: { type: String, required: true },
|
||||
})
|
||||
|
||||
@@ -49,7 +46,7 @@ const userSortDirectionModel = computed({
|
||||
<div class="sectionHeader">
|
||||
<div>
|
||||
<div class="panel__title">회원 관리</div>
|
||||
<div class="hint hint--tight">회원 프로필을 정리하고, 필요한 경우에만 권한 변경과 비밀번호 초기화를 진행할 수 있어요.</div>
|
||||
<div class="hint hint--tight">팔로워·즐겨찾기 지표로 핵심 작성자를 확인하고, 회원 정보와 권한만 최소한으로 관리해요.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,6 +56,8 @@ const userSortDirectionModel = computed({
|
||||
<option value="recent">최근 활동순</option>
|
||||
<option value="created">가입순</option>
|
||||
<option value="tierlists">작성 티어표 많은 순</option>
|
||||
<option value="followers">팔로워 많은 순</option>
|
||||
<option value="favorites">받은 즐겨찾기 많은 순</option>
|
||||
</select>
|
||||
<select v-model="userSortDirectionModel" class="select toolbar__select" @change="props.submitUserFilters">
|
||||
<option value="desc">내림차순</option>
|
||||
@@ -113,6 +112,8 @@ const userSortDirectionModel = computed({
|
||||
<div class="userInfoList">
|
||||
<div class="userInfoLine"><span>가입일</span><strong>{{ props.fmt(user.createdAt) }}</strong></div>
|
||||
<div class="userInfoLine"><span>작성 티어표</span><strong>{{ user.tierListCount }}개</strong></div>
|
||||
<div class="userInfoLine"><span>팔로워</span><strong>{{ user.followerCount || 0 }}명</strong></div>
|
||||
<div class="userInfoLine"><span>받은 즐겨찾기</span><strong>{{ user.receivedFavoriteCount || 0 }}개</strong></div>
|
||||
<div class="userInfoLine"><span>최근 활동</span><strong>{{ props.fmt(user.recentActivityAt || user.createdAt) }}</strong></div>
|
||||
<div class="userInfoLine"><span>계정명</span><strong>{{ user.email }}</strong></div>
|
||||
<div class="userInfoLine"><span>닉네임</span><strong>{{ user.nickname || '미설정' }}</strong></div>
|
||||
@@ -120,15 +121,6 @@ const userSortDirectionModel = computed({
|
||||
</div>
|
||||
|
||||
<div class="userCard__actions userCard__actions--compact">
|
||||
<button
|
||||
class="iconActionButton"
|
||||
type="button"
|
||||
title="비밀번호 초기화"
|
||||
:disabled="!props.canResetUserPassword(user)"
|
||||
@click="props.openUserPasswordModal(user)"
|
||||
>
|
||||
<SvgIcon class="iconActionButton__icon" :src="props.lockResetIcon" :size="18" />
|
||||
</button>
|
||||
<button
|
||||
class="iconActionButton iconActionButton--danger"
|
||||
type="button"
|
||||
|
||||
@@ -81,10 +81,10 @@ export const api = {
|
||||
request(
|
||||
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}`
|
||||
),
|
||||
listAdminTierLists: ({ q = '', topicId = '', page = 1, limit = 50 } = {}) =>
|
||||
request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`),
|
||||
getAdminTierListStats: ({ q = '', topicId = '' } = {}) =>
|
||||
request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}`),
|
||||
listAdminTierLists: ({ q = '', topicId = '', sort = 'recent', minFavorites = 0, page = 1, limit = 50 } = {}) =>
|
||||
request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&sort=${encodeURIComponent(sort)}&minFavorites=${encodeURIComponent(minFavorites)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`),
|
||||
getAdminTierListStats: ({ q = '', topicId = '', minFavorites = 0 } = {}) =>
|
||||
request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&minFavorites=${encodeURIComponent(minFavorites)}`),
|
||||
updateAdminTierList: (tierListId, payload) =>
|
||||
request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}`, { method: 'PATCH', body: payload }),
|
||||
updateAdminTierListFeatured: (tierListId, payload) =>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { api } from '../lib/api'
|
||||
import { editorPath } from '../lib/paths'
|
||||
import { toApiUrl } from '../lib/runtime'
|
||||
import lockResetIcon from '../assets/icons/lock_reset.svg'
|
||||
import deleteIcon from '../assets/icons/delete.svg'
|
||||
import SvgIcon from '../components/SvgIcon.vue'
|
||||
import AdminFeaturedSection from '../components/admin/AdminFeaturedSection.vue'
|
||||
@@ -51,6 +50,8 @@ const customItemModalTargetTemplateId = ref('')
|
||||
const adminTierLists = ref([])
|
||||
const adminTierListQuery = ref('')
|
||||
const adminTierListTopicId = ref('')
|
||||
const adminTierListSort = ref('recent')
|
||||
const adminTierListMinFavorites = ref(0)
|
||||
const adminTierListPage = ref(1)
|
||||
const adminTierListLimit = ref(50)
|
||||
const adminTierListTotal = ref(0)
|
||||
@@ -824,6 +825,8 @@ async function refreshAdminTierLists() {
|
||||
const data = await api.listAdminTierLists({
|
||||
q: adminTierListQuery.value,
|
||||
topicId: adminTierListTopicId.value,
|
||||
sort: adminTierListSort.value,
|
||||
minFavorites: adminTierListMinFavorites.value,
|
||||
page: adminTierListPage.value,
|
||||
limit: adminTierListLimit.value,
|
||||
})
|
||||
@@ -840,7 +843,11 @@ async function refreshAdminTierLists() {
|
||||
async function refreshAdminTierListStats() {
|
||||
if (!auth.user?.isAdmin) return
|
||||
try {
|
||||
const data = await api.getAdminTierListStats({ q: adminTierListQuery.value, topicId: adminTierListTopicId.value })
|
||||
const data = await api.getAdminTierListStats({
|
||||
q: adminTierListQuery.value,
|
||||
topicId: adminTierListTopicId.value,
|
||||
minFavorites: adminTierListMinFavorites.value,
|
||||
})
|
||||
adminTierListStats.value = {
|
||||
total: data.total || 0,
|
||||
publicCount: data.publicCount || 0,
|
||||
@@ -1312,6 +1319,17 @@ function submitAdminTierListSearch() {
|
||||
refreshAdminTierLists()
|
||||
}
|
||||
|
||||
function changeAdminTierListSort() {
|
||||
adminTierListPage.value = 1
|
||||
refreshAdminTierLists()
|
||||
}
|
||||
|
||||
function changeAdminTierListMinFavorites() {
|
||||
adminTierListMinFavorites.value = Math.max(Number(adminTierListMinFavorites.value) || 0, 0)
|
||||
adminTierListPage.value = 1
|
||||
refreshAdminTierLists()
|
||||
}
|
||||
|
||||
function setAdminTierListTopicId(topicId) {
|
||||
adminTierListTopicId.value = topicId || ''
|
||||
adminTierListPage.value = 1
|
||||
@@ -1825,14 +1843,11 @@ function userAvatarFallback(user) {
|
||||
:remove-user-avatar="removeUserAvatar"
|
||||
:can-edit-user-avatar="canEditUserAvatar"
|
||||
:can-edit-user-info="canEditUserInfo"
|
||||
:can-reset-user-password="canResetUserPassword"
|
||||
:can-delete-user="canDeleteUser"
|
||||
:role-label-of="roleLabelOf"
|
||||
:fmt="fmt"
|
||||
:open-user-password-modal="openUserPasswordModal"
|
||||
:open-user-delete-modal="openUserDeleteModal"
|
||||
:open-user-edit-modal="openUserEditModal"
|
||||
:lock-reset-icon="lockResetIcon"
|
||||
:delete-icon="deleteIcon"
|
||||
@update:user-query="userQuery = $event"
|
||||
@update:user-sort="userSort = $event"
|
||||
@@ -1906,31 +1921,6 @@ function userAvatarFallback(user) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="userPasswordModalOpen" class="modalOverlay" @click.self="closeUserPasswordModal">
|
||||
<div class="modalCard" role="dialog" aria-modal="true">
|
||||
<div class="modalCard__title">비밀번호 초기화</div>
|
||||
<div class="modalCard__desc">{{ modalTargetUser ? `${userDisplayName(modalTargetUser)} 계정에 설정할 새 비밀번호를 입력해주세요.` : '' }}</div>
|
||||
<div class="modalCard__form">
|
||||
<label class="field">
|
||||
<span class="field__label">새 비밀번호</span>
|
||||
<input
|
||||
v-model="modalPasswordDraft"
|
||||
class="field__input"
|
||||
type="password"
|
||||
maxlength="120"
|
||||
placeholder="초기화할 비밀번호 입력"
|
||||
@keydown.enter.prevent="confirmUserPasswordReset"
|
||||
/>
|
||||
<span class="field__hint">6~120자 권장 · {{ modalPasswordDraft.length }}/120자</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modalCard__actions">
|
||||
<button class="btn btn--ghost" @click="closeUserPasswordModal">취소</button>
|
||||
<button class="btn btn--primary" :disabled="!modalPasswordDraft.trim()" @click="confirmUserPasswordReset">초기화</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="userDeleteModalOpen" class="modalOverlay" @click.self="closeUserDeleteModal">
|
||||
<div class="modalCard" role="dialog" aria-modal="true">
|
||||
<div class="modalCard__title">회원 삭제</div>
|
||||
@@ -2350,6 +2340,21 @@ function userAvatarFallback(user) {
|
||||
<option :value="50">50개씩 보기</option>
|
||||
<option :value="200">200개씩 보기</option>
|
||||
</select>
|
||||
<select v-model="adminTierListSort" class="select" @change="changeAdminTierListSort">
|
||||
<option value="recent">최근 수정순</option>
|
||||
<option value="created">최근 생성순</option>
|
||||
<option value="favorites">즐겨찾기 많은 순</option>
|
||||
</select>
|
||||
<input
|
||||
v-model.number="adminTierListMinFavorites"
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000000"
|
||||
placeholder="최소 즐겨찾기 수"
|
||||
@change="changeAdminTierListMinFavorites"
|
||||
@keydown.enter.prevent="changeAdminTierListMinFavorites"
|
||||
/>
|
||||
</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshAdminTierLists">새로고침</button>
|
||||
|
||||
Reference in New Issue
Block a user