담당 기능
ADMIN 페이지 회원 관리 기능 구현 담당을 맡아서 책임감 갖고 기능 구현을 해보겠습니다.
프로젝트 구조는 다음과 같습니다.
기능 구현할 때 사용된 객체/메서드/패턴은 이전 포스팅에서 자세하게 설명했으므로 생략하겠습니다.
프로젝트 구조
admin_main
백엔드 코드 구현하기 전에 UI 페이지를 생성합니다.
메인 페이지에서는 content에 ADMIN 페이지에서 관리하는 기능을 축소시켜서 ajax로 불러오는 디자인으로 피그마 작업을 했기 때문에 코드는 다음과 같이 구현했습니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<c:set var="path" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<script type="text/javascript">
function openModal(modalId) {
document.getElementById(modalId).style.display = "block";
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = "none";
}
function redirectToPage() {
location.href = "${path}/faq/faq_list.jsp";
}
function memberList() {
location.href="${path}/admin/memberList";
}
</script>
<style>
#container {
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
}
#category {
display: flex;
flex-direction: column;
height: 100%;
width: 15%;
gap: 25px;
align-items: center;
border-right: 1px solid #000000;
padding-top: 50px;
}
div a.menubar {
text-decoration: none;
display: flex;
color: #000;
padding: 25px 25px 25px 25px;
font-weight: bold;
}
.menu > a:hover {
background-color: #333;
color: #fff;
}
.bi-plus-lg {
font-size: 22px;
}
p.content2 {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1; /* Set the number of lines to display */
-webkit-box-orient: vertical;
}
</style>
<body>
<%@ include file="../header.jspf" %>
<div id="container">
<div style="display: flex; height: auto;">
<div id="category" class="menu" style="width:10%; height:auto;">
<a class="menubar" href="${path}/admin/memberList">회원관리</a>
<a class="menubar" href="${path}/trip/list_admin.do">관광명소 관리</a>
<a class="menubar" href="${path}/review/list.do" >리뷰리스트 관리</a>
<a class="menubar" href="${path}/faq/list.do">FAQ</a>
</div>
<div class="container" style="padding-left: 50px; padding-right: 50px;">
<div id="result" style="display: flex; width:100%; flex-direction: column;">
<div class="content">
<div class="header" style="display:flex; height:auto; padding-top: 50px;">
<div class="container" style="flex-basis: 50%;">
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3>회원관리</h3>
<button class="btn" type="button" onclick="memberList()" style="float:right;">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th>이메일</th>
<th>이름</th>
<th>주소</th>
<th>가입일자</th>
</tr>
</thead>
<tbody>
<c:forEach var="dto" items="${memberList}" varStatus="status">
<c:choose>
<c:when test="${status.index < 4}">
<tr>
<td>${dto.mem_email}</td>
<td><a href="${path}/admin/view.do?mem_num=${dto.mem_num}">${dto.mem_name}</a></td>
<td>${dto.mem_address1}</td>
<td><fmt:formatDate value="${dto.mem_join_date}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
</tr>
</c:when>
</c:choose>
</c:forEach>
</tbody>
</table>
</div>
</div>
<div class="container" style="flex-basis: 50%;">
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3>리뷰페이지</h3>
<button class="btn" type="button" onclick="memberList()" style="float:right;">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th>이메일</th>
<th>이름</th>
<th>주소</th>
<th>가입일자</th>
</tr>
</thead>
<tbody>
<tr>
<td>dkjfwk@naver.com</td>
<td><a href="#" onclick="memberList()">홍길동</a></td>
<td>서울시 강남구</td>
<td>2023년 5월 31일</td>
</tr>
<tr>
<td>ddwwwd@naver.com</td>
<td><a href="#" onclick="memberList()">이순신</a></td>
<td>서울시 용산구</td>
<td>2023년 5월 3일</td>
</tr>
<tr>
<td>daaaewe@naver.com</td>
<td><a href="#" onclick="memberList()">김김김</a></td>
<td>서울시 종로구</td>
<td>2023년 5월 1일</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="footer" style="display:flex; height:auto; margin-top: 20px;">
<div class="container" style="flex-basis: 50%; ">
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3>FAQ</h3>
<button class="btn" type="button" onclick="faqList()"
style="float: right;">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th style="width: 10%">번호</th>
<th style="width: 55%">제목</th>
<th style="width: 20%">작성일자</th>
<th style="width: 15%">조회수</th>
</tr>
</thead>
<tbody>
<c:set var="recentList" value="${list.subList(0, 5)}"/>
<c:forEach var="row" items="${recentList}">
<tr>
<td>${row.faq_num}</td>
<td>
<a href="${path}/faq/view.do?faq_num=${row.faq_num}">${row.faq_subject}</a>
</td>
<td><fmt:formatDate value="${row.faq_reg_date}"
pattern="yyyy-MM-dd"/></td>
<td>${row.readcount}</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
<div class="container" style="flex-basis: 50%;">
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3>관광지</h3>
<button class="btn" type="button" onclick="tripList()"
style="float: right;">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th style="width: 8%">번호</th>
<th style="width: 18%">관광명소</th>
<th style="width: 62%">내용</th>
<th style="width: 12%">조회수</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><a href="#" onclick="openModal('myModal3')">북촌 한옥마을</a></td>
<td><p class="content2">북촌 한옥마을은 서울에 위치한
한옥마을으로 사진찍기도 좋고 예쁜 카페도 많이 있습니다.
</p>
</td>
<td>0</td>
</tr>
<tr>
<td>2</td>
<td><a href="#" onclick="openModal('myModal3')">전주 한옥마을</a></td>
<td><p class="content2"> 전주한옥마을은 전라북도 전주시 완산구 풍남동에 있는
한옥마을이다.
원래 자연부락 형태의 마을들이 산자락에 형성되었었으나, 665년 신라 문무왕 때
완산주(完山州)가 설치되면서 주거지가
평지로 이동했다.
</p>
</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td><a href="#" onclick="openModal('myModal3')">경복궁</a></td>
<td><p class="content2">경복궁은 조선 왕조 제일의 법궁이다. 북으로
북악산을 기대어 자리 잡았고 정문인 광화문
앞으로는 넓은 육조거리가 펼쳐져,
왕도인 한양(서울) 도시 계획의 중심이기도 하다.
</p>
</td>
<td>25</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%@include file="../footer.jspf" %>
</div>
</body>
</html>
위 코드를 실행해서 HTML 화면단을 확인하면 아래 사진처럼 페이지 디자인 설정이 된 걸 확인할 수 있습니다.
일상 용어로 뼈대는 어느 정도 만들었으니, 디테일 작업은 백엔드 코드 구현이 끝나면 진행합니다.
다음은 해당 페이지에서 담당하고 있는 [회원관리] 페이지 기능 구현을 해야 합니다.
메인 페이지에서 보이는 것처럼 회원관리 페이지를 클릭했을 때 content에 회원 목록 리스트 페이지 호출해야 합니다.
그렇다면 회원 목록 리스트 페이지도 UI 작업을 해둡니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<c:set var="path" value="${pageContext.request.contextPath}" />
<script src="${path}/include/js/bootstrap.js"></script>
<link rel="stylesheet" href="${path}/include/style.css">
<script src="${path}/include/jquery-3.6.3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
table {
width: 700px;
border-collapse: collapse;
}
th, td {
border: 1px solid black;
padding: 8px;
}
div a.menubar {
text-decoration: none;
display: flex;
color: #000;
padding: 25px 25px 25px 25px;
font-weight: bold;
}
#container {
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
}
#category {
display: flex;
flex-direction: column;
height: 100%;
width: 15%;
gap: 25px;
align-items: center;
border-right: 1px solid #000000;
padding-top: 50px;
}
</style>
<body>
<%@ include file="../header.jspf" %>
<div id="container">
<div style="display: flex; height: auto;">
<div id="category" class="menu" style="width:10%; height:auto;">
<a class="menubar" href="${path}/admin/memberList">회원관리</a>
<a class="menubar" href="${path}/trip/list_admin.do">관광명소 관리</a>
<a class="menubar" href="${path}/review/list.do" >리뷰리스트 관리</a>
<a class="menubar" href="${path}/faq/list.do">FAQ</a>
</div>
<div class="container" >
<h2>유저 목록</h2>
<table class="table table-hover">
<thead>
<tr>
<th>회원번호</th>
<th>이메일</th>
<th>이름</th>
<th>주소</th>
<th>가입일자</th>
</tr>
</thead>
<tbody>
<c:forEach var="row" items="${list}">
<tr>
<td>${row.mem_num}</td>
<td>${row.mem_email}</td>
<td>
<input type="hidden" id="mem_num" name="mem_num" value="${row.mem_num}">
<a href="${path}/admin/view.do?mem_num=${row.mem_num}">${row.mem_name}</a>
</td>
<td>${row.mem_address1}</td>
<td><fmt:formatDate value="${row.mem_join_date}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
위 코드는 회원 목록 리스트 코드이며, 간단하게 테이블을 만들었습니다.
ERD에서 칼럼 명을 지정했으니, 타입과 칼럼에 맞게 DTO 생성해서 데이터를 요청해서 화면단에 호출하는 코드 흐름으로 작업하면 될 거 같습니다.
목록 리스트까지 UI 끝냈으니, 다음으로는 MVC 패턴으로 LIST를 호출해 주는 코드를 구현합니다.
AdminController
@RequestMapping("/memberList")
public String memberList(Model model) {
List<MemberDTO> list = adminService.memberList();
model.addAttribute("list", list);
long result = System.currentTimeMillis();
logger.info("유저 목록 페이지 : " + result + " ms" );
return "admin/admin_memberList";
}
@RequestMapping("/adminPage.do")
public ModelAndView adminPage(ModelAndView mav) throws Exception{
List<FaqDTO> items = faqService.list();
List<MemberDTO> memberList = adminService.memberList();
int memberCount = adminService.memberCount();
mav.setViewName("admin/admin_main");
mav.addObject("memberList", memberList);
mav.addObject("list", items);
mav.addObject("memberCount", memberCount);
return mav;
}
view단에서의 요청 처리를 해주고, Service 단으로 보내줍니다.
AdminService
Service단에서는 DAO단에 DI 주입을 해주고 요청을 보내줘서 mapper 처리를 할 수 있게 해 줍니다.
@Service
public class AdminServiceImpl implements AdminService {
@Autowired
MemberDAO memberDao;
@Override
public List<MemberDTO> memberList() {
return memberDao.memberList();
}
MemberDAO
네임스페이스를 지정하고, adminmapper.xml 파일에 요청을 보내서 DB에 데이터를 호출합니다.
//memberMapper 와의 상호작용을 담당합니다
@Repository
public class MemberDAOImpl implements MemberDAO {
@Autowired
SqlSession sqlSession;
// admin 회원 조회
@Override
public List<MemberDTO> memberList() {
return sqlSession.selectList("admin.adminMemberList");
}
mapper
호출하는 쿼리는 간단하게 select문을 작성해서 데이터를 가져옵니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 다른 mapper와 중복되지 않도록 네임스페이스 기재 -->
<mapper namespace="admin">
<select id="adminMemberList" resultType="member">
select*from member
</select>
목록 페이지가 제대로 구현됐는지 중간 점검을 해줍니다.
admin_memberList
회원 목록 테이블이 제대로 구현된 걸 확인했습니다.
다음으로는 이름을 클릭하면 상세 정보 페이지로 이동할 수 있는 코드를 구현합니다.
해당 코드를 보시면
<td><%-- 클릭 시 상세페이지 호출--%>
<input type="hidden" id="mem_num" name="mem_num" value="${row.mem_num}">
<a href="${path}/admin/view.do?mem_num=${row.mem_num}">${row.mem_name}</a>
</td>
상세 페이지에 회원 번호와 함께 Controller에 요청을 보내는 코드를 작성했습니다.
Controller에서는 요청을 받아서 service단에 보내줍니다.
Controller
// 상세페이지 코드 추가
@RequestMapping("/view.do")
public String view(Model model, @RequestParam int mem_num) {
model.addAttribute("dto", adminService.adminViewMember(mem_num));
return "admin/admin_memberView";
}
view단에서 해당 유저 번호를 같이 요청을 보내줬기 때문에 @RequestParam으로 지정해 주고 service단에 같이 보내줍니다.
Service
상세정보 페이지 기능도 목록 리스트와 동일하게 DAO에 요청을 보내주는 코드만 작성해 줍니다.
// 코드 추가
@Override
public MemberDTO adminViewMember(int mem_num) {
return memberDao.adminViewMember(mem_num);
}
DAO
DAO에서는 요청을 받아서 mapper 파일에 mem_num(회원번호)와 같이 보내줍니다.
// admin 회원 상세 정보 조회
@Override
public MemberDTO adminViewMember(int mem_num) {
return sqlSession.selectOne("admin.adminMemberView",mem_num);
}
mapper
요청받은 mem_num 값을 where 절에 대입시켜서 해당 유저 번호를 찾아서 정보를 호출하는 쿼리문으로 작성합니다.
<select id="adminMemberView" resultType="member">
select *
from member
where mem_num =#{mem_num}
</select>
mapper 파일 작업까지 끝냈다면 상세 정보 페이지를 생성합니다.
admin_mamberView
View 페이지에서는 목록 페이지와 동일하게 EL 식으로 대입을 해주고 우편번호는 api를 사용해서 수정할 수 있는 코드까지만 작성해 줍니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<c:set var="path" value="${pageContext.request.contextPath}"/>
<script src="${path}/resources/include/js/bootstrap.js"></script>
<link rel="stylesheet" href="${path}/resources/include/style.css">
<script src="${path}/resources/include/jquery-3.6.3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>adminList</title>
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script type="text/javascript">
function daumZipCode() {
new daum.Postcode({
oncomplete: function(data) {
var addr = ''; // 주소 변수
var extraAddr = ''; // 참고항목 변수
if (data.userSelectedType === 'R') {
addr = data.roadAddress;
} else {
addr = data.jibunAddress;
}
if(data.userSelectedType === 'R'){
if(data.bname !== '' && /[동|로|가]$/g.test(data.bname)){
extraAddr += data.bname;
}
if(data.buildingName !== '' && data.apartment === 'Y'){
extraAddr += (extraAddr !== '' ? ', ' + data.buildingName : data.buildingName);
}
if(extraAddr !== ''){
extraAddr = ' (' + extraAddr + ')';
}
document.getElementById("address1").value = extraAddr;
} else {
document.getElementById("address2").value = '';
}
document.getElementById('zipcode').value = data.zonecode;
document.getElementById("address1").value = addr;
document.getElementById("address2").focus();
}
}).open();
}
</script>
<style>
body {
font-family: Arial, sans-serif;
}
h2 {
padding: 20px;
text-align: center;
color: #333;
}
table {
width: 600px;
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 8px;
}
#profileimg{
text-align: center;
}
input[type="email"],
input[type="password"],
input[type="text"]{
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
width: 100%;
padding: 8px 16px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button-container {
display: flex;
justify-content: space-between;
margin-top: 16px;
}
.button-container button {
width: 48%;
}
#container {
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
}
#category {
display: flex;
flex-direction: column;
height: 100%;
width: 15%;
gap: 25px;
align-items: center;
border-right: 1px solid #000000;
padding-top: 50px;
}
div a.menubar {
text-decoration: none;
display: flex;
color: #000;
padding: 25px 25px 25px 25px;
font-weight: bold;
}
</style>
</head>
<body>
<%@ include file="../header.jspf" %>
<div id="container">
<div style="display: flex; height: auto;">
<div id="category" class="menu" style="width:10%; height:auto;">
<a class="menubar" href="${path}/admin/memberList">회원관리</a>
<a class="menubar" href="${path}/trip/list_admin.do">관광명소 관리</a>
<a class="menubar" href="${path}/review/list.do" >리뷰리스트 관리</a>
<a class="menubar" href="${path}/faq/list.do">FAQ</a>
</div>
<h2 style="font-size: 20px; margin-left: 100px;">프로필 수정</h2>
<form name="form1" id="form1" method="post" style="text-align: left; padding-left: 100px; margin-top: 80px; ">
<table width="600px" >
<tr>
<td colspan="2" id="profileimg">
<img style="width:200px; height:200px;" src="../resources/images/car.gif" class="img-thumbnail rounded-circle">
</td>
</tr>
<tr>
<td>이메일</td>
<td>이름</td>
</tr>
<tr>
<td><input type="email" id="mem_email" name="mem_email" value="${dto.mem_email}" readonly></td>
<td><input type="text" id="mem_name" name="mem_name" value="${dto.mem_name}"></td>
</tr>
<tr>
<td>비밀번호</td>
<td >비밀번호 확인</td>
</tr>
<tr>
<td><input type="password" id="mem_pass" name="mem_pass" value="${dto.mem_pass}" readonly></td>
<td><input type="password" id="passwd_ck" name="passwd_ck" value="${dto.mem_pass}" readonly></td>
<tr>
<td>닉네임</td>
<td>전화번호</td>
</tr>
<tr>
<td><input type="text" id="mem_nickname" name="mem_nickname" value="${dto.mem_nickname}"></td>
<td><input type="text" id="mem_phone" name="mem_phone" value="${dto.mem_phone}"></td>
</tr>
<tr>
<td style="text-align:center;">우편번호</td>
<td><input type="text" id="mem_zipcode" name="mem_zipcode" onclick="daumZipCode()" value="${dto.mem_zipcode}" placeholder="우편번호 찾기" readonly></td>
</tr>
<tr>
<td colspan="2"><input type="text" id="mem_address1" name="mem_address1" value="${dto.mem_address1}" readonly></td>
</tr>
<tr>
<td colspan="2"><input type="text" id="mem_address2" name="mem_address2" value="${dto.mem_address2}" placeholder="상세주소를 입력해주세요."></td>
</tr>
<tr>
<td colspan="2">
<input type="hidden" name="mem_num" value="${dto.mem_num}">
<button type="button" id="userUpdate">수정하기</button>
</td>
</tr>
<tr>
<td colspan="2"><button type="button" id="logback" name="logback">목록</button>
<div style="color: red;">${message}</div></td>
</tr>
</table>
</form>
</div>
<script>
$("#logback").click(function (){
location.href="${path}/admin/memberList";
});
$("#userUpdate").click(function (){
var mem_name = $("#mem_name").val();
var mem_nickname = $("#mem_nickname").val();
var mem_phone = $("#mem_phone").val();
var mem_zipcode = $("#mem_zipcode").val();
var mem_address1 = $("#mem_address1").val();
var mem_address2 = $("#mem_address2").val();
if(mem_name == ""){
alert("이름은 필수입니다.");
$("#mem_name").focus();
return;
}
if(mem_nickname == ""){
alert("닉네임을 입력하세요.");
$("#mem_nickname").focus();
return;
}
if(mem_phone == ""){
alert("번호를 입력하세요.");
$("#mem_phone").focus();
return;
}
if(mem_zipcode == ""){
alert("우편번호를 입력하세요.");
$("#mem_zipcode").focus();
return;
}
if(mem_address2 == ""){
alert("상세주소를 입력하세요.");
$("#mem_address2").focus();
return;
}
if(confirm("정보 수정 완료")){
document.form1.action="${path}/admin/userUpdate";
document.form1.submit();
}
});
</script>
</div>
</body>
</html>
결과를 확인해 보면
DB에서 제대로 데이터를 가져오는 걸 확인할 수 있습니다.
마치며
오늘은 [회원관리] 페이지 구현 중 목록/상세정보 페이지를 구현했습니다.
즉, select 기능만 사용한 코드입니다.
다음 포스팅은 수정하기 구현을 진행합니다.
'[ Project ] > Team' 카테고리의 다른 글
[ Team ] OAuth - Kakao API 로그인 구현 (0) | 2023.06.17 |
---|---|
[ Team ] OAuth - NAVER API 로그인 구현 (0) | 2023.06.16 |
[ Team ] 관리자 페이지 생성/ CRUD 구현 (수정 페이지) (0) | 2023.06.15 |
[ Team ] 팀 프로젝트 기획 (0) | 2023.06.11 |
[ Team ] 팀 프로젝트 개요 (0) | 2023.06.09 |