활동로그 추가
This commit is contained in:
@@ -1,7 +1,15 @@
|
|||||||
const { Model, DataTypes } = require('sequelize');
|
const { Model, DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../config/database');
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
class ActivityLog extends Model {}
|
class ActivityLog extends Model {
|
||||||
|
static associate(models) {
|
||||||
|
// User 모델과의 연관 관계 설정
|
||||||
|
ActivityLog.belongsTo(models.User, {
|
||||||
|
foreignKey: 'userId',
|
||||||
|
as: 'User'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ActivityLog.init({
|
ActivityLog.init({
|
||||||
id: {
|
id: {
|
||||||
|
|||||||
@@ -747,6 +747,7 @@ router.get('/activity-logs', async (req, res) => {
|
|||||||
const logs = await ActivityLog.findAndCountAll({
|
const logs = await ActivityLog.findAndCountAll({
|
||||||
include: [{
|
include: [{
|
||||||
model: User,
|
model: User,
|
||||||
|
as: 'User',
|
||||||
attributes: ['username', 'name']
|
attributes: ['username', 'name']
|
||||||
}],
|
}],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
|
|||||||
Generated
+518
-520
File diff suppressed because it is too large
Load Diff
@@ -258,6 +258,13 @@ const adminMenuItems = ref([
|
|||||||
command: () => {
|
command: () => {
|
||||||
router.push('/admin/subscriptions');
|
router.push('/admin/subscriptions');
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '활동 로그',
|
||||||
|
icon: 'pi pi-history',
|
||||||
|
command: () => {
|
||||||
|
router.push('/admin/activity-logs');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ dayjs.extend(utc)
|
|||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.tz.setDefault('Asia/Seoul')
|
dayjs.tz.setDefault('Asia/Seoul')
|
||||||
|
|
||||||
|
// dayjs를 전역 객체로 등록
|
||||||
|
window.dayjs = dayjs
|
||||||
|
|
||||||
// PrimeVue
|
// PrimeVue
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
import Aura from '@primeuix/themes/aura';
|
import Aura from '@primeuix/themes/aura';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const AdminDashboard = () => import('@/views/admin/Dashboard.vue');
|
|||||||
const ClubManagement = () => import('@/views/admin/ClubManagement.vue');
|
const ClubManagement = () => import('@/views/admin/ClubManagement.vue');
|
||||||
const UserManagement = () => import('@/views/admin/UserManagement.vue');
|
const UserManagement = () => import('@/views/admin/UserManagement.vue');
|
||||||
const SubscriptionManagement = () => import('@/views/admin/SubscriptionManagement.vue');
|
const SubscriptionManagement = () => import('@/views/admin/SubscriptionManagement.vue');
|
||||||
|
const ActivityLogManagement = () => import('@/views/admin/ActivityLogManagement.vue');
|
||||||
|
|
||||||
// 클럽 뷰
|
// 클럽 뷰
|
||||||
const ClubDashboard = () => import('@/views/club/Dashboard.vue');
|
const ClubDashboard = () => import('@/views/club/Dashboard.vue');
|
||||||
@@ -76,6 +77,12 @@ const routes = [
|
|||||||
component: SubscriptionManagement,
|
component: SubscriptionManagement,
|
||||||
meta: { requiresAuth: true, requiresAdmin: true }
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/activity-logs',
|
||||||
|
name: 'ActivityLogManagement',
|
||||||
|
component: ActivityLogManagement,
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
|
||||||
// 클럽 경로
|
// 클럽 경로
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -332,6 +332,16 @@ const adminService = {
|
|||||||
return apiClient.get('/api/admin/activities/recent');
|
return apiClient.get('/api/admin/activities/recent');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활동 로그 목록 조회
|
||||||
|
* @param {Number} page - 페이지 번호
|
||||||
|
* @param {Number} limit - 페이지당 활동 로그 수
|
||||||
|
* @returns {Promise} - 활동 로그 목록
|
||||||
|
*/
|
||||||
|
getActivityLogs(page = 1, limit = 20) {
|
||||||
|
return apiClient.get(`/api/admin/activity-logs?page=${page}&limit=${limit}`);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 통계 데이터 조회
|
* 통계 데이터 조회
|
||||||
* @returns {Promise} - 통계 데이터
|
* @returns {Promise} - 통계 데이터
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<div class="activity-log-container">
|
||||||
|
<div class="header">
|
||||||
|
<h2>활동 로그</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:value="logs"
|
||||||
|
:loading="loading"
|
||||||
|
:paginator="true"
|
||||||
|
:rows="rowsPerPage"
|
||||||
|
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
|
currentPageReportTemplate="{first}에서 {last}까지 {totalRecords} 중"
|
||||||
|
responsiveLayout="scroll"
|
||||||
|
stripedRows
|
||||||
|
:sortField="'createdAt'"
|
||||||
|
:sortOrder="-1"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:lazy="true"
|
||||||
|
@page="onPageChange"
|
||||||
|
>
|
||||||
|
<Column field="id" header="ID" sortable style="width: 5%"></Column>
|
||||||
|
<Column field="user.name" header="사용자" sortable style="width: 15%">
|
||||||
|
<template #body="slotProps">
|
||||||
|
{{ slotProps.data.user ? slotProps.data.user.name : '-' }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="type" header="활동 유형" sortable style="width: 15%">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Badge :value="getActionTypeName(slotProps.data.type)" :severity="getActionTypeSeverity(slotProps.data.type)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="description" header="설명" style="width: 50%"></Column>
|
||||||
|
<Column field="createdAt" header="시간" sortable style="width: 15%">
|
||||||
|
<template #body="slotProps">
|
||||||
|
{{ formatDate(slotProps.data.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<Toast />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import adminService from '@/services/adminService';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// 상태 변수
|
||||||
|
const logs = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const rowsPerPage = ref(20);
|
||||||
|
|
||||||
|
// 페이지 로드 시 활동 로그 목록 가져오기
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchLogs();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 활동 로그 목록 가져오기
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await adminService.getActivityLogs(currentPage.value, rowsPerPage.value);
|
||||||
|
logs.value = response.data.logs || [];
|
||||||
|
totalRecords.value = response.data.totalItems || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('활동 로그 목록을 가져오는 중 오류 발생:', error);
|
||||||
|
toast.add({ severity: 'error', summary: '오류', detail: '활동 로그 목록을 가져오는 중 오류가 발생했습니다.', life: 3000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 페이지 변경 이벤트 핸들러
|
||||||
|
const onPageChange = (event) => {
|
||||||
|
// PrimeVue DataTable은 0부터 시작하는 페이지 인덱스를 사용하므로 1을 더해줍니다
|
||||||
|
currentPage.value = event.page + 1;
|
||||||
|
rowsPerPage.value = event.rows;
|
||||||
|
fetchLogs();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 활동 유형 이름 가져오기
|
||||||
|
const getActionTypeName = (actionType) => {
|
||||||
|
switch (actionType) {
|
||||||
|
case 'LOGIN':
|
||||||
|
return '로그인';
|
||||||
|
case 'LOGOUT':
|
||||||
|
return '로그아웃';
|
||||||
|
case 'CREATE_USER':
|
||||||
|
return '사용자 생성';
|
||||||
|
case 'UPDATE_USER':
|
||||||
|
return '사용자 수정';
|
||||||
|
case 'DELETE_USER':
|
||||||
|
return '사용자 삭제';
|
||||||
|
case 'CREATE_CLUB':
|
||||||
|
return '클럽 생성';
|
||||||
|
case 'UPDATE_CLUB':
|
||||||
|
return '클럽 수정';
|
||||||
|
case 'DELETE_CLUB':
|
||||||
|
return '클럽 삭제';
|
||||||
|
case 'ADD_CLUB_MEMBER':
|
||||||
|
return '클럽 회원 추가';
|
||||||
|
case 'UPDATE_CLUB_MEMBER':
|
||||||
|
return '클럽 회원 수정';
|
||||||
|
case 'DELETE_CLUB_MEMBER':
|
||||||
|
return '클럽 회원 삭제';
|
||||||
|
case 'CREATE_EVENT':
|
||||||
|
return '이벤트 생성';
|
||||||
|
case 'UPDATE_EVENT':
|
||||||
|
return '이벤트 수정';
|
||||||
|
case 'DELETE_EVENT':
|
||||||
|
return '이벤트 삭제';
|
||||||
|
case 'ADD_EVENT_PARTICIPANT':
|
||||||
|
return '이벤트 참가자 추가';
|
||||||
|
case 'UPDATE_EVENT_PARTICIPANT':
|
||||||
|
return '이벤트 참가자 수정';
|
||||||
|
case 'DELETE_EVENT_PARTICIPANT':
|
||||||
|
return '이벤트 참가자 삭제';
|
||||||
|
case 'VIEW_ACTIVITY_LOGS':
|
||||||
|
return '활동 로그 조회';
|
||||||
|
default:
|
||||||
|
return actionType;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 활동 유형 심각도 가져오기
|
||||||
|
const getActionTypeSeverity = (actionType) => {
|
||||||
|
if (actionType.includes('DELETE')) {
|
||||||
|
return 'danger';
|
||||||
|
} else if (actionType.includes('CREATE')) {
|
||||||
|
return 'success';
|
||||||
|
} else if (actionType.includes('UPDATE')) {
|
||||||
|
return 'warning';
|
||||||
|
} else if (actionType === 'LOGIN') {
|
||||||
|
return 'info';
|
||||||
|
} else if (actionType === 'LOGOUT') {
|
||||||
|
return 'secondary';
|
||||||
|
} else if (actionType.includes('VIEW')) {
|
||||||
|
return 'info';
|
||||||
|
} else {
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜 포맷팅
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.activity-log-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 테이블 스타일 */
|
||||||
|
:deep(.p-datatable-wrapper) {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -47,23 +47,23 @@
|
|||||||
<Dialog v-model:visible="clubDialog" :style="{width: '450px'}"
|
<Dialog v-model:visible="clubDialog" :style="{width: '450px'}"
|
||||||
:header="dialogMode === 'new' ? '새 클럽 추가' : '클럽 수정'"
|
:header="dialogMode === 'new' ? '새 클럽 추가' : '클럽 수정'"
|
||||||
:modal="true" class="p-fluid">
|
:modal="true" class="p-fluid">
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="name">클럽명</label>
|
<label for="name">클럽명</label>
|
||||||
<InputText id="name" v-model="club.name" required autofocus :class="{'p-invalid': submitted && !club.name}" />
|
<InputText id="name" v-model="club.name" required autofocus :class="{'p-invalid': submitted && !club.name}" class="w-full"/>
|
||||||
<small class="p-error" v-if="submitted && !club.name">클럽명은 필수입니다.</small>
|
<small class="p-error" v-if="submitted && !club.name">클럽명은 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="description">클럽 설명</label>
|
<label for="description">클럽 설명</label>
|
||||||
<Textarea id="description" v-model="club.description" rows="3" />
|
<Textarea id="description" v-model="club.description" rows="3" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="ownerEmail">모임장 이메일</label>
|
<label for="ownerEmail">모임장 이메일</label>
|
||||||
<InputText id="ownerEmail" v-model="club.ownerEmail" required :class="{'p-invalid': submitted && !club.ownerEmail}" />
|
<InputText id="ownerEmail" v-model="club.ownerEmail" required :class="{'p-invalid': submitted && !club.ownerEmail}" class="w-full" />
|
||||||
<small class="p-error" v-if="submitted && !club.ownerEmail">모임장 이메일은 필수입니다.</small>
|
<small class="p-error" v-if="submitted && !club.ownerEmail">모임장 이메일은 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="location">볼링장</label>
|
<label for="location">볼링장</label>
|
||||||
<InputText id="location" v-model="club.location" />
|
<InputText id="location" v-model="club.location" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<Column field="gender" header="성별" sortable style="min-width: 6rem"></Column>
|
<Column field="gender" header="성별" sortable style="min-width: 6rem"></Column>
|
||||||
<Column field="memberType" header="회원 유형" sortable style="min-width: 8rem">
|
<Column field="memberType" header="회원 유형" sortable style="min-width: 8rem">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<Dropdown v-model="slotProps.data.memberType"
|
<Select v-model="slotProps.data.memberType"
|
||||||
:options="memberTypes"
|
:options="memberTypes"
|
||||||
@change="updateMemberType(slotProps.data)"
|
@change="updateMemberType(slotProps.data)"
|
||||||
:disabled="slotProps.data.memberType === '모임장' && !canChangeOwner" />
|
:disabled="slotProps.data.memberType === '모임장' && !canChangeOwner" />
|
||||||
@@ -165,36 +165,36 @@
|
|||||||
<Dialog v-model:visible="memberFormDialog" :style="{width: '450px'}"
|
<Dialog v-model:visible="memberFormDialog" :style="{width: '450px'}"
|
||||||
:header="memberDialogMode === 'new' ? '새 회원 추가' : '회원 정보 수정'"
|
:header="memberDialogMode === 'new' ? '새 회원 추가' : '회원 정보 수정'"
|
||||||
:modal="true" class="p-fluid">
|
:modal="true" class="p-fluid">
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="name">이름</label>
|
<label for="name">이름</label>
|
||||||
<InputText id="name" v-model="member.name" required autofocus :class="{'p-invalid': memberSubmitted && !member.name}" />
|
<InputText id="name" v-model="member.name" required autofocus :class="{'p-invalid': memberSubmitted && !member.name}" class="w-full"/>
|
||||||
<small class="p-error" v-if="memberSubmitted && !member.name">이름은 필수입니다.</small>
|
<small class="p-error" v-if="memberSubmitted && !member.name">이름은 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="gender">성별</label>
|
<label for="gender">성별</label>
|
||||||
<Dropdown id="gender" v-model="member.gender"
|
<Select id="gender" v-model="member.gender"
|
||||||
:options="['남성', '여성']" placeholder="성별 선택"
|
:options="['남성', '여성']" placeholder="성별 선택"
|
||||||
:class="{'p-invalid': memberSubmitted && !member.gender}" />
|
:class="{'p-invalid': memberSubmitted && !member.gender}" class="w-full" />
|
||||||
<small class="p-error" v-if="memberSubmitted && !member.gender">성별은 필수입니다.</small>
|
<small class="p-error" v-if="memberSubmitted && !member.gender">성별은 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="memberType">회원 유형</label>
|
<label for="memberType">회원 유형</label>
|
||||||
<Dropdown id="memberType" v-model="member.memberType"
|
<Select id="memberType" v-model="member.memberType"
|
||||||
:options="memberTypes" placeholder="회원 유형 선택"
|
:options="memberTypes" placeholder="회원 유형 선택"
|
||||||
:class="{'p-invalid': memberSubmitted && !member.memberType}" />
|
:class="{'p-invalid': memberSubmitted && !member.memberType}" class="w-full" />
|
||||||
<small class="p-error" v-if="memberSubmitted && !member.memberType">회원 유형은 필수입니다.</small>
|
<small class="p-error" v-if="memberSubmitted && !member.memberType">회원 유형은 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="handicap">핸디캡</label>
|
<label for="handicap">핸디캡</label>
|
||||||
<InputNumber id="handicap" v-model="member.handicap" :min="0" :max="300" />
|
<InputNumber id="handicap" v-model="member.handicap" :min="0" :max="300" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="phone">연락처</label>
|
<label for="phone">연락처</label>
|
||||||
<InputText id="phone" v-model="member.phone" />
|
<InputText id="phone" v-model="member.phone" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="email">이메일</label>
|
<label for="email">이메일</label>
|
||||||
<InputText id="email" v-model="member.email" type="email" />
|
<InputText id="email" v-model="member.email" type="email" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|||||||
@@ -52,6 +52,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-6 lg:col-3">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="pi pi-history"></i>
|
||||||
|
<h3>활동 로그</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<p>시스템 활동 기록</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<Button label="활동 로그" icon="pi pi-arrow-right" @click="navigateTo('/admin/activity-logs')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 최근 데이터 -->
|
<!-- 최근 데이터 -->
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
stripedRows
|
stripedRows
|
||||||
filterDisplay="menu"
|
filterDisplay="menu"
|
||||||
v-model:filters="filters"
|
v-model:filters="filters"
|
||||||
scrollable
|
:sortField="'id'"
|
||||||
scrollHeight="calc(100vh - 200px)"
|
:sortOrder="1"
|
||||||
>
|
>
|
||||||
<Column field="id" header="ID" sortable style="width: 5%"></Column>
|
<Column field="id" header="ID" sortable style="width: 5%"></Column>
|
||||||
<Column field="name" header="이름" sortable style="width: 15%">
|
<Column field="name" header="이름" sortable style="width: 15%">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<Badge :value="getRoleName(slotProps.data.role)" :severity="getRoleSeverity(slotProps.data.role)" />
|
<Badge :value="getRoleName(slotProps.data.role)" :severity="getRoleSeverity(slotProps.data.role)" />
|
||||||
</template>
|
</template>
|
||||||
<template #filter="{ filterModel, filterCallback }">
|
<template #filter="{ filterModel, filterCallback }">
|
||||||
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="roleOptions" optionLabel="name" optionValue="value" placeholder="역할 선택" class="p-column-filter" />
|
<Select v-model="filterModel.value" @change="filterCallback()" :options="roleOptions" optionLabel="name" optionValue="value" placeholder="역할 선택" class="p-column-filter" />
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="status" header="상태" sortable style="width: 10%">
|
<Column field="status" header="상태" sortable style="width: 10%">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<Badge :value="getStatusName(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
<Badge :value="getStatusName(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||||
</template>
|
</template>
|
||||||
<template #filter="{ filterModel, filterCallback }">
|
<template #filter="{ filterModel, filterCallback }">
|
||||||
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="statusOptions" optionLabel="name" optionValue="value" placeholder="상태 선택" class="p-column-filter" />
|
<Select v-model="filterModel.value" @change="filterCallback()" :options="statusOptions" optionLabel="name" optionValue="value" placeholder="상태 선택" class="p-column-filter" />
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="createdAt" header="가입일" sortable style="width: 12%">
|
<Column field="createdAt" header="가입일" sortable style="width: 12%">
|
||||||
@@ -69,34 +69,34 @@
|
|||||||
|
|
||||||
<!-- 사용자 추가/수정 다이얼로그 -->
|
<!-- 사용자 추가/수정 다이얼로그 -->
|
||||||
<Dialog v-model:visible="userDialog" :header="dialogTitle" :style="{ width: '450px' }" :modal="true" class="p-fluid">
|
<Dialog v-model:visible="userDialog" :header="dialogTitle" :style="{ width: '450px' }" :modal="true" class="p-fluid">
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="name">이름</label>
|
<label for="name">이름</label>
|
||||||
<InputText id="name" v-model="user.name" :class="{'p-invalid': submitted && !user.name}" required autofocus />
|
<InputText id="name" v-model="user.name" :class="{'p-invalid': submitted && !user.name}" required autofocus class="w-full"/>
|
||||||
<small class="p-error" v-if="submitted && !user.name">이름은 필수입니다.</small>
|
<small class="p-error" v-if="submitted && !user.name">이름은 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="email">이메일</label>
|
<label for="email">이메일</label>
|
||||||
<InputText id="email" v-model="user.email" :class="{'p-invalid': submitted && !user.email}" required />
|
<InputText id="email" v-model="user.email" :class="{'p-invalid': submitted && !user.email}" required class="w-full"/>
|
||||||
<small class="p-error" v-if="submitted && !user.email">이메일은 필수입니다.</small>
|
<small class="p-error" v-if="submitted && !user.email">이메일은 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="password">비밀번호</label>
|
<label for="password">비밀번호</label>
|
||||||
<Password id="password" v-model="user.password" :class="{'p-invalid': submitted && isNewUser && !user.password}" toggleMask :feedback="isNewUser" />
|
<Password id="password" v-model="user.password" :class="{'p-invalid': submitted && isNewUser && !user.password}" toggleMask :feedback="isNewUser" class="w-full" />
|
||||||
<small class="p-error" v-if="submitted && isNewUser && !user.password">비밀번호는 필수입니다.</small>
|
<small class="p-error" v-if="submitted && isNewUser && !user.password">비밀번호는 필수입니다.</small>
|
||||||
<small v-if="!isNewUser">비밀번호를 변경하지 않으려면 비워두세요.</small>
|
<small v-if="!isNewUser">비밀번호를 변경하지 않으려면 비워두세요.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="role">역할</label>
|
<label for="role">역할</label>
|
||||||
<Dropdown id="role" v-model="user.role" :options="roleOptions" optionLabel="name" optionValue="value" placeholder="역할 선택" required />
|
<Select id="role" v-model="user.role" :options="roleOptions" optionLabel="name" optionValue="value" placeholder="역할 선택" required class="w-full" />
|
||||||
<small class="p-error" v-if="submitted && !user.role">역할은 필수입니다.</small>
|
<small class="p-error" v-if="submitted && !user.role">역할은 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="status">상태</label>
|
<label for="status">상태</label>
|
||||||
<Dropdown id="status" v-model="user.status" :options="statusOptions" optionLabel="name" optionValue="value" placeholder="상태 선택" required />
|
<Select id="status" v-model="user.status" :options="statusOptions" optionLabel="name" optionValue="value" placeholder="상태 선택" required class="w-full" />
|
||||||
<small class="p-error" v-if="submitted && !user.status">상태는 필수입니다.</small>
|
<small class="p-error" v-if="submitted && !user.status">상태는 필수입니다.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,9 +125,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, reactive, computed } from 'vue';
|
import { ref, onMounted, reactive, computed } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { FilterMatchMode } from 'primevue/api';
|
|
||||||
import Badge from 'primevue/badge';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import adminService from '@/services/adminService';
|
import adminService from '@/services/adminService';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -143,10 +140,10 @@ const isNewUser = ref(false);
|
|||||||
|
|
||||||
// 필터 설정
|
// 필터 설정
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
name: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
name: { value: null, matchMode: 'contains' },
|
||||||
email: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
email: { value: null, matchMode: 'contains' },
|
||||||
role: { value: null, matchMode: FilterMatchMode.EQUALS },
|
role: { value: null, matchMode: 'equals' },
|
||||||
status: { value: null, matchMode: FilterMatchMode.EQUALS }
|
status: { value: null, matchMode: 'equals' }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 역할 옵션
|
// 역할 옵션
|
||||||
@@ -357,4 +354,13 @@ const formatDate = (date) => {
|
|||||||
.mr-3 {
|
.mr-3 {
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Password 컴포넌트 스타일 수정 */
|
||||||
|
:deep(.p-password) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-password-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user