489 lines
20 KiB
Vue
489 lines
20 KiB
Vue
|
|
<template>
|
||
|
|
<div class="p-8 max-w-6xl mx-auto space-y-8 select-none" data-theme="dark">
|
||
|
|
<!-- Header -->
|
||
|
|
<div class="flex items-center justify-between pb-6 border-b border-base-content/10">
|
||
|
|
<div class="flex items-center gap-4">
|
||
|
|
<div class="bg-secondary/10 p-3 rounded-2xl text-secondary shadow-lg shadow-secondary/5">
|
||
|
|
<UsersIcon :size="28" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h1 class="text-3xl font-black tracking-tight">회원 및 권한 관리</h1>
|
||
|
|
<p class="text-sm opacity-60 mt-1">회원가입 승인 대기 처리 및 회원별 접근 가능한 카테고리를 설정합니다.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
|
|
<!-- Left / Top: Approval Pending List -->
|
||
|
|
<div class="lg:col-span-2 space-y-6">
|
||
|
|
<div class="bg-base-200 border border-base-content/5 rounded-3xl p-6 shadow-xl space-y-4">
|
||
|
|
<h2 class="text-lg font-bold flex items-center gap-2">
|
||
|
|
<UserPlusIcon :size="20" class="text-secondary" />
|
||
|
|
가입 승인 대기 목록
|
||
|
|
<div class="badge badge-secondary badge-sm font-black">{{ pendingUsers.length }}</div>
|
||
|
|
</h2>
|
||
|
|
|
||
|
|
<div v-if="pendingLoading" class="flex justify-center py-8">
|
||
|
|
<span class="loading loading-spinner loading-md text-secondary"></span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-else-if="pendingUsers.length === 0" class="text-center py-10 border border-dashed border-base-content/10 rounded-2xl">
|
||
|
|
<p class="opacity-55 text-sm font-bold">승인 대기 중인 회원가입 신청이 없습니다.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-else class="space-y-3 max-h-80 overflow-y-auto pr-1">
|
||
|
|
<div
|
||
|
|
v-for="user in pendingUsers"
|
||
|
|
:key="user.username"
|
||
|
|
class="flex flex-col sm:flex-row sm:items-center justify-between p-4 bg-base-100 border border-base-content/5 rounded-2xl gap-4"
|
||
|
|
>
|
||
|
|
<div class="space-y-1">
|
||
|
|
<div class="flex items-center gap-2 flex-wrap">
|
||
|
|
<span class="font-black text-sm whitespace-nowrap">{{ user.nickname }}</span>
|
||
|
|
<span class="text-xs opacity-50 font-mono whitespace-nowrap">@{{ user.username }}</span>
|
||
|
|
<span
|
||
|
|
class="badge badge-xs font-bold whitespace-nowrap"
|
||
|
|
:class="user.apiKey ? 'badge-info text-info-content' : 'badge-neutral opacity-60'"
|
||
|
|
>
|
||
|
|
{{ user.apiKey ? 'API 키 등록됨' : 'API 키 없음' }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div class="text-xs opacity-75 flex flex-wrap gap-x-4">
|
||
|
|
<span class="whitespace-nowrap">연락처: {{ user.phone }}</span>
|
||
|
|
<span class="whitespace-nowrap">신청일: {{ formatDate(user.createdAt) }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="flex items-center gap-2 self-end sm:self-center shrink-0">
|
||
|
|
<button @click="handleApprove(user.username)" class="btn btn-success btn-sm font-bold rounded-xl px-4 whitespace-nowrap">
|
||
|
|
승인
|
||
|
|
</button>
|
||
|
|
<button @click="handleReject(user.username)" class="btn btn-error btn-sm btn-ghost hover:btn-error font-bold rounded-xl px-4 whitespace-nowrap">
|
||
|
|
거절
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- All Users List -->
|
||
|
|
<div class="bg-base-200 border border-base-content/5 rounded-3xl p-6 shadow-xl space-y-4">
|
||
|
|
<h2 class="text-lg font-bold flex items-center gap-2">
|
||
|
|
<ListIcon :size="20" class="text-secondary" />
|
||
|
|
전체 회원 목록
|
||
|
|
<div class="badge badge-secondary badge-sm font-black">{{ allUsers.length }}</div>
|
||
|
|
</h2>
|
||
|
|
|
||
|
|
<div v-if="usersLoading" class="flex justify-center py-8">
|
||
|
|
<span class="loading loading-spinner loading-md text-secondary"></span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-else class="overflow-x-auto">
|
||
|
|
<table class="table table-zebra w-full text-left">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th class="bg-transparent border-b border-base-content/5 text-xs font-black uppercase opacity-60 whitespace-nowrap">사용자</th>
|
||
|
|
<th class="bg-transparent border-b border-base-content/5 text-xs font-black uppercase opacity-60 whitespace-nowrap">연락처</th>
|
||
|
|
<th class="bg-transparent border-b border-base-content/5 text-xs font-black uppercase opacity-60 whitespace-nowrap">API 키</th>
|
||
|
|
<th class="bg-transparent border-b border-base-content/5 text-xs font-black uppercase opacity-60 whitespace-nowrap">상태</th>
|
||
|
|
<th class="bg-transparent border-b border-base-content/5 text-xs font-black uppercase opacity-60 text-center whitespace-nowrap">작업</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr
|
||
|
|
v-for="user in allUsers"
|
||
|
|
:key="user.username"
|
||
|
|
class="hover:bg-base-100/30 cursor-pointer"
|
||
|
|
:class="selectedUser?.username === user.username ? 'bg-secondary/10 hover:bg-secondary/15 font-bold' : ''"
|
||
|
|
@click="selectUser(user)"
|
||
|
|
>
|
||
|
|
<td class="border-b border-base-content/5 whitespace-nowrap">
|
||
|
|
<div class="font-bold text-sm">{{ user.nickname }}</div>
|
||
|
|
<div class="text-xs opacity-50 font-mono">@{{ user.username }}</div>
|
||
|
|
</td>
|
||
|
|
<td class="border-b border-base-content/5 text-xs font-semibold whitespace-nowrap">{{ user.phone }}</td>
|
||
|
|
<td class="border-b border-base-content/5 whitespace-nowrap">
|
||
|
|
<span
|
||
|
|
class="badge badge-xs font-bold"
|
||
|
|
:class="user.apiKey ? 'badge-info text-info-content' : 'badge-neutral opacity-40'"
|
||
|
|
>
|
||
|
|
{{ user.apiKey ? '등록됨' : '미등록' }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td class="border-b border-base-content/5 whitespace-nowrap">
|
||
|
|
<span
|
||
|
|
class="badge badge-sm font-black uppercase whitespace-nowrap"
|
||
|
|
:class="{
|
||
|
|
'badge-success': user.status === 'approved',
|
||
|
|
'badge-warning': user.status === 'pending',
|
||
|
|
'badge-error': user.status === 'rejected'
|
||
|
|
}"
|
||
|
|
>
|
||
|
|
{{ user.status === 'approved' ? '승인완료' : user.status === 'pending' ? '대기중' : '거절됨' }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td class="border-b border-base-content/5 text-center whitespace-nowrap space-x-1">
|
||
|
|
<button
|
||
|
|
@click.stop="selectUser(user)"
|
||
|
|
class="btn btn-xs btn-outline btn-secondary font-bold rounded-lg whitespace-nowrap"
|
||
|
|
:class="selectedUser?.username === user.username ? 'btn-active' : ''"
|
||
|
|
>
|
||
|
|
권한 설정
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
v-if="user.username !== 'admin'"
|
||
|
|
@click.stop="openEditModal(user)"
|
||
|
|
class="btn btn-xs btn-warning btn-outline font-bold rounded-lg whitespace-nowrap"
|
||
|
|
>
|
||
|
|
수정
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
v-if="user.username !== 'admin'"
|
||
|
|
@click.stop="handleDeleteUser(user.username)"
|
||
|
|
class="btn btn-xs btn-error btn-outline font-bold rounded-lg whitespace-nowrap"
|
||
|
|
>
|
||
|
|
탈퇴
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Right/Bottom: Category Permission Configuration -->
|
||
|
|
<div class="lg:col-span-1">
|
||
|
|
<div class="bg-base-200 border border-base-content/5 rounded-3xl p-6 shadow-xl space-y-6 sticky top-8">
|
||
|
|
<div class="pb-4 border-b border-base-content/10">
|
||
|
|
<h2 class="text-lg font-bold flex items-center gap-2">
|
||
|
|
<ShieldCheckIcon :size="20" class="text-secondary" />
|
||
|
|
카테고리 열람 권한 설정
|
||
|
|
</h2>
|
||
|
|
<p class="text-xs opacity-60 mt-1">목록에서 사용자를 선택하여 허용할 카테고리를 지정하세요.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-if="!selectedUser" class="text-center py-16 opacity-40">
|
||
|
|
<UserCheckIcon :size="48" class="mx-auto mb-3" />
|
||
|
|
<p class="text-sm font-bold">선택된 사용자가 없습니다.</p>
|
||
|
|
<p class="text-xs mt-1">좌측 목록에서 회원을 선택해 주세요.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-else class="space-y-6">
|
||
|
|
<!-- User Header -->
|
||
|
|
<div class="bg-base-300 p-4 rounded-2xl border border-base-content/5">
|
||
|
|
<div class="text-xs font-black uppercase tracking-widest opacity-60 mb-1">선택된 사용자</div>
|
||
|
|
<div class="flex items-baseline gap-2">
|
||
|
|
<span class="font-black text-lg text-secondary">{{ selectedUser.nickname }}</span>
|
||
|
|
<span class="text-xs opacity-50 font-mono">@{{ selectedUser.username }}</span>
|
||
|
|
</div>
|
||
|
|
<div v-if="selectedUser.username === 'admin'" class="text-xs text-info font-black mt-2">
|
||
|
|
* 관리자는 모든 카테고리에 상시 접근 가능합니다.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Categories Checklist -->
|
||
|
|
<div v-if="selectedUser.username !== 'admin'" class="space-y-4">
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<span class="text-sm font-bold opacity-80">허용할 카테고리 목록</span>
|
||
|
|
<div class="flex gap-2">
|
||
|
|
<button @click="selectAllCategories" class="btn btn-xs btn-ghost text-[10px] font-bold">전체 선택</button>
|
||
|
|
<button @click="deselectAllCategories" class="btn btn-xs btn-ghost text-[10px] font-bold">전체 해제</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="space-y-1.5 bg-base-300/40 p-4 rounded-2xl border border-base-content/5 max-h-72 overflow-y-auto">
|
||
|
|
<label
|
||
|
|
v-for="cat in availableCategories"
|
||
|
|
:key="cat.name"
|
||
|
|
class="flex items-center gap-3 hover:bg-base-100/40 px-3 py-2.5 rounded-xl cursor-pointer transition-colors h-14 border border-transparent hover:border-base-content/5"
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
:value="cat.name"
|
||
|
|
v-model="allowedCategories"
|
||
|
|
class="checkbox checkbox-sm checkbox-secondary shrink-0"
|
||
|
|
/>
|
||
|
|
<div class="flex flex-col text-left min-w-0 flex-1">
|
||
|
|
<span class="text-sm font-bold truncate whitespace-nowrap block" :title="cat.name">{{ cat.name }}</span>
|
||
|
|
<span class="text-[10px] opacity-50 font-bold mt-0.5">문항수: {{ cat.count }}개</span>
|
||
|
|
</div>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Save Button -->
|
||
|
|
<button
|
||
|
|
@click="savePermissions"
|
||
|
|
:disabled="savingPermissions"
|
||
|
|
class="btn btn-secondary w-full font-bold text-base rounded-2xl"
|
||
|
|
>
|
||
|
|
<span v-if="savingPermissions" class="loading loading-spinner"></span>
|
||
|
|
{{ savingPermissions ? '저장 중...' : '권한 설정 저장' }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-else class="text-center py-10 text-xs opacity-60">
|
||
|
|
관리자 계정은 권한 제어 대상이 아닙니다.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Edit User Modal (Admin Force Edit) -->
|
||
|
|
<div v-if="showEditModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||
|
|
<div class="bg-base-200 border border-base-content/10 w-full max-w-md p-6 sm:p-8 rounded-3xl shadow-2xl relative space-y-6">
|
||
|
|
<button @click="closeEditModal" class="btn btn-sm btn-circle btn-ghost absolute right-4 top-4">✕</button>
|
||
|
|
|
||
|
|
<div class="flex items-center gap-3">
|
||
|
|
<div class="bg-warning/10 p-2.5 rounded-2xl text-warning">
|
||
|
|
<UsersIcon :size="24" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h3 class="text-xl font-black">회원 정보 강제 수정</h3>
|
||
|
|
<p class="text-xs opacity-60">관리자 권한으로 회원의 정보를 수정합니다.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form @submit.prevent="handleUpdateUserInfo" class="space-y-4">
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label py-1">
|
||
|
|
<span class="label-text font-bold text-xs opacity-80">대상 계정명</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
:value="editingUser?.username"
|
||
|
|
type="text"
|
||
|
|
disabled
|
||
|
|
class="input input-bordered input-sm w-full bg-base-300 font-mono py-4 cursor-not-allowed"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label py-1">
|
||
|
|
<span class="label-text font-bold text-xs opacity-80">닉네임</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
v-model="editNickname"
|
||
|
|
type="text"
|
||
|
|
required
|
||
|
|
placeholder="닉네임 입력"
|
||
|
|
class="input input-bordered input-sm w-full bg-base-100/50 focus:input-warning py-4"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label py-1">
|
||
|
|
<span class="label-text font-bold text-xs opacity-80">연락처</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
v-model="editPhone"
|
||
|
|
type="text"
|
||
|
|
required
|
||
|
|
placeholder="연락처 입력"
|
||
|
|
class="input input-bordered input-sm w-full bg-base-100/50 focus:input-warning py-4"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label py-1">
|
||
|
|
<span class="label-text font-bold text-xs opacity-80">새 비밀번호 강제 변경 (선택사항)</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
v-model="editPassword"
|
||
|
|
type="password"
|
||
|
|
placeholder="미기입 시 기존 비번 유지"
|
||
|
|
class="input input-bordered input-sm w-full bg-base-100/50 focus:input-warning py-4"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
:disabled="updatingUser"
|
||
|
|
class="btn btn-warning btn-sm w-full font-bold py-2.5 h-auto rounded-xl mt-4"
|
||
|
|
>
|
||
|
|
<span v-if="updatingUser" class="loading loading-spinner loading-xs mr-1"></span>
|
||
|
|
회원 정보 강제 저장
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, onMounted } from 'vue'
|
||
|
|
import { UsersIcon, UserPlusIcon, ListIcon, ShieldCheckIcon, UserCheckIcon } from 'lucide-vue-next'
|
||
|
|
import { QuizService, CategoryInfo, User } from '../../bindings/changeme'
|
||
|
|
import { UpdateUserInfoForce, DeleteUser } from '../../bindings/changeme/quizservice'
|
||
|
|
|
||
|
|
const pendingUsers = ref<User[]>([])
|
||
|
|
const allUsers = ref<User[]>([])
|
||
|
|
const availableCategories = ref<CategoryInfo[]>([])
|
||
|
|
|
||
|
|
const pendingLoading = ref(false)
|
||
|
|
const usersLoading = ref(false)
|
||
|
|
const savingPermissions = ref(false)
|
||
|
|
|
||
|
|
const selectedUser = ref<User | null>(null)
|
||
|
|
const allowedCategories = ref<string[]>([])
|
||
|
|
|
||
|
|
// Admin force edit state
|
||
|
|
const showEditModal = ref(false)
|
||
|
|
const editingUser = ref<User | null>(null)
|
||
|
|
const editNickname = ref('')
|
||
|
|
const editPhone = ref('')
|
||
|
|
const editPassword = ref('')
|
||
|
|
const updatingUser = ref(false)
|
||
|
|
|
||
|
|
async function fetchPending() {
|
||
|
|
pendingLoading.value = true
|
||
|
|
try {
|
||
|
|
const list = await QuizService.GetPendingRegistrations()
|
||
|
|
pendingUsers.value = list || []
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to fetch pending list:', err)
|
||
|
|
} finally {
|
||
|
|
pendingLoading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchUsers() {
|
||
|
|
usersLoading.value = true
|
||
|
|
try {
|
||
|
|
const list = await QuizService.GetUsers()
|
||
|
|
allUsers.value = list || []
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to fetch users:', err)
|
||
|
|
} finally {
|
||
|
|
usersLoading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchCategories() {
|
||
|
|
try {
|
||
|
|
const list = await QuizService.GetCategories()
|
||
|
|
availableCategories.value = list || []
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to fetch categories:', err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleApprove(username: string) {
|
||
|
|
try {
|
||
|
|
await QuizService.ApproveRegistration(username)
|
||
|
|
await fetchPending()
|
||
|
|
await fetchUsers()
|
||
|
|
} catch (err) {
|
||
|
|
alert(`승인 처리 실패: ${err}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleReject(username: string) {
|
||
|
|
if (!confirm(`@${username} 회원의 가입 신청을 거절하시겠습니까?`)) return
|
||
|
|
try {
|
||
|
|
await QuizService.RejectRegistration(username)
|
||
|
|
await fetchPending()
|
||
|
|
await fetchUsers()
|
||
|
|
} catch (err) {
|
||
|
|
alert(`거절 처리 실패: ${err}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function selectUser(user: User) {
|
||
|
|
selectedUser.value = user
|
||
|
|
allowedCategories.value = []
|
||
|
|
if (user.username === 'admin') return
|
||
|
|
|
||
|
|
try {
|
||
|
|
const list = await QuizService.GetUserAllowedCategories(user.username)
|
||
|
|
allowedCategories.value = list || []
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to fetch user permissions:', err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectAllCategories() {
|
||
|
|
allowedCategories.value = availableCategories.value.map(c => c.name)
|
||
|
|
}
|
||
|
|
|
||
|
|
function deselectAllCategories() {
|
||
|
|
allowedCategories.value = []
|
||
|
|
}
|
||
|
|
|
||
|
|
async function savePermissions() {
|
||
|
|
if (!selectedUser.value) return
|
||
|
|
|
||
|
|
savingPermissions.value = true
|
||
|
|
try {
|
||
|
|
await QuizService.SetUserAllowedCategories(selectedUser.value.username, allowedCategories.value)
|
||
|
|
alert('권한이 성공적으로 저장되었습니다.')
|
||
|
|
} catch (err) {
|
||
|
|
alert(`권한 저장 실패: ${err}`)
|
||
|
|
} finally {
|
||
|
|
savingPermissions.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDate(dateStr: string) {
|
||
|
|
if (!dateStr) return ''
|
||
|
|
return dateStr.split(' ')[0] // Simple YYYY-MM-DD
|
||
|
|
}
|
||
|
|
|
||
|
|
function openEditModal(user: User) {
|
||
|
|
editingUser.value = user
|
||
|
|
editNickname.value = user.nickname
|
||
|
|
editPhone.value = user.phone
|
||
|
|
editPassword.value = ''
|
||
|
|
showEditModal.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeEditModal() {
|
||
|
|
showEditModal.value = false
|
||
|
|
editingUser.value = null
|
||
|
|
editNickname.value = ''
|
||
|
|
editPhone.value = ''
|
||
|
|
editPassword.value = ''
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleUpdateUserInfo() {
|
||
|
|
if (!editingUser.value) return
|
||
|
|
|
||
|
|
updatingUser.value = true
|
||
|
|
try {
|
||
|
|
await UpdateUserInfoForce(
|
||
|
|
editingUser.value.username,
|
||
|
|
editNickname.value.trim(),
|
||
|
|
editPhone.value.trim(),
|
||
|
|
editPassword.value.trim()
|
||
|
|
)
|
||
|
|
alert('회원 정보가 강제로 수정되었습니다.')
|
||
|
|
closeEditModal()
|
||
|
|
await fetchUsers()
|
||
|
|
} catch (err: any) {
|
||
|
|
alert(`정보 수정 실패: ${err.message || String(err)}`)
|
||
|
|
} finally {
|
||
|
|
updatingUser.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleDeleteUser(username: string) {
|
||
|
|
if (!confirm(`@${username} 회원을 탈퇴(완전 삭제) 처리하시겠습니까? 데이터가 유실되며 되돌릴 수 없습니다.`)) return
|
||
|
|
|
||
|
|
try {
|
||
|
|
await DeleteUser(username)
|
||
|
|
alert('회원이 성공적으로 탈퇴 처리되었습니다.')
|
||
|
|
if (selectedUser.value?.username === username) {
|
||
|
|
selectedUser.value = null
|
||
|
|
}
|
||
|
|
await fetchUsers()
|
||
|
|
} catch (err: any) {
|
||
|
|
alert(`탈퇴 처리 실패: ${err.message || String(err)}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
fetchPending()
|
||
|
|
fetchUsers()
|
||
|
|
fetchCategories()
|
||
|
|
})
|
||
|
|
</script>
|