CRUD
(1) xml
(2) model, DTO
(3) Interfaces -> Repository
(4) Service
(5) Controller
(6) 화면 구성
관리자 페이지에서 어떤 목적과 어떤 기능을 만들건지 잘 설계하고 들어가기!!
회원 관리 기능
- - 회원 조회
- - 회원 생성
- - 회원 삭제
- - 회원 수정
에서 조회랑 탈퇴(삭제) 기능만 만들 예정
(생성과 수정은 다른 페이지에서 하기로 설계를 했습니다.)
sql
-- 유저 테이블
create table user_tb (
id int primary key auto_increment,
user_id varchar(50) not null,
name varchar(20) null,
password varchar(1000) not null,
nickname varchar(20) null,
UNIQUE (nickname),
phone_number varchar(20) null,
gender varchar(10) null,
profile Blob,
birth int null,
point int default 0,
user_role int default 0,
online_status boolean default false,
active_status varchar(7) default '활동중',
created_at timestamp default CURRENT_TIMESTAMP
);
-- 탈퇴 사유 조회 테이블
create table withdrawal_reason_tb (
id int primary key auto_increment,
user_id int not null,
reason varchar(255) not null,
details text null,
created_at timestamp default CURRENT_TIMESTAMP,
foreign key (user_id) references user_tb(id) on delete cascade
);
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!-- mapper DTD 선언 -->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.amigo_project.repository.interfaces.AdminRepository">
<!-- 일반 유저 리스트 받아 오기 -->
<select id="findUserAll" resultType="com.example.amigo_project.repository.model.User">
SELECT *
FROM user_tb
WHERE user_role = 0
AND active_status = '활동중'
</select>
<!-- 탈퇴 회원 조회 -->
<select id="findDeletedUsers" resultType="com.example.amigo_project.repository.model.User">
SELECT *
FROM user_tb
WHERE active_status = '탈퇴'
</select>
<!-- 탈퇴 사유 조회 -->
<select id="findWithdrawalReason" parameterType="int" resultType="com.example.amigo_project.dto.WithdrawalReasonDTO">
SELECT
u.id,
u.name AS user_name,
wr.reason,
wr.details,
wr.created_at
FROM
withdrawal_reason_tb wr
JOIN
user_tb u ON wr.user_id = u.id
WHERE
wr.user_id = #{id}
</select>
<select id="findById" resultType="com.example.amigo_project.repository.model.User">
SELECT * FROM user_tb WHERE id = #{id}
</select>
<!-- 유저 탈퇴 -->
<update id="deactivatedUserId" parameterType="com.example.amigo_project.repository.model.User">
UPDATE user_tb
SET active_status = '탈퇴'
WHERE id = #{id}
</update>
<!-- 탈퇴 해지 -->
<update id="restoreUserStatus" parameterType="int">
UPDATE user_tb
SET active_status = '활동중'
WHERE id = #{id}
</update>
role로 유저와 관리자를 확인합니다 ➡️ where user_role= 0 로 유저만 조회
active_status로 현재 유저 상태를 확인합니다(활동, 탈퇴, 정지 등)
➡️ active_status로 탈퇴를 시킬 수 있습니다 / '활동중' 으로 update를 하면 탈퇴 해지를 시킬 수 있습니다
User (model)
/**
* onlineStatus 접속상태 default 0
* activeStatus 가입상태(탈퇴) default 0
*/
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
@Data
public class User {
private Integer id; // (pk) auto_increment
private String userId;
private String name;
private String password;
private String nickname;
private String phoneNumber;
private String gender;
private Integer birth;
private Integer point;
private Integer userRole;
private byte[] profile;
private boolean onlineStatus;
private String activeStatus;
private String school;
private Timestamp createdAt;
private String createdAtFormat;
private LocalDate suspensionEndDate; // 정지 종료 날짜
public void getFormattedCreatedAt() {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
this.createdAtFormat = formatter.format(createdAt);
}
}
WithdrawalReasonDTO (DTO) // 탈퇴 사유
@Data
public class WithdrawalReasonDTO {
private Integer id;
private String userName;
private String reason;
private String details;
private Timestamp createdAt;
}
💡팁
resultType에 model을 넣어야 할지.. DTO를 넣어야 할지 잘 모르겠으면
- Model: 데이터베이스의 엔티티와 매핑된 객체로, 조회 시 모든 필드를 포함하여 사용됩니다. 즉, 전체 정보를 필요로 할 때 사용합니다. 예를 들어, 특정 엔티티의 모든 속성 값을 가져와야 하는 경우 모델 객체를 사용합니다.
- DTO (Data Transfer Object): 클라이언트나 다른 계층으로 데이터를 전달할 때, 필요한 필드만 선택적으로 포함하는 객체입니다. 부분적으로 필요한 정보만 추출하거나, 전송 데이터의 양을 줄이고 싶을 때 사용합니다.
model은 조회 -> 모든 정보를 조회할 때
dto는 부분적으로 사용할 때 사용하면 된다
Repository
@Mapper
public interface AdminRepository {
// 유저 조회
public List<User> findUserAll();
// 탈퇴 회원 조회
public List<User> findDeletedUsers();
// 탈퇴 사유 조회
public WithdrawalReasonDTO findWithdrawalReason(int id);
// 탈퇴 해지
public int restoreUserStatus(UserDTO userDTO);
// 유저 상세보기 (Detail)
public User findById(int id);
// 유저 탈퇴 처리
public int deactivatedUserId(int id);
}
Service
@Service
@RequiredArgsConstructor
public class AdminService {
private final AdminRepository adminRepository;
private final NoticeRepository noticeRepository;
// 유저 조회
public List<User> getUserList(){
List<User> userList = adminRepository.findUserAll();
return userList;
}
// 상세보기 (특정 유저 조회)
public User findById(int id){
User user = adminRepository.findById(id);
return user;
}
// 유저 탈퇴
public void deactivatedUserId(int id){
adminRepository.deactivatedUserId(id);
}
// 탈퇴 회원 조회
public List<User> findDeletedUsers(){
return adminRepository.findDeletedUsers();
}
// 탈퇴 사유 조회
public WithdrawalReasonDTO findWithdrawalReason(int id) {
WithdrawalReasonDTO reason = adminRepository.findWithdrawalReason(id);
return reason;
}
// 탈퇴 해지
public int restoreUserStatus(UserDTO userDTO) {
return adminRepository.restoreUserStatus(userDTO);
}
}
Controller
@Controller
@RequiredArgsConstructor
@RequestMapping("/admin")
public class AdminController {
private final AdminService adminService;
// 메인 화면
@GetMapping("/main")
public String home() {
return "views/admins/admin"; // index.mustache 파일을 반환 (임시)
}
/**
* 회원관리
* @param model
* @return
*/
// 회원 관리 - 유저 관리 페이지
@GetMapping("/user")
public String userPage(Model model){
List<User> userList = adminService.getUserList();
model.addAttribute("userList", userList);
return "views/admins/user"; // 임시
}
// // 유저 탈퇴
@PostMapping("/deleteUsers/{id}")
public String deleteUser(@PathVariable(name = "id") Integer id){
adminService.deactivatedUserId(id);
return "redirect:/admin/user/detail/" + id;
}
// 탈퇴 회원 조회
@GetMapping("/deletedUsers")
public String deletedUsersPage(Model model){
List<User> deletedUser = adminService.findDeletedUsers();
model.addAttribute("deletedUser", deletedUser);
return "views/admins/deletedUsers";
}
// 특정 유저의 탈퇴 사유 조회
@GetMapping("/deletedUsers/withdrawalReason/{id}")
@ResponseBody
public WithdrawalReasonDTO findWithdrawalReason(@PathVariable("id") int id) {
WithdrawalReasonDTO reason = adminService.findWithdrawalReason(id);
System.out.println("조회된 탈퇴 사유: " + reason); // 탈퇴 사유 데이터 출력
return reason;
}
// 탈퇴 해지 요청
@PostMapping("/restoreUser")
public ResponseEntity<String> restoreUserStatus(@RequestBody UserDTO userDTO) {
int result = adminService.restoreUserStatus(userDTO);
if (result > 0) {
return ResponseEntity.ok("탈퇴 해지 되었습니다.");
} else {
return ResponseEntity.status(400).body("탈퇴 해지에 실패했습니다.");
}
}
// 유저 관리 - 상세보기
@GetMapping("/user/detail/{id}")
public String userDetail(Model model, @PathVariable(name = "id") int id){
User user = adminService.findById(id);
System.out.println(user);
// 게시글 개수 조회
AdminDTO adminDTO = adminService.findBoardCount(id);
// 댓글 개수 조회
CommentDTO commentDTO = adminService.findCommentCount(id);
// 시간
user.getFormattedCreatedAt();
model.addAttribute("user", user);
System.out.println("게시글 수 : " + adminDTO.getBoardCount());
System.out.println("댓글 수 : " + commentDTO.getCommentCount());
model.addAttribute("boardCount",adminDTO.getBoardCount());
model.addAttribute("commentCount",commentDTO.getCommentCount());
return "views/admins/userDetail"; // 임시
}
}
화면 구성 (mustache 사용)
[회원조회 - 목록]
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회원 조회</title>
<!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.25/css/jquery.dataTables.min.css">
<!-- Custom CSS -->
<style>
/* 기본 스타일 */
body {
font-family: 'Arial', sans-serif;
background-color: #f8f9fc;
}
h1.h3, h6.m-0 {
color: #4e73df;
}
/* 테이블 스타일 */
.table thead th {
background-color: #4e73df;
color: white;
text-align: center;
}
.table td {
vertical-align: middle;
text-align: center;
}
/* 상태 아이콘 */
.status-icon {
width: 24px;
height: 24px;
}
/* DataTable 검색 및 페이지네이션 커스터마이징 */
.dataTables_filter {
float: right;
text-align: right;
}
.dataTables_length {
float: left;
}
.dataTables_info {
float: left;
margin-top: 10px;
}
.dataTables_paginate {
float: right;
margin-top: 10px;
}
</style>
</head>
<body id="page-top">
{{>layout/admin/adminSidebar}}
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<div id="content">
<!-- Main Content -->
<div class="container-fluid">
<h1 class="h3 mb-2 text-gray-800">회원 조회</h1><br>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-secondary">회원 목록</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="userTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>이름</th>
<th>아이디</th>
<th>온라인 상태</th>
<th>활성상태</th>
<th>상세보기</th>
</tr>
</thead>
<tbody>
{{#userList}}
<tr>
<td>{{name}}</td>
<td>{{userId}}</td>
<td>
{{#onlineStatus}}
<img src="/image/admin/free-icon-checkmark.png" class="status-icon" alt="온라인 상태">
{{/onlineStatus}}
{{^onlineStatus}}
<img src="/image/admin/free-icon-delete.png" class="status-icon" alt="오프라인 상태">
{{/onlineStatus}}
</td>
<td>{{activeStatus}}</td>
<td><a href="/admin/user/detail/{{id}}" class="btn btn-primary btn-sm">상세보기</a></td>
</tr>
{{/userList}}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- End of Main Content -->
</div>
</div>
<!-- End of Content Wrapper -->
<!-- JavaScript Files Load -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js"></script>
<script src="/vendor/js/sb-admin-2.min.js"></script>
<script>
$(document).ready(function() {
var table = $('#userTable').DataTable({
"paging": true, // 페이지네이션 활성화
"pageLength": 10, // 한 페이지에 표시할 행 수
"language": {
"search": "검색:",
"lengthMenu": "페이지당 _MENU_ 개 보기",
"info": "총 _TOTAL_명의 회원 중 _START_에서 _END_까지 표시",
"paginate": {
"previous": "이전",
"next": "다음"
}
}
});
// 전체 검색 필드를 이름, 아이디 열로 제한하는 커스텀 검색 기능 추가
$.fn.dataTable.ext.search.push(
function(settings, data, dataIndex) {
var searchTerm = $('#userTable_filter input').val().toLowerCase();
var name = data[0].toLowerCase(); // 이름 열
var userId = data[1].toLowerCase(); // 아이디 열
// 이름이나 아이디 열에 검색어가 포함되어 있는 경우에만 결과로 포함
if (name.includes(searchTerm) || userId.includes(searchTerm)) {
return true;
}
return false;
}
);
// 검색 필드에 입력될 때마다 테이블 다시 그리기
$('#userTable_filter input').on('keyup change', function() {
table.draw();
});
});
</script>
</body>
</html>
[회원조회 - 상세보기]
{{>layout/admin/adminSidebar}}
<div id="content-wrapper" class="d-flex flex-column">
<div id="content">
<div class="container-fluid">
<h1 class="h3 mb-2 text-gray-800">회원 상세보기</h1>
<br><br>
<div class="row align-items-stretch">
<div class="col-lg-6 d-flex mb-4">
<div class="card w-100 border-0">
<div class="card-header py-3 bg-light">
<h6 class="m-0 font-weight-bold text-secondary">회원 정보</h6>
</div>
<div class="card-body">
<h5 class="font-weight-bold">{{user.nickname}} : 회원 정보</h5><br>
<table class="table table-sm table-borderless">
<tbody>
<tr>
<th>아이디 :</th>
<td>{{user.userId}}</td>
</tr>
<tr>
<th>비밀번호 :</th>
<td>********</td>
</tr>
<tr>
<th>이름 :</th>
<td>{{user.nickname}}</td>
</tr>
<tr>
<th>휴대폰 번호 :</th>
<td>{{user.phoneNumber}}</td>
</tr>
<tr>
<th>성별 :</th>
<td>{{user.gender}}</td>
</tr>
<tr>
<th>나이 :</th>
<td>{{user.birth}}살</td>
</tr>
</tbody>
</table>
<button class="btn btn-danger btn-sm mt-3" onclick="deletePost({{user.id}})">탈퇴 처리</button>
</div>
</div>
</div>
<div class="col-lg-6 d-flex mb-4">
<div class="card w-100 border-0">
<div class="card-header py-3 bg-light">
<h6 class="m-0 font-weight-bold text-secondary">활동 정보</h6>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tbody>
<tr>
<th>가입일 :</th>
<td data-created-at>{{user.createdAtFormat}}</td>
</tr>
<tr>
<th>게시글 수 :</th>
<td>{{boardCount}}개</td>
</tr>
<tr>
<th>댓글 수 :</th>
<td>{{commentCount}}개</td>
</tr>
<tr>
<th>포인트 :</th>
<td>{{user.point}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="d-flex justify-content-end w-100 mt-3">
<a href="/admin/user" class="btn btn-secondary btn-sm">뒤로가기</a>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript -->
<script src="/vendor/jquery/jquery.min.js"></script>
<script src="/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="/vendor/jquery-easing/jquery.easing.min.js"></script>
<script src="/vendor/js/sb-admin-2.min.js"></script>
<script>
$(document).ready(function () {
const createdAtElement = document.querySelector('td[data-created-at]');
if (createdAtElement) {
createdAtElement.textContent = formatDate(createdAtElement.textContent);
}
});
function formatDate(dateString) {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function deletePost(postId) {
if (confirm('정말로 탈퇴하시겠습니까?')) {
$.ajax({
url: '/admin/deleteUsers/' + postId,
type: 'POST',
success: function () {
alert('탈퇴 완료되었습니다.');
window.location.href = "/admin/user";
},
error: function () {
alert('삭제에 실패했습니다. 다시 시도해주세요.');
}
});
}
}
</script>
</body>
</html>
[탈퇴 회원 조회 - 목록 ]
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.25/css/jquery.dataTables.min.css">
<style>
.status-icon {
width: 24px;
height: 24px;
}
.modal {
display: none;
position: fixed;
z-index: 1;
padding-top: 100px;
left: 0;
top: 0;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 45% !important;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
</style>
<body id="page-top">
{{>layout/admin/adminSidebar}}
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<div id="content">
<!-- Main Content -->
<div class="container-fluid">
<h1 class="h3 mb-2 text-gray-800">탈퇴 회원 조회</h1><br>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-secondary">탈퇴 회원 목록</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="userTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>이름</th>
<th>아이디</th>
<th>활성상태</th>
<th>상세보기</th>
<th>탈퇴 해지</th> <!-- 탈퇴 해지 버튼 열 -->
</tr>
</thead>
<tbody>
{{#deletedUser}}
<tr>
<td>{{name}}</td>
<td>{{userId}}</td>
<td>{{activeStatus}}</td>
<td><button class="btn btn-primary btn-sm detail-btn" data-id="{{id}}">상세보기</button></td>
<td><button class="btn btn-success btn-sm restore-btn" data-id="{{id}}">탈퇴 해지</button></td> <!-- 탈퇴 해지 버튼 -->
</tr>
{{/deletedUser}}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- End of Main Content -->
</div>
</div>
<!-- End of Content Wrapper -->
<!-- Modal -->
<div id="detailModal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<h2>탈퇴 사유</h2>
<p><strong>사유:</strong> <span id="withdrawReason">탈퇴 사유를 불러오는 중...</span></p>
<p><strong>상세 내용:</strong> <span id="withdrawDetails">상세 내용을 불러오는 중...</span></p>
<p><strong>탈퇴 날짜:</strong> <span id="withdrawDate">날짜를 불러오는 중...</span></p>
</div>
</div>
<!-- JavaScript Files Load -->
<!-- jQuery CDN -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap JS CDN -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
<!-- DataTables JavaScript -->
<script src="https://cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="/vendor/js/sb-admin-2.min.js"></script>
<script>
$(document).ready(function() {
var table = $('#userTable').DataTable({
"language": {
"search": "검색:",
"lengthMenu": "_MENU_ 개씩 보기",
"info": "총 _TOTAL_명의 회원 중 _START_에서 _END_까지 표시",
"paginate": {
"previous": "이전",
"next": "다음"
}
}
});
// 전체 검색 필드를 특정 열로만 제한하는 커스텀 검색 기능 추가
$.fn.dataTable.ext.search.push(
function(settings, data, dataIndex) {
var searchTerm = $('#userTable_filter input').val().toLowerCase();
var name = data[0].toLowerCase(); // 이름 열
var userId = data[1].toLowerCase(); // 아이디 열
if (name.includes(searchTerm) || userId.includes(searchTerm)) {
return true;
}
return false;
}
);
// 검색 필드에 입력될 때마다 테이블 다시 그리기
$('#userTable_filter input').on('keyup change', function() {
table.draw();
});
// Modal 관련 스크립트
var modal = $('#detailModal');
var span = $('.close');
// 상세보기 버튼 클릭 시
$('.detail-btn').on('click', function() {
var userId = $(this).data('id');
// AJAX를 사용해 특정 유저의 탈퇴 사유를 가져옵니다
$.ajax({
url: '/admin/deletedUsers/withdrawalReason/' + userId,
method: 'GET',
success: function(response) {
console.log(response); // 응답을 확인해 보기 위해 콘솔에 출력
if (response) {
// 탈퇴 사유와 관련된 데이터를 안전하게 설정
$('#withdrawReason').text(response.reason ? response.reason : '사유가 없습니다.');
$('#withdrawDetails').text(response.details ? response.details : '상세 내용이 없습니다.');
// createdAt 필드가 존재하고 유효한지 확인 후 처리
if (response.createdAt) {
try {
const withdrawalDate = response.createdAt.replace(" ", "T");
$('#withdrawDate').text(new Date(withdrawalDate).toLocaleDateString());
} catch (e) {
console.error('날짜 형식 오류:', e);
$('#withdrawDate').text('날짜 형식이 잘못되었습니다.');
}
} else {
$('#withdrawDate').text('탈퇴 날짜를 불러오지 못했습니다.');
}
} else {
$('#withdrawReason').text('탈퇴 사유를 불러오지 못했습니다.');
$('#withdrawDetails').text('상세 내용을 불러오지 못했습니다.');
$('#withdrawDate').text('날짜를 불러오지 못했습니다.');
}
modal.show();
},
error: function(xhr, status, error) {
console.error("AJAX 요청 실패:", error);
// 오류 발생 시 모달 내용 초기화 후 알림
$('#withdrawReason').text('탈퇴 사유를 불러오지 못했습니다.');
$('#withdrawDetails').text('상세 내용을 불러오지 못했습니다.');
$('#withdrawDate').text('날짜를 불러오지 못했습니다.');
modal.show();
}
});
});
// 탈퇴 해지 버튼 클릭 시
$('.restore-btn').on('click', function() {
var userId = $(this).data('id');
$.ajax({
url: '/admin/restoreUser',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ id: userId }),
success: function(response) {
alert(response); // 성공 메시지 표시
location.reload(); // 페이지 새로고침으로 변경된 상태 반영
},
error: function() {
alert('탈퇴 해지에 실패했습니다.');
}
});
});
// 모달 닫기 버튼 클릭 시
span.on('click', function() {
modal.hide();
});
// 모달 바깥 클릭 시 모달 닫기
$(window).on('click', function(event) {
if (event.target == modal[0]) {
modal.hide();
}
});
});
</script>
</body>
</html>
➡️ 탈퇴 상세보기는 (modal) 모달 창을 이용해서 조회하기
'My Project > amigo' 카테고리의 다른 글
[3일차] Admin Page - 게시글 관리 (0) | 2024.11.25 |
---|---|
[1일차] Admin Page Bootstrap 사용 (2) | 2024.10.18 |