156 lines
7.1 KiB
Vue
156 lines
7.1 KiB
Vue
<script setup>
|
|
import { computed } from 'vue'
|
|
import SvgIcon from '../SvgIcon.vue'
|
|
|
|
const props = defineProps({
|
|
userQuery: { type: String, required: true },
|
|
userSort: { type: String, required: true },
|
|
userSortDirection: { type: String, required: true },
|
|
users: { type: Array, required: true },
|
|
submitUserFilters: { type: Function, required: true },
|
|
setUserAvatarInput: { type: Function, required: true },
|
|
onUserAvatarChange: { type: Function, required: true },
|
|
openUserAvatarPicker: { type: Function, required: true },
|
|
userAvatarUrl: { type: Function, required: true },
|
|
userDisplayName: { type: Function, required: true },
|
|
userAvatarFallback: { type: Function, required: true },
|
|
removeUserAvatar: { type: Function, required: true },
|
|
canEditUserAvatar: { type: Function, required: true },
|
|
canEditUserInfo: { type: Function, required: true },
|
|
canDeleteUser: { type: Function, required: true },
|
|
roleLabelOf: { type: Function, required: true },
|
|
fmt: { type: Function, required: true },
|
|
openUserProfile: { type: Function, required: true },
|
|
openUserDeleteModal: { type: Function, required: true },
|
|
openUserEditModal: { type: Function, required: true },
|
|
deleteIcon: { type: String, required: true },
|
|
})
|
|
|
|
const emit = defineEmits(['update:userQuery', 'update:userSort', 'update:userSortDirection'])
|
|
|
|
const userQueryModel = computed({
|
|
get: () => props.userQuery,
|
|
set: (value) => emit('update:userQuery', value),
|
|
})
|
|
const userSortModel = computed({
|
|
get: () => props.userSort,
|
|
set: (value) => emit('update:userSort', value),
|
|
})
|
|
const userSortDirectionModel = computed({
|
|
get: () => props.userSortDirection,
|
|
set: (value) => emit('update:userSortDirection', value),
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="panel">
|
|
<div class="sectionHeader">
|
|
<div>
|
|
<div class="panel__title">회원 관리</div>
|
|
<div class="hint hint--tight">팔로워·즐겨찾기 지표로 핵심 작성자를 확인하고, 회원 정보와 권한만 최소한으로 관리해요.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toolbar toolbar--secondary">
|
|
<input v-model="userQueryModel" class="input toolbar__search" placeholder="이메일, 닉네임 검색" @keydown.enter.prevent="props.submitUserFilters" />
|
|
<select v-model="userSortModel" class="select toolbar__select" @change="props.submitUserFilters">
|
|
<option value="recent">최근 콘텐츠 활동순</option>
|
|
<option value="lastLogin">마지막 접속순</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>
|
|
<option value="asc">오름차순</option>
|
|
</select>
|
|
<button class="btn btn--ghost toolbar__button" type="button" @click="props.submitUserFilters">조회</button>
|
|
</div>
|
|
|
|
<div v-if="!props.users.length" class="hint">아직 가입한 회원이 없어요.</div>
|
|
<div v-else class="userList">
|
|
<article v-for="user in props.users" :key="user.id" class="userCard">
|
|
<div class="userCard__head">
|
|
<div class="userCard__identity">
|
|
<input
|
|
:ref="(el) => props.setUserAvatarInput(user.id, el)"
|
|
type="file"
|
|
accept="image/*"
|
|
class="srOnlyInput"
|
|
@change="props.onUserAvatarChange(user, $event)"
|
|
/>
|
|
<div class="userAvatarWrap">
|
|
<button
|
|
class="userAvatar userAvatarButton"
|
|
type="button"
|
|
:disabled="user.isAvatarBusy || !props.canEditUserAvatar(user)"
|
|
@click="props.openUserAvatarPicker(user)"
|
|
>
|
|
<img v-if="props.userAvatarUrl(user)" class="userAvatar__image" :src="props.userAvatarUrl(user)" :alt="props.userDisplayName(user)" />
|
|
<span v-else class="userAvatar__fallback">{{ props.userAvatarFallback(user) }}</span>
|
|
<span class="userAvatarButton__overlay">{{ user.isAvatarBusy ? '업데이트중...' : '수정' }}</span>
|
|
</button>
|
|
<button
|
|
v-if="user?.avatarSrc"
|
|
class="userAvatarRemoveButton"
|
|
type="button"
|
|
title="회원 썸네일 삭제"
|
|
:disabled="user.isAvatarBusy || !props.canEditUserAvatar(user)"
|
|
@click.stop="props.removeUserAvatar(user)"
|
|
>
|
|
<SvgIcon class="userAvatarRemoveIcon" :src="props.deleteIcon" :size="12" />
|
|
</button>
|
|
</div>
|
|
<div class="userCard__identityMeta">
|
|
<div class="userCard__title">{{ props.userDisplayName(user) }}</div>
|
|
<div class="userCard__meta">{{ user.email }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="user.isAdmin" class="roleBadge userCard__roleBadge">{{ props.roleLabelOf(user) }}</div>
|
|
|
|
<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.lastLoginAt ? props.fmt(user.lastLoginAt) : '기록 없음' }}</strong></div>
|
|
<div class="userInfoLine"><span>계정명</span><strong>{{ user.email }}</strong></div>
|
|
<div class="userInfoLine"><span>닉네임</span><strong>{{ user.nickname || '미설정' }}</strong></div>
|
|
<div class="userInfoLine"><span>권한</span><strong>{{ props.roleLabelOf(user) }}</strong></div>
|
|
</div>
|
|
|
|
<div class="userCard__actions userCard__actions--compact">
|
|
<button
|
|
class="iconActionButton iconActionButton--danger"
|
|
type="button"
|
|
title="회원 삭제"
|
|
:disabled="!props.canDeleteUser(user)"
|
|
@click="props.openUserDeleteModal(user)"
|
|
>
|
|
<SvgIcon class="iconActionButton__icon" :src="props.deleteIcon" :size="18" />
|
|
</button>
|
|
<button
|
|
class="btn btn--ghost userSaveButton"
|
|
type="button"
|
|
:disabled="!props.canEditUserInfo(user)"
|
|
@click="props.openUserEditModal(user)"
|
|
>
|
|
회원 정보 수정
|
|
</button>
|
|
<button
|
|
class="btn btn--ghost userSaveButton"
|
|
type="button"
|
|
@click="props.openUserProfile(user)"
|
|
>
|
|
프로필 보기
|
|
</button>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</template>
|