360 lines
14 KiB
Vue
360 lines
14 KiB
Vue
<template>
|
|
<div>
|
|
<AppHeader />
|
|
|
|
<div class="container py-4">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
<!-- 헤더 -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h4 class="mb-0">
|
|
<i class="bi bi-pencil me-2"></i>사용자 수정
|
|
</h4>
|
|
<NuxtLink to="/admin/user" class="btn btn-outline-secondary">
|
|
<i class="bi bi-arrow-left me-1"></i>목록으로
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<div v-if="isLoading" class="text-center py-5">
|
|
<span class="spinner-border spinner-border-sm me-2"></span>로딩 중...
|
|
</div>
|
|
|
|
<template v-else-if="userInfo">
|
|
<!-- 기본 정보 폼 -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<strong>기본 정보</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<form @submit.prevent="saveUser">
|
|
<div class="row mb-3">
|
|
<label class="col-3 col-form-label">이름 <span class="text-danger">*</span></label>
|
|
<div class="col-9">
|
|
<input type="text" class="form-control" v-model="form.employeeName" required />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<label class="col-3 col-form-label">이메일</label>
|
|
<div class="col-9">
|
|
<input type="email" class="form-control" :value="userInfo.employeeEmail" disabled />
|
|
<small class="text-muted">이메일은 변경할 수 없습니다.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<label class="col-3 col-form-label">소속사</label>
|
|
<div class="col-9">
|
|
<select class="form-select" v-model="form.company">
|
|
<option value="">(선택)</option>
|
|
<option value="(주)터보소프트">(주)터보소프트</option>
|
|
<option value="(주)코쿤">(주)코쿤</option>
|
|
<option value="(주)오솔정보기술">(주)오솔정보기술</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<label class="col-3 col-form-label">직급</label>
|
|
<div class="col-9">
|
|
<select class="form-select" v-model="form.employeePosition">
|
|
<option value="">선택</option>
|
|
<optgroup label="일반">
|
|
<option value="인턴">인턴</option>
|
|
<option value="사원">사원</option>
|
|
<option value="주임">주임</option>
|
|
<option value="대리">대리</option>
|
|
<option value="과장">과장</option>
|
|
<option value="차장">차장</option>
|
|
<option value="부장">부장</option>
|
|
</optgroup>
|
|
<optgroup label="연구소">
|
|
<option value="연구원">연구원</option>
|
|
<option value="주임연구원">주임연구원</option>
|
|
<option value="선임연구원">선임연구원</option>
|
|
<option value="책임연구원">책임연구원</option>
|
|
<option value="수석연구원">수석연구원</option>
|
|
<option value="연구소장">연구소장</option>
|
|
</optgroup>
|
|
<optgroup label="임원">
|
|
<option value="이사">이사</option>
|
|
<option value="상무이사">상무이사</option>
|
|
<option value="전무이사">전무이사</option>
|
|
<option value="부사장">부사장</option>
|
|
<option value="사장">사장</option>
|
|
<option value="대표이사">대표이사</option>
|
|
</optgroup>
|
|
<optgroup label="기타">
|
|
<option value="팀장">팀장</option>
|
|
<option value="실장">실장</option>
|
|
<option value="본부장">본부장</option>
|
|
<option value="고문">고문</option>
|
|
<option value="감사">감사</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<label class="col-3 col-form-label">연락처</label>
|
|
<div class="col-9">
|
|
<input type="tel" class="form-control" v-model="form.employeePhone" placeholder="010-0000-0000" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<label class="col-3 col-form-label">입사일</label>
|
|
<div class="col-9">
|
|
<input type="date" class="form-control" v-model="form.joinDate" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<label class="col-3 col-form-label">상태</label>
|
|
<div class="col-9">
|
|
<div class="form-check form-switch">
|
|
<input type="checkbox" class="form-check-input" id="isActiveSwitch" v-model="form.isActive" />
|
|
<label class="form-check-label" for="isActiveSwitch">
|
|
{{ form.isActive ? '활성' : '비활성' }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-3" />
|
|
|
|
<div class="row mb-2">
|
|
<label class="col-3 col-form-label text-muted small">최초입력</label>
|
|
<div class="col-9">
|
|
<span class="form-control-plaintext small text-muted">
|
|
{{ formatDateTime(userInfo.createdAt) }}
|
|
<span v-if="userInfo.createdIp" class="ms-2"><code>{{ userInfo.createdIp }}</code></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<label class="col-3 col-form-label text-muted small">최종수정</label>
|
|
<div class="col-9">
|
|
<span class="form-control-plaintext small text-muted">
|
|
{{ formatDateTime(userInfo.updatedAt) }}
|
|
<span v-if="userInfo.updatedIp" class="ms-2"><code>{{ userInfo.updatedIp }}</code></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-end">
|
|
<NuxtLink to="/admin/user" class="btn btn-secondary me-2">취소</NuxtLink>
|
|
<button type="submit" class="btn btn-primary" :disabled="!canSave || isSaving">
|
|
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1"></span>
|
|
저장
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로그인 이력 -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<strong>로그인 이력</strong>
|
|
<small class="text-muted ms-2">(최근 20건)</small>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>로그인 시간</th>
|
|
<th>로그인 IP</th>
|
|
<th>로그아웃 시간</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="loginHistory.length === 0">
|
|
<td colspan="3" class="text-center text-muted py-3">로그인 이력이 없습니다.</td>
|
|
</tr>
|
|
<tr v-else v-for="h in loginHistory" :key="h.historyId">
|
|
<td>{{ formatDateTime(h.loginAt) }}</td>
|
|
<td><code>{{ h.loginIp || '-' }}</code></td>
|
|
<td>{{ h.logoutAt ? formatDateTime(h.logoutAt) : '-' }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 위험 영역 -->
|
|
<div class="card border-danger" v-if="!isSelf">
|
|
<div class="card-header bg-danger text-white">
|
|
<strong><i class="bi bi-exclamation-triangle me-2"></i>위험 영역</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-2">이 사용자를 삭제합니다. 주간보고가 있는 경우 비활성화 처리됩니다.</p>
|
|
<button class="btn btn-danger" @click="confirmDelete" :disabled="isDeleting">
|
|
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1"></span>
|
|
<i class="bi bi-trash me-1"></i>사용자 삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 삭제 확인 모달 -->
|
|
<div v-if="showDeleteModal" class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5)">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-danger text-white">
|
|
<h5 class="modal-title">삭제 확인</h5>
|
|
<button type="button" class="btn-close btn-close-white" @click="showDeleteModal = false"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-0"><strong>{{ userInfo?.employeeName }}</strong>님을 삭제하시겠습니까?</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" @click="showDeleteModal = false">취소</button>
|
|
<button type="button" class="btn btn-danger" @click="deleteUser" :disabled="isDeleting">
|
|
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1"></span>삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const { fetchCurrentUser, hasMenuAccess } = useAuth()
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
|
|
const employeeId = computed(() => route.params.id as string)
|
|
|
|
const isLoading = ref(true)
|
|
const isSaving = ref(false)
|
|
const isDeleting = ref(false)
|
|
const showDeleteModal = ref(false)
|
|
const currentUser = ref<any>(null)
|
|
const userInfo = ref<any>(null)
|
|
const loginHistory = ref<any[]>([])
|
|
|
|
const form = ref({
|
|
employeeName: '',
|
|
company: '',
|
|
employeePosition: '',
|
|
employeePhone: '',
|
|
joinDate: '',
|
|
isActive: true
|
|
})
|
|
|
|
const canSave = computed(() => form.value.employeeName.trim())
|
|
const isSelf = computed(() => currentUser.value?.employeeId === parseInt(employeeId.value))
|
|
|
|
onMounted(async () => {
|
|
const user = await fetchCurrentUser()
|
|
if (!user) {
|
|
router.push('/login')
|
|
return
|
|
}
|
|
|
|
currentUser.value = user
|
|
|
|
if (!hasMenuAccess('ADMIN_USER')) {
|
|
alert('접근 권한이 없습니다.')
|
|
router.push('/')
|
|
return
|
|
}
|
|
|
|
await loadUserInfo()
|
|
})
|
|
|
|
async function loadUserInfo() {
|
|
isLoading.value = true
|
|
try {
|
|
const res = await $fetch<any>(`/api/employee/${employeeId.value}/detail`)
|
|
userInfo.value = res.employee
|
|
loginHistory.value = res.loginHistory || []
|
|
|
|
// 폼에 기존 데이터 설정
|
|
form.value = {
|
|
employeeName: res.employee.employeeName || '',
|
|
company: res.employee.company || '',
|
|
employeePosition: res.employee.employeePosition || '',
|
|
employeePhone: res.employee.employeePhone || '',
|
|
joinDate: res.employee.joinDate ? res.employee.joinDate.split('T')[0] : '',
|
|
isActive: res.employee.isActive
|
|
}
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
alert('사용자 정보를 불러오는데 실패했습니다.')
|
|
router.push('/admin/user')
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function saveUser() {
|
|
if (!canSave.value) return
|
|
|
|
isSaving.value = true
|
|
try {
|
|
await $fetch(`/api/employee/${employeeId.value}/update`, {
|
|
method: 'PUT',
|
|
body: {
|
|
employeeName: form.value.employeeName,
|
|
company: form.value.company || null,
|
|
employeePosition: form.value.employeePosition || null,
|
|
employeePhone: form.value.employeePhone || null,
|
|
joinDate: form.value.joinDate || null,
|
|
isActive: form.value.isActive
|
|
}
|
|
})
|
|
|
|
alert('저장되었습니다.')
|
|
router.push('/admin/user')
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
alert(e.data?.message || '저장에 실패했습니다.')
|
|
} finally {
|
|
isSaving.value = false
|
|
}
|
|
}
|
|
|
|
function confirmDelete() {
|
|
showDeleteModal.value = true
|
|
}
|
|
|
|
async function deleteUser() {
|
|
isDeleting.value = true
|
|
try {
|
|
const response = await $fetch<any>(`/api/employee/${employeeId.value}/delete`, {
|
|
method: 'DELETE'
|
|
})
|
|
|
|
alert(response.message)
|
|
router.push('/admin/user')
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
alert(e.data?.message || '삭제에 실패했습니다.')
|
|
} finally {
|
|
isDeleting.value = false
|
|
showDeleteModal.value = false
|
|
}
|
|
}
|
|
|
|
function formatDateTime(dateStr: string) {
|
|
if (!dateStr) return '-'
|
|
const d = new Date(dateStr)
|
|
const year = d.getFullYear()
|
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
const hour = String(d.getHours()).padStart(2, '0')
|
|
const minute = String(d.getMinutes()).padStart(2, '0')
|
|
const second = String(d.getSeconds()).padStart(2, '0')
|
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
|
}
|
|
</script>
|