Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f8de5adf3 |
@@ -1,5 +1,9 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.51
|
||||||
|
- 관리자 리팩터링은 본문 분리 다음 단계에서 `회원 관리`처럼 모달과 부수 액션이 많은 영역을 composable로 떼어내는 편이 효과가 크다고 판단했다.
|
||||||
|
- 이 단계에서는 UI 문구나 사용자가 이미 손본 CSS를 다시 건드리기보다, 현재 동작을 유지한 채 책임 경계만 옮기는 쪽이 더 안전하다고 정리했다.
|
||||||
|
|
||||||
## 2026-04-02 v1.3.50
|
## 2026-04-02 v1.3.50
|
||||||
- 템플릿 요청 카드는 게임 이름/ID만 남기기보다 대표 썸네일까지 유지하는 편이 운영자가 요청 대상을 훨씬 빨리 구분할 수 있다고 정리했다.
|
- 템플릿 요청 카드는 게임 이름/ID만 남기기보다 대표 썸네일까지 유지하는 편이 운영자가 요청 대상을 훨씬 빨리 구분할 수 있다고 정리했다.
|
||||||
- 템플릿 요청의 `확인하기`는 단순히 해당 게임을 선택하는 동작이 아니라, 게임 관리 화면에서 요청 아이템 후보가 실제 작업 상태로 복원되어야 한다고 판단했다.
|
- 템플릿 요청의 `확인하기`는 단순히 해당 게임을 선택하는 동작이 아니라, 게임 관리 화면에서 요청 아이템 후보가 실제 작업 상태로 복원되어야 한다고 판단했다.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
## 중기 개선
|
## 중기 개선
|
||||||
- 관리자 URL 분리는 시작했으므로, 다음 단계에서는 `AdminView.vue` 단일 대형 파일을 섹션별 뷰/컴포저블로 쪼개고 직접 진입 시 선택 게임/요청 작업 상태 복원 범위도 함께 정리한다.
|
- 관리자 URL 분리는 시작했으므로, 다음 단계에서는 `AdminView.vue` 단일 대형 파일을 섹션별 뷰/컴포저블로 쪼개고 직접 진입 시 선택 게임/요청 작업 상태 복원 범위도 함께 정리한다.
|
||||||
- 관리자 본문 컴포넌트 분리는 시작했으므로, 다음 단계에서는 `AdminView.vue`에 남아 있는 상태/액션도 `useAdmin*` composable 단위로 분리해 실제 로직 결합도를 줄인다.
|
- 관리자 본문 컴포넌트 분리와 `게임/템플릿 요청/회원 관리` composable 분리는 시작했으므로, 다음 단계에서는 `아이템 관리`와 `목록 관리`도 같은 기준으로 옮기고 공통 모달 상태를 어느 계층에서 소유할지 정리한다.
|
||||||
- 관리자 게임/템플릿 요청 composable 분리는 시작했으므로, 다음 단계에서는 회원/아이템/목록 관리도 같은 기준으로 정리하고 공통 모달 상태를 어느 계층에서 소유할지 정리한다.
|
- 관리자 화면은 섹션 경로 분리까지 끝났으므로, 다음 단계에서는 `AdminView.vue`를 실제 레이아웃 뷰와 섹션별 라우트 컴포넌트로 더 쪼갤지 결정한다.
|
||||||
- 관리자 게임 아이템 순서 저장은 추가됐으므로, 다음 단계에서는 새 아이템 추가 직후 `자동 맨 앞 배치`와 `관리자 수동 고정 순서`의 우선순위를 실제 운영 흐름 기준으로 한 번 더 QA한다.
|
- 관리자 게임 아이템 순서 저장은 추가됐으므로, 다음 단계에서는 새 아이템 추가 직후 `자동 맨 앞 배치`와 `관리자 수동 고정 순서`의 우선순위를 실제 운영 흐름 기준으로 한 번 더 QA한다.
|
||||||
- 관리자 템플릿 요청 미리보기는 실제 완성본 iframe 방식과의 체감 차이를 마지막으로 한 번 더 QA한다.
|
- 관리자 템플릿 요청 미리보기는 실제 완성본 iframe 방식과의 체감 차이를 마지막으로 한 번 더 QA한다.
|
||||||
- 라이트모드/다크모드 2차 보정까지 반영했으므로, 남은 작업은 전체 화면을 실제 사용 흐름으로 돌려 보며 대비·명도·아이콘 가독성을 미세하게 QA하는 최종 테마 점검 단계로 가져간다.
|
- 라이트모드/다크모드 2차 보정까지 반영했으므로, 남은 작업은 전체 화면을 실제 사용 흐름으로 돌려 보며 대비·명도·아이콘 가독성을 미세하게 QA하는 최종 테마 점검 단계로 가져간다.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.51
|
||||||
|
- 관리자 리팩터링 3차로 회원 관리 액션을 `useAdminUsers` composable로 분리해, 아바타 변경, 회원 정보 수정, 비밀번호 초기화, 권한 변경, 삭제 모달 흐름을 `AdminView.vue` 밖으로 옮김.
|
||||||
|
- 따라서 관리자 메인 뷰는 섹션 연결과 공통 상태에 더 집중하고, 회원 관리 로직은 다른 관리자 영역과 같은 composable 분리 기준으로 맞추기 시작함.
|
||||||
|
- 이번 정리에서도 관리자 화면에 직접 반영돼 있던 텍스트와 게임 관리 CSS 수정분은 유지한 채 구조만 옮기도록 정리함.
|
||||||
|
|
||||||
## 2026-04-02 v1.3.50
|
## 2026-04-02 v1.3.50
|
||||||
- 관리자 `템플릿 요청 관리` 카드에서는 대표 썸네일을 다시 복구해 게임 이름/ID와 함께 요청 대상을 더 빠르게 식별할 수 있게 정리함.
|
- 관리자 `템플릿 요청 관리` 카드에서는 대표 썸네일을 다시 복구해 게임 이름/ID와 함께 요청 대상을 더 빠르게 식별할 수 있게 정리함.
|
||||||
- `확인하기` 후 게임을 불러오면서 요청 아이템 임시 목록이 비워지던 흐름을 수정하고, 신규 게임 생성 직후에도 요청 아이템이 기본 아이템 추가 미리보기에 유지되도록 보강함.
|
- `확인하기` 후 게임을 불러오면서 요청 아이템 임시 목록이 비워지던 흐름을 수정하고, 신규 게임 생성 직후에도 요청 아이템이 기본 아이템 추가 미리보기에 유지되도록 보강함.
|
||||||
|
|||||||
265
frontend/src/composables/useAdminUsers.js
Normal file
265
frontend/src/composables/useAdminUsers.js
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
export function useAdminUsers({
|
||||||
|
api,
|
||||||
|
auth,
|
||||||
|
users,
|
||||||
|
userQuery,
|
||||||
|
userSort,
|
||||||
|
userSortDirection,
|
||||||
|
userAvatarInputs,
|
||||||
|
modalTargetUser,
|
||||||
|
modalPasswordDraft,
|
||||||
|
modalRoleNextAdmin,
|
||||||
|
modalUserDraftEmail,
|
||||||
|
modalUserDraftNickname,
|
||||||
|
modalUserDraftIsAdmin,
|
||||||
|
userEditModalOpen,
|
||||||
|
userPasswordModalOpen,
|
||||||
|
userDeleteModalOpen,
|
||||||
|
userRoleModalOpen,
|
||||||
|
resetMessages,
|
||||||
|
refreshUsers,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
}) {
|
||||||
|
function setUserAvatarInput(userId, el) {
|
||||||
|
if (!userId) return
|
||||||
|
if (!el) {
|
||||||
|
delete userAvatarInputs.value[userId]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userAvatarInputs.value[userId] = el
|
||||||
|
}
|
||||||
|
|
||||||
|
const canManageModalRole = computed(() => {
|
||||||
|
if (!auth.user?.isPrimaryAdmin) return false
|
||||||
|
if (!modalTargetUser.value) return false
|
||||||
|
return !modalTargetUser.value.isPrimaryAdmin
|
||||||
|
})
|
||||||
|
|
||||||
|
const isUserEditDirty = computed(() => {
|
||||||
|
if (!modalTargetUser.value) return false
|
||||||
|
return (
|
||||||
|
modalUserDraftEmail.value.trim() !== (modalTargetUser.value.email || '') ||
|
||||||
|
modalUserDraftNickname.value.trim() !== (modalTargetUser.value.nickname || '') ||
|
||||||
|
!!modalUserDraftIsAdmin.value !== !!modalTargetUser.value.isAdmin
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function roleLabelOf(user) {
|
||||||
|
if (user?.isPrimaryAdmin) return '최고 관리자'
|
||||||
|
if (user?.isAdmin) return '운영자'
|
||||||
|
return '일반 회원'
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUserAvatarPicker(user) {
|
||||||
|
userAvatarInputs.value[user?.id]?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadUserAvatar(user, file, { remove = false } = {}) {
|
||||||
|
resetMessages()
|
||||||
|
if (!user?.id) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
user.isAvatarBusy = true
|
||||||
|
const data = await api.updateAdminUserAvatar(user.id, { file, removeAvatar: remove })
|
||||||
|
const updated = data.user
|
||||||
|
users.value = users.value.map((entry) =>
|
||||||
|
entry.id === updated.id
|
||||||
|
? {
|
||||||
|
...entry,
|
||||||
|
...updated,
|
||||||
|
isAvatarBusy: false,
|
||||||
|
}
|
||||||
|
: entry
|
||||||
|
)
|
||||||
|
if (modalTargetUser.value?.id === updated.id) {
|
||||||
|
modalTargetUser.value = { ...modalTargetUser.value, ...updated }
|
||||||
|
}
|
||||||
|
if (updated.id === auth.user?.id) await auth.refresh()
|
||||||
|
await refreshUsers()
|
||||||
|
success.value = remove ? '회원 썸네일을 삭제했어요.' : '회원 썸네일을 업데이트했어요.'
|
||||||
|
} catch (e) {
|
||||||
|
error.value = remove ? '회원 썸네일 삭제에 실패했어요.' : '회원 썸네일 변경에 실패했어요.'
|
||||||
|
} finally {
|
||||||
|
const target = users.value.find((entry) => entry.id === user.id)
|
||||||
|
if (target) target.isAvatarBusy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUserAvatarChange(user, event) {
|
||||||
|
const file = event.target.files && event.target.files[0] ? event.target.files[0] : null
|
||||||
|
event.target.value = ''
|
||||||
|
if (!file) return
|
||||||
|
await uploadUserAvatar(user, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeUserAvatar(user) {
|
||||||
|
if (!user?.avatarSrc) return
|
||||||
|
await uploadUserAvatar(user, null, { remove: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUserEditModal(user) {
|
||||||
|
resetMessages()
|
||||||
|
modalTargetUser.value = user ? { ...user } : null
|
||||||
|
modalUserDraftEmail.value = user?.email || ''
|
||||||
|
modalUserDraftNickname.value = user?.nickname || ''
|
||||||
|
modalUserDraftIsAdmin.value = !!user?.isAdmin
|
||||||
|
userEditModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUserEditModal() {
|
||||||
|
userEditModalOpen.value = false
|
||||||
|
modalTargetUser.value = null
|
||||||
|
modalUserDraftEmail.value = ''
|
||||||
|
modalUserDraftNickname.value = ''
|
||||||
|
modalUserDraftIsAdmin.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveUserEdit() {
|
||||||
|
resetMessages()
|
||||||
|
if (!modalTargetUser.value?.id) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await api.updateAdminUser(modalTargetUser.value.id, {
|
||||||
|
email: modalUserDraftEmail.value.trim(),
|
||||||
|
nickname: modalUserDraftNickname.value.trim(),
|
||||||
|
isAdmin: !!modalUserDraftIsAdmin.value,
|
||||||
|
})
|
||||||
|
const updated = data.user
|
||||||
|
users.value = users.value.map((entry) =>
|
||||||
|
entry.id === updated.id
|
||||||
|
? {
|
||||||
|
...entry,
|
||||||
|
...updated,
|
||||||
|
isAvatarBusy: entry.isAvatarBusy || false,
|
||||||
|
}
|
||||||
|
: entry
|
||||||
|
)
|
||||||
|
if (updated.id === auth.user?.id) await auth.refresh()
|
||||||
|
closeUserEditModal()
|
||||||
|
await refreshUsers()
|
||||||
|
success.value = '회원 정보를 저장했어요.'
|
||||||
|
} catch (e) {
|
||||||
|
error.value = '회원 정보 저장에 실패했어요.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUserPasswordModal(user) {
|
||||||
|
resetMessages()
|
||||||
|
modalTargetUser.value = user ? { ...user } : null
|
||||||
|
modalPasswordDraft.value = ''
|
||||||
|
userPasswordModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUserPasswordModal() {
|
||||||
|
userPasswordModalOpen.value = false
|
||||||
|
modalTargetUser.value = null
|
||||||
|
modalPasswordDraft.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function userDisplayName(user) {
|
||||||
|
return user?.nickname || user?.email?.split('@')[0] || '알 수 없음'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmUserPasswordReset() {
|
||||||
|
resetMessages()
|
||||||
|
if (!modalTargetUser.value?.id) return
|
||||||
|
|
||||||
|
const password = modalPasswordDraft.value.trim()
|
||||||
|
if (!password) {
|
||||||
|
error.value = '초기화할 비밀번호를 입력해주세요.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.updateAdminUserPassword(modalTargetUser.value.id, { password })
|
||||||
|
success.value = `${userDisplayName(modalTargetUser.value)} 계정 비밀번호를 초기화했어요.`
|
||||||
|
closeUserPasswordModal()
|
||||||
|
} catch (e) {
|
||||||
|
error.value = '비밀번호 초기화에 실패했어요.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUserDeleteModal(user) {
|
||||||
|
resetMessages()
|
||||||
|
modalTargetUser.value = user ? { ...user } : null
|
||||||
|
userDeleteModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUserDeleteModal() {
|
||||||
|
userDeleteModalOpen.value = false
|
||||||
|
modalTargetUser.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmUserDelete() {
|
||||||
|
resetMessages()
|
||||||
|
if (!modalTargetUser.value?.id) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deletingSelf = modalTargetUser.value.id === auth.user?.id
|
||||||
|
const deletedName = userDisplayName(modalTargetUser.value)
|
||||||
|
await api.deleteAdminUser(modalTargetUser.value.id)
|
||||||
|
users.value = users.value.filter((entry) => entry.id !== modalTargetUser.value.id)
|
||||||
|
closeUserDeleteModal()
|
||||||
|
success.value = `${deletedName} 계정을 삭제했어요.`
|
||||||
|
if (deletingSelf) await auth.refresh()
|
||||||
|
} catch (e) {
|
||||||
|
error.value = '회원 삭제에 실패했어요.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUserRoleModal(user, nextIsAdmin = !modalUserDraftIsAdmin.value) {
|
||||||
|
resetMessages()
|
||||||
|
modalTargetUser.value = user ? { ...user } : null
|
||||||
|
modalRoleNextAdmin.value = !!nextIsAdmin
|
||||||
|
userRoleModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUserRoleModal() {
|
||||||
|
userRoleModalOpen.value = false
|
||||||
|
if (!userEditModalOpen.value) modalTargetUser.value = null
|
||||||
|
modalRoleNextAdmin.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmUserRoleDraft() {
|
||||||
|
if (!modalTargetUser.value?.id) return
|
||||||
|
modalUserDraftIsAdmin.value = modalRoleNextAdmin.value
|
||||||
|
const targetLabel = modalRoleNextAdmin.value ? '운영자 권한을 저장 대기 상태로 반영했어요.' : '운영자 권한 해제를 저장 대기 상태로 반영했어요.'
|
||||||
|
closeUserRoleModal()
|
||||||
|
success.value = targetLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitUserFilters() {
|
||||||
|
refreshUsers({
|
||||||
|
q: userQuery.value,
|
||||||
|
sort: userSort.value,
|
||||||
|
direction: userSortDirection.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setUserAvatarInput,
|
||||||
|
canManageModalRole,
|
||||||
|
isUserEditDirty,
|
||||||
|
roleLabelOf,
|
||||||
|
openUserAvatarPicker,
|
||||||
|
onUserAvatarChange,
|
||||||
|
removeUserAvatar,
|
||||||
|
openUserEditModal,
|
||||||
|
closeUserEditModal,
|
||||||
|
saveUserEdit,
|
||||||
|
openUserPasswordModal,
|
||||||
|
closeUserPasswordModal,
|
||||||
|
confirmUserPasswordReset,
|
||||||
|
openUserDeleteModal,
|
||||||
|
closeUserDeleteModal,
|
||||||
|
confirmUserDelete,
|
||||||
|
openUserRoleModal,
|
||||||
|
closeUserRoleModal,
|
||||||
|
confirmUserRoleDraft,
|
||||||
|
submitUserFilters,
|
||||||
|
userDisplayName,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import AdminTierlistsSection from '../components/admin/AdminTierlistsSection.vue
|
|||||||
import AdminUsersSection from '../components/admin/AdminUsersSection.vue'
|
import AdminUsersSection from '../components/admin/AdminUsersSection.vue'
|
||||||
import { useAdminGameManager } from '../composables/useAdminGameManager'
|
import { useAdminGameManager } from '../composables/useAdminGameManager'
|
||||||
import { useAdminTemplateRequests } from '../composables/useAdminTemplateRequests'
|
import { useAdminTemplateRequests } from '../composables/useAdminTemplateRequests'
|
||||||
|
import { useAdminUsers } from '../composables/useAdminUsers'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import { useToast } from '../composables/useToast'
|
import { useToast } from '../composables/useToast'
|
||||||
|
|
||||||
@@ -646,83 +647,6 @@ async function refreshGames() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUserAvatarInput(userId, el) {
|
|
||||||
if (!userId) return
|
|
||||||
if (!el) {
|
|
||||||
delete userAvatarInputs.value[userId]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userAvatarInputs.value[userId] = el
|
|
||||||
}
|
|
||||||
|
|
||||||
const canManageModalRole = computed(() => {
|
|
||||||
if (!auth.user?.isPrimaryAdmin) return false
|
|
||||||
if (!modalTargetUser.value) return false
|
|
||||||
return !modalTargetUser.value.isPrimaryAdmin
|
|
||||||
})
|
|
||||||
|
|
||||||
const isUserEditDirty = computed(() => {
|
|
||||||
if (!modalTargetUser.value) return false
|
|
||||||
return (
|
|
||||||
modalUserDraftEmail.value.trim() !== (modalTargetUser.value.email || '') ||
|
|
||||||
modalUserDraftNickname.value.trim() !== (modalTargetUser.value.nickname || '') ||
|
|
||||||
!!modalUserDraftIsAdmin.value !== !!modalTargetUser.value.isAdmin
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function roleLabelOf(user) {
|
|
||||||
if (user?.isPrimaryAdmin) return '최고 관리자'
|
|
||||||
if (user?.isAdmin) return '운영자'
|
|
||||||
return '일반 회원'
|
|
||||||
}
|
|
||||||
|
|
||||||
function openUserAvatarPicker(user) {
|
|
||||||
userAvatarInputs.value[user?.id]?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadUserAvatar(user, file, { remove = false } = {}) {
|
|
||||||
resetMessages()
|
|
||||||
if (!user?.id) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
user.isAvatarBusy = true
|
|
||||||
const data = await api.updateAdminUserAvatar(user.id, { file, removeAvatar: remove })
|
|
||||||
const updated = data.user
|
|
||||||
users.value = users.value.map((entry) =>
|
|
||||||
entry.id === updated.id
|
|
||||||
? {
|
|
||||||
...entry,
|
|
||||||
...updated,
|
|
||||||
isAvatarBusy: false,
|
|
||||||
}
|
|
||||||
: entry
|
|
||||||
)
|
|
||||||
if (modalTargetUser.value?.id === updated.id) {
|
|
||||||
modalTargetUser.value = { ...modalTargetUser.value, ...updated }
|
|
||||||
}
|
|
||||||
if (updated.id === auth.user?.id) await auth.refresh()
|
|
||||||
await refreshUsers()
|
|
||||||
success.value = remove ? '회원 썸네일을 삭제했어요.' : '회원 썸네일을 업데이트했어요.'
|
|
||||||
} catch (e) {
|
|
||||||
error.value = remove ? '회원 썸네일 삭제에 실패했어요.' : '회원 썸네일 변경에 실패했어요.'
|
|
||||||
} finally {
|
|
||||||
const target = users.value.find((entry) => entry.id === user.id)
|
|
||||||
if (target) target.isAvatarBusy = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onUserAvatarChange(user, event) {
|
|
||||||
const file = event.target.files && event.target.files[0] ? event.target.files[0] : null
|
|
||||||
event.target.value = ''
|
|
||||||
if (!file) return
|
|
||||||
await uploadUserAvatar(user, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeUserAvatar(user) {
|
|
||||||
if (!user?.avatarSrc) return
|
|
||||||
await uploadUserAvatar(user, null, { remove: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroyFeaturedSortable() {
|
function destroyFeaturedSortable() {
|
||||||
if (featuredSortable.value) {
|
if (featuredSortable.value) {
|
||||||
featuredSortable.value.destroy()
|
featuredSortable.value.destroy()
|
||||||
@@ -892,6 +816,52 @@ const {
|
|||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
setUserAvatarInput,
|
||||||
|
canManageModalRole,
|
||||||
|
isUserEditDirty,
|
||||||
|
roleLabelOf,
|
||||||
|
openUserAvatarPicker,
|
||||||
|
onUserAvatarChange,
|
||||||
|
removeUserAvatar,
|
||||||
|
openUserEditModal,
|
||||||
|
closeUserEditModal,
|
||||||
|
saveUserEdit,
|
||||||
|
openUserPasswordModal,
|
||||||
|
closeUserPasswordModal,
|
||||||
|
confirmUserPasswordReset,
|
||||||
|
openUserDeleteModal,
|
||||||
|
closeUserDeleteModal,
|
||||||
|
confirmUserDelete,
|
||||||
|
openUserRoleModal,
|
||||||
|
closeUserRoleModal,
|
||||||
|
confirmUserRoleDraft,
|
||||||
|
submitUserFilters,
|
||||||
|
userDisplayName,
|
||||||
|
} = useAdminUsers({
|
||||||
|
api,
|
||||||
|
auth,
|
||||||
|
users,
|
||||||
|
userQuery,
|
||||||
|
userSort,
|
||||||
|
userSortDirection,
|
||||||
|
userAvatarInputs,
|
||||||
|
modalTargetUser,
|
||||||
|
modalPasswordDraft,
|
||||||
|
modalRoleNextAdmin,
|
||||||
|
modalUserDraftEmail,
|
||||||
|
modalUserDraftNickname,
|
||||||
|
modalUserDraftIsAdmin,
|
||||||
|
userEditModalOpen,
|
||||||
|
userPasswordModalOpen,
|
||||||
|
userDeleteModalOpen,
|
||||||
|
userRoleModalOpen,
|
||||||
|
resetMessages,
|
||||||
|
refreshUsers,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
function handleThumbFile(file) {
|
function handleThumbFile(file) {
|
||||||
const nextFile = file && (file.type || '').startsWith('image/') ? file : null
|
const nextFile = file && (file.type || '').startsWith('image/') ? file : null
|
||||||
@@ -1043,138 +1013,6 @@ async function removeGame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openUserEditModal(user) {
|
|
||||||
resetMessages()
|
|
||||||
modalTargetUser.value = user ? { ...user } : null
|
|
||||||
modalUserDraftEmail.value = user?.email || ''
|
|
||||||
modalUserDraftNickname.value = user?.nickname || ''
|
|
||||||
modalUserDraftIsAdmin.value = !!user?.isAdmin
|
|
||||||
userEditModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeUserEditModal() {
|
|
||||||
userEditModalOpen.value = false
|
|
||||||
modalTargetUser.value = null
|
|
||||||
modalUserDraftEmail.value = ''
|
|
||||||
modalUserDraftNickname.value = ''
|
|
||||||
modalUserDraftIsAdmin.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveUserEdit() {
|
|
||||||
resetMessages()
|
|
||||||
if (!modalTargetUser.value?.id) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await api.updateAdminUser(modalTargetUser.value.id, {
|
|
||||||
email: modalUserDraftEmail.value.trim(),
|
|
||||||
nickname: modalUserDraftNickname.value.trim(),
|
|
||||||
isAdmin: !!modalUserDraftIsAdmin.value,
|
|
||||||
})
|
|
||||||
const updated = data.user
|
|
||||||
users.value = users.value.map((entry) =>
|
|
||||||
entry.id === updated.id
|
|
||||||
? {
|
|
||||||
...entry,
|
|
||||||
...updated,
|
|
||||||
isAvatarBusy: entry.isAvatarBusy || false,
|
|
||||||
}
|
|
||||||
: entry
|
|
||||||
)
|
|
||||||
if (updated.id === auth.user?.id) await auth.refresh()
|
|
||||||
closeUserEditModal()
|
|
||||||
await refreshUsers()
|
|
||||||
success.value = '회원 정보를 저장했어요.'
|
|
||||||
} catch (e) {
|
|
||||||
error.value = '회원 정보 저장에 실패했어요.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openUserPasswordModal(user) {
|
|
||||||
resetMessages()
|
|
||||||
modalTargetUser.value = user ? { ...user } : null
|
|
||||||
modalPasswordDraft.value = ''
|
|
||||||
userPasswordModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeUserPasswordModal() {
|
|
||||||
userPasswordModalOpen.value = false
|
|
||||||
modalTargetUser.value = null
|
|
||||||
modalPasswordDraft.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmUserPasswordReset() {
|
|
||||||
resetMessages()
|
|
||||||
if (!modalTargetUser.value?.id) return
|
|
||||||
|
|
||||||
const password = modalPasswordDraft.value.trim()
|
|
||||||
if (!password) {
|
|
||||||
error.value = '초기화할 비밀번호를 입력해주세요.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.updateAdminUserPassword(modalTargetUser.value.id, { password })
|
|
||||||
success.value = `${userDisplayName(modalTargetUser.value)} 계정 비밀번호를 초기화했어요.`
|
|
||||||
closeUserPasswordModal()
|
|
||||||
} catch (e) {
|
|
||||||
error.value = '비밀번호 초기화에 실패했어요.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openUserDeleteModal(user) {
|
|
||||||
resetMessages()
|
|
||||||
modalTargetUser.value = user ? { ...user } : null
|
|
||||||
userDeleteModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeUserDeleteModal() {
|
|
||||||
userDeleteModalOpen.value = false
|
|
||||||
modalTargetUser.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmUserDelete() {
|
|
||||||
resetMessages()
|
|
||||||
if (!modalTargetUser.value?.id) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const deletingSelf = modalTargetUser.value.id === auth.user?.id
|
|
||||||
const deletedName = userDisplayName(modalTargetUser.value)
|
|
||||||
await api.deleteAdminUser(modalTargetUser.value.id)
|
|
||||||
users.value = users.value.filter((entry) => entry.id !== modalTargetUser.value.id)
|
|
||||||
closeUserDeleteModal()
|
|
||||||
success.value = `${deletedName} 계정을 삭제했어요.`
|
|
||||||
if (deletingSelf) await auth.refresh()
|
|
||||||
} catch (e) {
|
|
||||||
error.value = '회원 삭제에 실패했어요.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function openUserRoleModal(user, nextIsAdmin = !modalUserDraftIsAdmin.value) {
|
|
||||||
resetMessages()
|
|
||||||
modalTargetUser.value = user ? { ...user } : null
|
|
||||||
modalRoleNextAdmin.value = !!nextIsAdmin
|
|
||||||
userRoleModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeUserRoleModal() {
|
|
||||||
userRoleModalOpen.value = false
|
|
||||||
if (!userEditModalOpen.value) modalTargetUser.value = null
|
|
||||||
modalRoleNextAdmin.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmUserRoleDraft() {
|
|
||||||
if (!modalTargetUser.value?.id) return
|
|
||||||
modalUserDraftIsAdmin.value = modalRoleNextAdmin.value
|
|
||||||
const targetLabel = modalRoleNextAdmin.value ? '운영자 권한을 저장 대기 상태로 반영했어요.' : '운영자 권한 해제를 저장 대기 상태로 반영했어요.'
|
|
||||||
closeUserRoleModal()
|
|
||||||
success.value = targetLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitUserFilters() {
|
|
||||||
refreshUsers()
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitCustomItemSearch() {
|
function submitCustomItemSearch() {
|
||||||
customItemPage.value = 1
|
customItemPage.value = 1
|
||||||
refreshCustomItems()
|
refreshCustomItems()
|
||||||
@@ -1572,10 +1410,6 @@ function userAvatarUrl(user) {
|
|||||||
return user?.avatarSrc ? toApiUrl(user.avatarSrc) : ''
|
return user?.avatarSrc ? toApiUrl(user.avatarSrc) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function userDisplayName(user) {
|
|
||||||
return user?.nickname || user?.email?.split('@')[0] || '알 수 없음'
|
|
||||||
}
|
|
||||||
|
|
||||||
function userAvatarFallback(user) {
|
function userAvatarFallback(user) {
|
||||||
return (user?.email?.trim()?.[0] || '?').toUpperCase()
|
return (user?.email?.trim()?.[0] || '?').toUpperCase()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user