22.11.02 - Clone_Project - 4(로그인 처리, 인터셉터(Interceptor), 사용자 권한)
로그인 처리
config
SecurityConfig
package com.project.web_prj.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity // 시큐리티 설정을 웹에 적용 public class SecurityConfig { @Bean public BCryptPasswordEncoder encoder() { return new BCryptPasswordEncoder(); } // 시큐리티 기본 설정을 처리하는 빈 @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 초기에 나오는 디폴트 로그인 화면 안뜨게 하기 http.csrf().disable() // csrf공격 방어토큰 자동 생성 해제 .authorizeRequests() //권한요청 범위 설정 .antMatchers("/member/**") .permitAll() // /member로 시작하는 요청은 따로 권한 검증하지 말아라 ; return http.build(); } }
member
domain
Auth
package com.project.web_prj.member.domain; public enum Auth { COMMON, ADMIN }Member
package com.project.web_prj.member.domain; import lombok.*; import java.util.Date; @Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor public class Member { private String account; private String password; private String name; private String email; private Auth auth; private Date regDate; }
DTO
LoginDTO
package com.project.web_prj.member.dto; import lombok.*; @Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor public class LoginDTO { // 로그인 할때 클라이언트가 전송하는 데이터 private String account; private String password; private boolean autoLogin; }
repository
MemberMapper
package com.project.web_prj.member.repository; import com.project.web_prj.member.domain.Member; import org.apache.ibatis.annotations.Mapper; import java.util.Map; @Mapper public interface MemberMapper { // 회원 가입 기능 boolean register(Member member); // 중복체크 기능 // 체크타입: 계정 or 이메일 // 체크값: 중복검사대상 값 int isDuplicate(Map<String, Object> checkMap); // 회원정보 조회 기능 Member findUser(String account); }
service
LoginFlag
package com.project.web_prj.member.service; public enum LoginFlag { SUCCESS, NO_ACC, NO_PW }MemberService
package com.project.web_prj.member.service; import com.project.web_prj.member.domain.Member; import com.project.web_prj.member.dto.LoginDTO; import com.project.web_prj.member.repository.MemberMapper; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import javax.servlet.http.HttpSession; import java.util.HashMap; import java.util.Map; import static com.project.web_prj.member.service.LoginFlag.*; @Service @Log4j2 @RequiredArgsConstructor public class MemberService { private final MemberMapper memberMapper; private final BCryptPasswordEncoder encoder; // 회원 가입 중간 처리 public boolean signUp(Member member) { // 비밀번호 인코딩 member.setPassword(encoder.encode(member.getPassword())); return memberMapper.register(member); } // 중복확인 중간처리 /** * 계정과 이메일의 중복을 확인하는 메서드 * @param type - 확인할 정보 (ex: account or email) * @param value - 확인할 값 * @return 중복이라면 true, 중복이 아니라면 false */ public boolean checkSignUpValue(String type, String value) { Map<String, Object> checkMap = new HashMap<>(); checkMap.put("type", type); checkMap.put("value", value); return memberMapper.isDuplicate(checkMap) == 1; } // 회원 정보 조회 중간 처리 public Member getMember(String account) { return memberMapper.findUser(account); } // 로그인 처리 public LoginFlag login(LoginDTO inputData, HttpSession session) { // 회원가입 여부 확인 Member foundMember = memberMapper.findUser(inputData.getAccount()); if (foundMember != null) { if (encoder.matches(inputData.getPassword(), foundMember.getPassword())) { // 로그인 성공 // 세션에 사용자 정보기록 저장 session.setAttribute("loginUser", foundMember); // 세션 타임아웃 설정 session.setMaxInactiveInterval(60 * 60); // 1시간 return SUCCESS; } else { // 비번 틀림 return NO_PW; } } else { // 아이디 없음 return NO_ACC; } } }
Controller
MemberController
package com.project.web_prj.member.controller; import com.project.web_prj.member.domain.Member; import com.project.web_prj.member.dto.LoginDTO; import com.project.web_prj.member.service.LoginFlag; import com.project.web_prj.member.service.MemberService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @Controller @Log4j2 @RequiredArgsConstructor @RequestMapping("/member") public class MemberController { private final MemberService memberService; // 회원가입 양식 띄우기 요청 @GetMapping("/sign-up") public void signUp() { log.info("/member/sign-up GET! - forwarding to sign-up.jsp"); } // 회원가입 처리 요청 @PostMapping("/sign-up") public String signUp(Member member, RedirectAttributes ra) { log.info("/member/sign-up POST ! - {}", member); boolean flag = memberService.signUp(member); ra.addFlashAttribute("msg", "reg-success"); return flag ? "redirect:/member/sign-in" : "redirect:/member/sign-up"; } // 아이디, 이메일 중복확인 비동기 요청 처리 @GetMapping("/check") @ResponseBody public ResponseEntity<Boolean> check(String type, String value) { log.info("/member/check?type={}&value={} GET!! ASYNC", type, value); boolean flag = memberService.checkSignUpValue(type, value); return new ResponseEntity<>(flag, HttpStatus.OK); } // 로그인 화면을 열어주는 요청처리 @GetMapping("/sign-in") public void signIn(HttpServletRequest request) { log.info("/member/sign-in GET! - forwarding to sign-in.jsp"); // 요청 정보 헤더 안에는 Referer라는 키가 있는데 // 여기 안에는 이 페이지로 진입할 때 어디에서 왔는지 URI정보가 들어있음. String referer = request.getHeader("Referer"); log.info("referer: {}", referer); request.getSession().setAttribute("redirectURI", referer); } // 로그인 요청 처리 @PostMapping("/sign-in") public String signIn(LoginDTO inputData , Model model , HttpSession session // 세션정보 객체 ) { log.info("/member/sign-in POST - {}", inputData); // log.info("session timeout : {}", session.getMaxInactiveInterval()); // 로그인 서비스 호출 LoginFlag flag = memberService.login(inputData, session); if (flag == LoginFlag.SUCCESS) { log.info("login success!!"); String redirectURI = (String) session.getAttribute("redirectURI"); return "redirect:" + redirectURI; } model.addAttribute("loginMsg", flag); return "member/sign-in"; } @GetMapping("/sign-out") public String signOut(HttpSession session) { if (session.getAttribute("loginUser") != null) { // 1. 세션에서 정보를 삭제한다. session.removeAttribute("loginUser"); // 2. 세션을 무효화한다. session.invalidate(); return "redirect:/"; } return "redirect:/member/sign-in"; } }
View
include
header.jsp
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <!-- header --> <header> <div class="inner-header"> <h1 class="logo"> <a href="#"> <img src="/img/logo.png" alt="로고이미지"> </a> </h1> <h2 class="intro-text">Welcome <c:if test="${loginUser != null}"> ${loginUser.name}님 Hello!! </c:if> </h2> <a href="#" class="menu-open"> <span class="menu-txt">MENU</span> <span class="lnr lnr-menu"></span> </a> </div> <nav class="gnb"> <a href="#" class="close"> <span class="lnr lnr-cross"></span> </a> <ul> <li><a href="/">Home</a></li> <li><a href="#">About</a></li> <li><a href="/board/list">Board</a></li> <li><a href="#">Contact</a></li> <c:if test="${loginUser == null}"> <li><a href="/member/sign-up">Sign Up</a></li> <li><a href="/member/sign-in">Sign In</a></li> </c:if> <c:if test="${loginUser != null}"> <li><a href="#">My Page</a></li> <li><a href="/member/sign-out">Sign Out</a></li> </c:if> </ul> </nav> </header> <!-- //header -->sign-up.jsp
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html lang="ko"> <head> <%@ include file="../include/static-head.jsp" %> <style> .wrap { margin: 200px auto; } .c-red { color: #e00; } .c-blue { color: rgb(25, 236, 120); } </style> </head> <body> <%@ include file="../include/header.jsp" %> <div class="container wrap"> <div class="row"> <div class="offset-md-2 col-md-4"> <div class="card" style="width:200%;"> <div class="card-header text-white" style="background: #343A40;"> <h2><span style="color: gray;">MVC</span> 회원 가입</h2> </div> <div class="card-body"> <form action="/member/sign-up" name="signup" id="signUpForm" method="post" style="margin-bottom: 0;"> <table style="cellpadding: 0; cellspacing: 0; margin: 0 auto; width: 100%"> <tr> <td style="text-align: left"> <p><strong>아이디를 입력해주세요.</strong> <span id="idChk"></span></p> </td> </tr> <tr> <td><input type="text" name="account" id="user_id" class="form-control tooltipstered" maxlength="14" required="required" aria-required="true" style="margin-bottom: 25px; width: 100%; height: 40px; border: 1px solid #d9d9de" placeholder="숫자와 영어로 4-14자"> </td> </tr> <tr> <td style="text-align: left"> <p><strong>비밀번호를 입력해주세요.</strong> <span id="pwChk"></span></p> </td> </tr> <tr> <td><input type="password" size="17" maxlength="20" id="password" name="password" class="form-control tooltipstered" maxlength="20" required="required" aria-required="true" style="ime-mode: inactive; margin-bottom: 25px; height: 40px; border: 1px solid #d9d9de" placeholder="영문과 특수문자를 포함한 최소 8자"></td> </tr> <tr> <td style="text-align: left"> <p><strong>비밀번호를 재확인해주세요.</strong> <span id="pwChk2"></span> </p> </td> </tr> <tr> <td><input type="password" size="17" maxlength="20" id="password_check" name="pw_check" class="form-control tooltipstered" maxlength="20" required="required" aria-required="true" style="ime-mode: inactive; margin-bottom: 25px; height: 40px; border: 1px solid #d9d9de" placeholder="비밀번호가 일치해야합니다."></td> </tr> <tr> <td style="text-align: left"> <p><strong>이름을 입력해주세요.</strong> <span id="nameChk"></span></p> </td> </tr> <tr> <td><input type="text" name="name" id="user_name" class="form-control tooltipstered" maxlength="6" required="required" aria-required="true" style="margin-bottom: 25px; width: 100%; height: 40px; border: 1px solid #d9d9de" placeholder="한글로 최대 6자"></td> </tr> <tr> <td style="text-align: left"> <p><strong>이메일을 입력해주세요.</strong> <span id="emailChk"></span> </p> </td> </tr> <tr> <td><input type="email" name="email" id="user_email" class="form-control tooltipstered" required="required" aria-required="true" style="margin-bottom: 25px; width: 100%; height: 40px; border: 1px solid #d9d9de" placeholder="ex) abc@mvc.com"></td> </tr> <tr> <td style="padding-top: 10px; text-align: center"> <p><strong>회원가입하셔서 더 많은 서비스를 사용하세요~~!</strong></p> </td> </tr> <tr> <td style="width: 100%; text-align: center; colspan: 2;"> <input type="button" value="회원가입" class="btn form-control tooltipstered" id="signup-btn" style="background: gray; margin-top: 0; height: 40px; color: white; border: 0px solid #388E3C; opacity: 0.8"> </td> </tr> </table> </form> </div> </div> </div> </div> </div> <script> // 회원가입 폼 검증 $(document).ready(function () { //입력값 검증 정규표현식 const getIdCheck = RegExp(/^[a-zA-Z0-9]{4,14}$/); const getPwCheck = RegExp( /([a-zA-Z0-9].*[!,@,#,$,%,^,&,*,?,_,~])|([!,@,#,$,%,^,&,*,?,_,~].*[a-zA-Z0-9])/); const getName = RegExp(/^[가-힣]+$/); const getMail = RegExp(/^[A-Za-z0-9_\.\-]+@[A-Za-z0-9\-]+\.[A-Za-z0-9\-]+/); // 입력값 검증 배열 // 1: 아이디, 2: 비번, 3: 비번확인, 4: 이름, 5: 이메일 const checkArr = [false, false, false, false, false]; // 1. 아이디 검증 const $idInput = $('#user_id'); $idInput.on('keyup', e => { // 아이디를 입력하지 않은 경우 if ($idInput.val().trim() === '') { $idInput.css('border-color', 'red'); $('#idChk').html('<b class="c-red">[아이디는 필수 정보입니다.]</b>'); checkArr[0] = false; } // 아이디를 패턴에 맞지 않게 입력하였을 경우 // test() 메서드는 정규표현식을 검증하여 입력값이 표현식과 // 일치하면 true, 일치하지 않으면 false를 리턴 else if (!getIdCheck.test($idInput.val())) { $idInput.css('border-color', 'red'); $('#idChk').html('<b class="c-red">[영문, 숫자로 4~14자 사이로 작성하세요!]</b>'); checkArr[0] = false; } // 아이디 중복 확인 검증 else { fetch('/member/check?type=account&value=' + $idInput.val()) .then(res => res.text()) .then(flag => { console.log('flag:', flag); if (flag === 'true') { $idInput.css('border-color', 'red'); $('#idChk').html('<b class="c-red">[중복된 아이디입니다.]</b>'); checkArr[0] = false; } else { // 정상적으로 입력한 경우 $idInput.css('border-color', 'skyblue'); $('#idChk').html('<b class="c-blue">[사용가능한 아이디입니다.]</b>'); checkArr[0] = true; } }); } }); //end id check event //2. 패스워드 입력값 검증. $('#password').on('keyup', function () { //비밀번호 공백 확인 if ($("#password").val() === "") { $('#password').css('border-color', 'red'); $('#pwChk').html('<b class="c-red">[패스워드는 필수정보!]</b>'); checkArr[1] = false; } //비밀번호 유효성검사 else if (!getPwCheck.test($("#password").val()) || $("#password").val().length < 8) { $('#password').css('border-color', 'red'); $('#pwChk').html('<b class="c-red">[특수문자 포함 8자이상]</b>'); checkArr[1] = false; } else { $('#password').css('border-color', 'skyblue'); $('#pwChk').html('<b class="c-blue">[참 잘했어요]</b>'); checkArr[1] = true; } }); //패스워드 확인란 입력값 검증. $('#password_check').on('keyup', function () { //비밀번호 확인란 공백 확인 if ($("#password_check").val() === "") { $('#password_check').css('border-color', 'red'); $('#pwChk2').html('<b class="c-red">[패스워드확인은 필수정보!]</b>'); checkArr[2] = false; } //비밀번호 확인란 유효성검사 else if ($("#password").val() !== $("#password_check").val()) { $('#password_check').css('border-color', 'red'); $('#pwChk2').html('<b class="c-red">[위에랑 똑같이!!]</b>'); checkArr[2] = false; } else { $('#password_check').css('border-color', 'skyblue'); $('#pwChk2').html('<b class="c-blue">[참 잘했어요]</b>'); checkArr[2] = true; } }); //이름 입력값 검증. $('#user_name').on('keyup', function () { //이름값 공백 확인 if ($("#user_name").val() === "") { $('#user_name').css('border-color', 'red'); $('#nameChk').html('<b class="c-red">[이름은 필수정보!]</b>'); checkArr[3] = false; } //이름값 유효성검사 else if (!getName.test($("#user_name").val())) { $('#user_name').css('border-color', 'red'); $('#nameChk').html('<b class="c-red">[이름은 한글로 ~]</b>'); checkArr[3] = false; } else { $('#user_name').css('border-color', 'skyblue'); $('#nameChk').html('<b class="c-blue">[참 잘했어요]</b>'); checkArr[3] = true; } }); //이메일 입력값 검증. const $emailInput = $('#user_email'); $emailInput.on('keyup', function () { //이메일값 공백 확인 if ($emailInput.val() == "") { $emailInput.css('border-color', 'red'); $('#emailChk').html('<b class="c-red">[이메일은 필수정보에요!]</b>'); checkArr[4] = false; } //이메일값 유효성검사 else if (!getMail.test($emailInput.val())) { $emailInput.css('border-color', 'red'); $('#emailChk').html('<b class="c-red">[이메일 형식 몰라?]</b>'); checkArr[4] = false; } else { //이메일 중복확인 비동기 통신 fetch('/member/check?type=email&value=' + $emailInput.val()) .then(res => res.text()) .then(flag => { //console.log(flag); if (flag === 'true') { $emailInput.css('border-color', 'red'); $('#emailChk').html( '<b class="c-red">[이메일이 중복되었습니다!]</b>'); checkArr[4] = false; } else { $emailInput.css('border-color', 'skyblue'); $('#emailChk').html( '<b class="c-blue">[사용가능한 이메일입니다.]</b>' ); checkArr[4] = true; } }); } }); // 회원가입 양식 서버로 전송하는 클릭 이벤트 const $regForm = $('#signUpForm'); $('#signup-btn').on('click', e => { if (!checkArr.includes(false)) { $regForm.submit(); } else { alert('입력란을 다시 확인하세요!'); } }); }); // end jQuery </script> <%@ include file="../include/footer.jsp" %> </body> </html>sign-in.jsp
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html lang="ko"> <head> <%@ include file="../include/static-head.jsp" %> <style> .wrap { margin: 200px auto; } </style> </head> <body> <%@ include file="../include/header.jsp" %> <div class="container wrap"> <div class="row"> <div class="offset-md-2 col-md-4"> <div class="card" style="width:200%;"> <div class="card-header text-white" style="background: #343A40;"> <h2><span style="color: gray;">MVC</span> 로그인</h2> </div> <div class="card-body"> <form action="/member/sign-in" name="sign-in" method="post" id="signInForm" style="margin-bottom: 0;"> <table style="cellpadding: 0; cellspacing: 0; margin: 0 auto; width: 100%"> <tr> <td style="text-align: left"> <p><strong>아이디를 입력해주세요.</strong> <span id="idCheck"></span></p> </td> </tr> <tr> <td><input type="text" name="account" id="signInId" class="form-control tooltipstered" maxlength="10" required="required" aria-required="true" style="margin-bottom: 25px; width: 100%; height: 40px; border: 1px solid #d9d9de" placeholder="최대 10자"></td> </tr> <tr> <td style="text-align: left"> <p><strong>비밀번호를 입력해주세요.</strong> <span id="pwCheck"></span></p> </td> </tr> <tr> <td><input type="password" size="17" maxlength="20" id="signInPw" name="password" class="form-control tooltipstered" maxlength="20" required="required" aria-required="true" style="ime-mode: inactive; margin-bottom: 25px; height: 40px; border: 1px solid #d9d9de" placeholder="최소 8자"></td> </tr> <!-- 자동 로그인 체크박스 --> <tr> <td> <label for="auto-login"> <span> <i class="fas fa-sign-in-alt"></i> 자동 로그인 <input type="checkbox" id="auto-login" name="autoLogin"> </span> </label> </td> </tr> <tr> <td style="padding-top: 10px; text-align: center"> <p><strong>로그인하셔서 더 많은 서비스를 이용해보세요!</strong></p> </td> </tr> <tr> <td style="width: 100%; text-align: center; colspan: 2;"><input type="submit" value="로그인" class="btn form-control tooltipstered" id="signIn-btn" style="background-color: #343A40; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8"> </td> </tr> <tr> <td style="width: 100%; text-align: center; colspan: 2; margin-top: 24px; padding-top: 12px; border-top: 1px solid #ececec"> <a class="btn form-control tooltipstered" href="/member/sign-up" style="cursor: pointer; margin-top: 0; height: 40px; color: white; background-color: gray; border: 0px solid #388E3C; opacity: 0.8"> 회원가입</a> </td> </tr> <tr> <td style="width: 100%; text-align: center; colspan: 2; margin-top: 24px; padding-top: 12px; border-top: 1px solid #ececec"> <a id="custom-login-btn" href="https://kauth.kakao.com/oauth/authorize?client_id=9727a2bba3b021a605228cd4978e3491&redirect_uri=http://localhost/auth/kakao&response_type=code"> <img src="//mud-kage.kakao.com/14/dn/btqbjxsO6vP/KPiGpdnsubSq3a0PHEGUK1/o.jpg" width="300"/> </a> </td> </tr> </table> </form> </div> </div> </div> </div> </div> <script> const msg = '${msg}'; if (msg === 'reg-success') { alert('축하합니다. 회원가입에 성공했습니다.'); } const loginMsg = '${loginMsg}'; if (loginMsg === 'NO_ACC') { alert('존재하지 않는 회원입니다.'); } else if (loginMsg === 'NO_PW') { alert('비밀번호가 틀렸습니다.'); } const warning = '${warningMsg}'; if (warning === 'forbidden') { alert('로그인 후 사용할 수 있습니다.'); } </script> <%@ include file="../include/footer.jsp" %> </body>
resources
member.repository
MemberMapper.xml
<?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 namespace="com.project.web_prj.member.repository.MemberMapper"> <resultMap id="memberMap" type="com.project.web_prj.member.domain.Member"> <result column="reg_date" property="regDate" /> </resultMap> <!-- 회원 가입 기능 --> <insert id="register"> INSERT INTO tbl_member (account, password, name, email) VALUES (#{account}, #{password}, #{name}, #{email}) </insert> <!-- 중복체크 기능 (아이디, 이메일) --> <select id="isDuplicate" resultType="int"> SELECT COUNT(*) FROM tbl_member <if test="type=='account'"> WHERE account = #{value} </if> <if test="type=='email'"> WHERE email = #{value} </if> </select> <!-- 회원 조회 기능 --> <select id="findUser" resultMap="memberMap"> SELECT * FROM tbl_member WHERE account = #{account} </select> </mapper>
Test
repository
MemberMapperTest
package com.project.web_prj.member.repository; import com.project.web_prj.member.domain.Auth; import com.project.web_prj.member.domain.Member; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class MemberMapperTest { @Autowired MemberMapper mapper; @Autowired BCryptPasswordEncoder encoder; @Test @DisplayName("회원가입에 성공해야 한다.") void registerTest() { Member m = new Member(); m.setAccount("apple123"); m.setPassword("12345"); m.setName("사과왕"); m.setEmail("apple@gmail.com"); m.setAuth(Auth.ADMIN); boolean flag = mapper.register(m); assertTrue(flag); } @Test @DisplayName("비밀번호가 암호화인코딩 되어야 한다.") void encodePasswordTest() { // 인코딩 전 비밀번호 String rawPassword = "ddd5555"; // 인코딩을 위한 객체 생성 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); // 인코딩 후 비밀번호 String encodePassword = encoder.encode(rawPassword); System.out.println("rawPassword = " + rawPassword); System.out.println("encodePassword = " + encodePassword); } @Test @DisplayName("회원가입에 비밀번호가 인코딩된 상태로 성공해야 한다.") void registerTest2() { Member m = new Member(); m.setAccount("peach"); m.setPassword(new BCryptPasswordEncoder().encode("1234")); m.setName("천도복숭아"); m.setEmail("peach@gmail.com"); m.setAuth(Auth.ADMIN); boolean flag = mapper.register(m); assertTrue(flag); } @Test @DisplayName("특정 계정명으로 회원을 조회해야 한다.") void findUserTest() { //given String account = "peach"; //when Member member = mapper.findUser(account); //then System.out.println("member = " + member); assertEquals("천도복숭아", member.getName()); } @Test @DisplayName("특정 계정명으로 회원을 조회할수 없어야 한다.") void findUserTest2() { //given String account = "peach123"; //when Member member = mapper.findUser(account); //then assertNull(member); } @Test @DisplayName("아이디를 중복확인 할 수 있다.") void checkAccountTest() { //given Map<String, Object> checkMap = new HashMap<>(); checkMap.put("type", "account"); checkMap.put("value", "peach"); //when int flagNumber = mapper.isDuplicate(checkMap); //then assertEquals(1, flagNumber); } @Test @DisplayName("이메일을 중복확인 할 수 있다.") void checkEmailTest() { //given Map<String, Object> checkMap = new HashMap<>(); checkMap.put("type", "email"); checkMap.put("value", "peach@gmail.com"); //when int flagNumber = mapper.isDuplicate(checkMap); //then assertEquals(1, flagNumber); } @Test @DisplayName("로그인을 검증해야 한다.") void signInTest() { // 로그인 시도 계정, 패스워드 String inputId = "apeach"; String inputPw = "aaa1234!"; // 1. 로그인 시도한 계정명으로 회원정보 조회 Member foundMember = mapper.findUser(inputId); // 2. 회원가입 여부를 먼저 확인한다. if (foundMember != null) { //3. 패스워드를 대조한다. // 실제 회원의 비밀번호를 가져온다. String dbPw = foundMember.getPassword(); //4. 암호화된 패스워드를 디코딩하여 비교 if (encoder.matches(inputPw, dbPw)) { System.out.println("로그인 성공"); } else { System.out.println("비밀번호가 틀렸습니다."); } } else { System.out.println("존재하지 않는 아이디입니다."); } } }
service
MemberServiceTest
package com.project.web_prj.member.service; import com.project.web_prj.member.domain.Auth; import com.project.web_prj.member.domain.Member; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class MemberServiceTest { @Autowired MemberService service; @Test @DisplayName("평문 비밀번호로 회원가입하면 암호화되어 저장된다.") void signUpTest() { Member m = new Member(); m.setAccount("banana"); m.setPassword("bbb1234"); m.setName("돌빠나나"); m.setEmail("banana@hanmail.net"); m.setAuth(Auth.COMMON); service.signUp(m); } @Test @DisplayName("중복된 아이디를 전달하면 true가 나와야 한다.") void checkAccountServiceTest() { //given String account = "banana"; //when boolean flag = service.checkSignUpValue("account", account); //then assertTrue(flag); } }
인가
인터셉터
클라이언트가 WAS에 진입 직전에 ‘필터’를 통해 접근을 막을 수 있지만 인터셉터는 좀더 작은 내용
인터셉터는 디스패처 컨트롤러가 하위 컨트롤러 진입 직전에 막는 것
가로채서 먼저 처리하는 것은 PreHndle 하위 컨트롤러에서 상위 컨트롤러로 갈 때 전처리를 하면 postHandle
BoardController
package com.project.web_prj.board.controller; import com.project.web_prj.board.domain.Board; import com.project.web_prj.board.service.BoardService; import com.project.web_prj.common.paging.Page; import com.project.web_prj.common.paging.PageMaker; import com.project.web_prj.common.search.Search; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.ArrayList; import java.util.List; import java.util.Map; /** *게시물 목록요청: /board/list: GET *게시물 상세조회요청: /board/content: GET *게시글 쓰기 화면요청: /board/write: GET *게시글 등록요청: /board/write: POST *게시글 삭제요청: /board/delete: GET *게시글 수정화면요청: /board/modify: GET *게시글 수정요청: /board/modify: POST */ @Controller @Log4j2 @RequiredArgsConstructor @RequestMapping("/board") public class BoardController { private final BoardService boardService; // 게시물 목록 요청 @GetMapping("/list") public String list(@ModelAttribute("s") Search search, Model model) { log.info("controller request /board/list GET! - search: {}", search); Map<String, Object> boardMap = boardService.findAllService(search); log.debug("return data - {}", boardMap); // 페이지 정보 생성 PageMaker pm = new PageMaker( new Page(search.getPageNum(), search.getAmount()) , (Integer) boardMap.get("tc")); model.addAttribute("bList", boardMap.get("bList")); model.addAttribute("pm", pm); return "board/board-list"; } // 게시물 상세 조회 요청 @GetMapping("/content/{boardNo}") public String content(@PathVariable Long boardNo , Model model, HttpServletResponse response, HttpServletRequest request , @ModelAttribute("p") Page page //받은걸 바로 p에 담는다 ) { log.info("controller request /board/content GET! - {}", boardNo); Board board = boardService.findOneService(boardNo, response, request); log.info("return data - {}", board); model.addAttribute("b", board); return "board/board-detail"; } // 게시물 쓰기 화면 요청 @GetMapping("/write") public String write(HttpSession session, RedirectAttributes ra) { if (session.getAttribute("loginUser") == null) { ra.addFlashAttribute("warningMsg", "forbidden"); return "redirect:/member/sign-in"; } log.info("controller request /board/write GET!"); return "board/board-write"; } // 게시물 등록 요청 @PostMapping("/write") public String write(Board board, @RequestParam("files") List<MultipartFile> fileList, RedirectAttributes ra) { log.info("controller request /board/write POST! - {}", board); /*if (fileList != null) { List<String> fileNames = new ArrayList<>(); for (MultipartFile f : fileList) { log.info("attachmented file-name: {}", f.getOriginalFilename()); fileNames.add(f.getOriginalFilename()); } // board객체에 파일명 추가 board.setFileNames(fileNames); }*/ boolean flag = boardService.saveService(board); // 게시물 등록에 성공하면 클라이언트에 성공메시지 전송 if (flag) ra.addFlashAttribute("msg", "reg-success"); return flag ? "redirect:/board/list" : "redirect:/"; } // 게시물 삭제 요청 @GetMapping("/delete") public String delete(Long boardNo) { log.info("controller request /board/delete GET! - bno: {}", boardNo); return boardService.removeService(boardNo) ? "redirect:/board/list" : "redirect:/"; } // 수정 화면 요청 @GetMapping("/modify") public String modify(Long boardNo, Model model, HttpServletRequest request, HttpServletResponse response) { log.info("controller request /board/modify GET! - bno: {}", boardNo); Board board = boardService.findOneService(boardNo, response, request); log.info("find article: {}", board); model.addAttribute("board", board); return "board/board-modify"; } // 수정 처리 요청 @PostMapping("/modify") public String modify(Board board) { log.info("controller request /board/modify POST! - {}", board); boolean flag = boardService.modifyService(board); return flag ? "redirect:/board/content/" + board.getBoardNo() : "redirect:/"; } // 특정 게시물에 붙은 첨부파일경로 리스트를 클라이언트에게 비동기 전송 @GetMapping("/file/{bno}") @ResponseBody public ResponseEntity<List<String>> getFiles(@PathVariable Long bno) { List<String> files = boardService.getFiles(bno); log.info("/board/file/{} GET! ASYNC - {}", bno, files); return new ResponseEntity<>(files, HttpStatus.OK); } }
Interceptor
Controller 진입 직전에 시작됨
InterceptorConfig
**package com.project.web_prj.config; import com.project.web_prj.interceptor.AfterLoginInterceptor; import com.project.web_prj.interceptor.BoardInterceptor; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // 다양한 인터셉터들을 관리하는 설정 클래스 @Configuration @RequiredArgsConstructor public class InterceptorConfig implements WebMvcConfigurer { private final BoardInterceptor boardInterceptor; private final AfterLoginInterceptor afterLoginInterceptor; // 인터셉터 설정 추가 메서드 @Override public void addInterceptors(InterceptorRegistry registry) { // 게시판 인터셉터 설정 registry.addInterceptor(boardInterceptor) .addPathPatterns("/board/*")//'/board 로 시작하면 다 검사 .excludePathPatterns("/board/list", "/board/content"); //이 줄은 예외 // 애프터 로그인 인터셉터 설정 registry.addInterceptor(afterLoginInterceptor) .addPathPatterns("/member/sign-in", "/member/sign-up");//로그인을 한 경우 로그인,회원가입 창 접근 불가하게 } }**Util.LoginUtils
package com.project.web_prj.util; import javax.servlet.http.HttpSession; public class LoginUtils { public static final String LOGIN_FLAG = "loginUser"; // 로그인했는지 알려주기~~ public static boolean isLogin(HttpSession session) { return session.getAttribute(LOGIN_FLAG) != null; } }BoardInterceptor
package com.project.web_prj.interceptor; import com.project.web_prj.util.LoginUtils; import lombok.extern.log4j.Log4j2; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import static com.project.web_prj.util.LoginUtils.*; // 인터셉터: 컨트롤러에 요청이 들어가기 전, 후에 공통처리할 // 일들을 정의해놓는 클래스 @Configuration @Log4j2 public class BoardInterceptor implements HandlerInterceptor { /* 인터셉터의 전처리 메서드. 리턴값이 true일 경우 컨트롤러 진입을 허용하고 false일 경우 진입을 허용하지 않는다. */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); // 포워드 // RequestDispatcher dispatcher // = request.getRequestDispatcher("/WEB-INF/views/member/sign-in.jsp"); log.info("board interceptor preHandle()"); if (!isLogin(session)) { log.info("this request deny!! 집에 가"); // dispatcher.forward(request, response); response.sendRedirect("/member/sign-in"); return false; } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("board interceptor postHandle() ! "); } }AfterInterceptor
package com.project.web_prj.interceptor; import com.project.web_prj.util.LoginUtils; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import static com.project.web_prj.util.LoginUtils.*; @Configuration public class AfterLoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (isLogin(session)) { response.sendRedirect("/"); return false; } return true; } }
권한 설정
SQL
-- Oracle
-- 회원 관리 테이블
CREATE TABLE tbl_member (
account VARCHAR2(50),
password VARCHAR2(150) NOT NULL,
name VARCHAR2(50) NOT NULL,
email VARCHAR2(100) NOT NULL UNIQUE,
auth VARCHAR2(20) DEFAULT 'COMMON',
reg_date DATE DEFAULT SYSDATE,
CONSTRAINT pk_member PRIMARY KEY (account)
);
ALTER TABLE tbl_board ADD account VARCHAR2(50) NOT NULL;
ALTER TABLE tbl_reply ADD account VARCHAR2(50) NOT NULL;
SELECT * FROM tbl_member;
-- mariadb
-- 회원 관리 테이블
CREATE TABLE tbl_member (
account VARCHAR(50),
password VARCHAR(150) NOT NULL,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
auth VARCHAR(20) DEFAULT 'COMMON',
reg_date DATETIME DEFAULT current_timestamp,
CONSTRAINT pk_member PRIMARY KEY (account)
);
ALTER TABLE tbl_board ADD account VARCHAR(50) NOT NULL;
ALTER TABLE tbl_reply ADD account VARCHAR(50) NOT NULL;
Mapper
BoardMapper.xml
<?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 namespace="com.project.web_prj.board.repository.BoardMapper"> <resultMap id="boardMap" type="com.project.web_prj.board.domain.Board"> <result property="boardNo" column="board_no" /> <result property="regDate" column="reg_date" /> <result property="viewCnt" column="view_cnt" /> </resultMap> <!-- 동적 SQL 코드 재사용하기 --> <sql id="search"> <if test="type == 'title'">WHERE title LIKE CONCAT('%', #{keyword}, '%')</if> <if test="type == 'writer'">WHERE writer LIKE CONCAT('%', #{keyword}, '%')</if> <if test="type == 'content'">WHERE content LIKE CONCAT('%', #{keyword}, '%')</if> <if test="type == 'tc'"> WHERE title LIKE CONCAT('%', #{keyword}, '%') OR content LIKE CONCAT('%', #{keyword}, '%') </if> </sql> <insert id="save"> INSERT INTO tbl_board (writer, title, content, account) VALUES (#{writer}, #{title}, #{content}, #{account}) </insert> <select id="findAll" resultMap="boardMap"> SELECT * FROM tbl_board ORDER BY board_no DESC LIMIT #{start}, #{amount} </select> <select id="findAll2" resultMap="boardMap"> SELECT * FROM tbl_board <include refid="search" /> ORDER BY board_no DESC LIMIT #{start}, #{amount} <!-- SELECT *--> <!-- FROM (--> <!-- SELECT ROWNUM rn, v_board.*--> <!-- FROM (--> <!-- SELECT *--> <!-- FROM tbl_board--> <!-- <if test="type == 'title'">WHERE title LIKE '%' || #{keyword} || '%'</if>--> <!-- <if test="type == 'writer'">WHERE writer LIKE '%' || #{keyword} || '%'</if>--> <!-- <if test="type == 'content'">WHERE content LIKE '%' || #{keyword} || '%'</if>--> <!-- <if test="type == 'tc'">--> <!-- WHERE title LIKE '%' || #{keyword} || '%'--> <!-- OR content LIKE '%' || #{keyword} || '%'--> <!-- </if>--> <!-- ORDER BY board_no DESC--> <!-- ) v_board--> <!-- )--> <!-- WHERE rn BETWEEN (#{pageNum} - 1) * #{amount} + 1 AND (#{pageNum} * #{amount})--> </select> <select id="findOne" resultMap="boardMap"> SELECT * FROM tbl_board WHERE board_no=#{boardNo} </select> <delete id="remove"> DELETE FROM tbl_board WHERE board_no=#{boardNo} </delete> <update id="modify"> UPDATE tbl_board SET writer = #{writer}, title=#{title}, content=#{content} WHERE board_no=#{boardNo} </update> <select id="getTotalCount" resultType="int"> SELECT COUNT(*) FROM tbl_board </select> <select id="getTotalCount2" resultType="int"> SELECT COUNT(*) FROM tbl_board <include refid="search" /> </select> <update id="upViewCount"> UPDATE tbl_board SET view_cnt = view_cnt + 1 WHERE board_no=#{boardNo} </update> <!-- 첨부파일 추가 --> <insert id="addFile"> INSERT INTO file_upload (file_name, bno) VALUES (#{fileName}, LAST_INSERT_ID()) </insert> <select id="findFileNames" resultType="string"> SELECT file_name FROM file_upload WHERE bno = #{bno} </select> <select id="findMemberByBoardNo" resultType="com.project.web_prj.board.dto.ValidateMemberDTO"> SELECT account, auth FROM tbl_member WHERE account = ( SELECT account FROM tbl_board WHERE board_no = #{boardNo} ) </select> </mapper>ReplyMapper.xml
<?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 namespace="com.project.web_prj.reply.repository.ReplyMapper"> <resultMap id="replyMap" type="com.project.web_prj.reply.domain.Reply"> <result property="replyNo" column="reply_no" /> <result property="replyText" column="reply_text" /> <result property="replyWriter" column="reply_writer" /> <result property="replyDate" column="reply_date" /> <result property="boardNo" column="board_no" /> </resultMap> <insert id="save"> <!-- INSERT INTO tbl_reply--> <!-- (reply_no, reply_text, reply_writer, board_no)--> <!-- VALUES--> <!-- (seq_tbl_reply.nextval, #{replyText}, #{replyWriter}, #{boardNo})--> INSERT INTO tbl_reply (reply_text, reply_writer, board_no, account) VALUES (#{replyText}, #{replyWriter}, #{boardNo}, #{account}) </insert> <!-- 댓글 수정 --> <update id="modify"> UPDATE tbl_reply SET reply_text = #{replyText} WHERE reply_no = #{replyNo} </update> <!-- 댓글 삭제 --> <delete id="remove"> DELETE FROM tbl_reply WHERE reply_no = #{replyNo} </delete> <!-- 댓글 전체 삭제 --> <delete id="removeAll"> DELETE FROM tbl_reply WHERE board_no = #{boardNo} </delete> <!-- 댓글 개별조회 --> <select id="findOne" resultMap="replyMap"> SELECT * FROM tbl_reply WHERE reply_no = #{replyNo} </select> <!-- 댓글 목록 조회 --> <select id="findAll" resultMap="replyMap"> <!-- SELECT *--> <!-- FROM (--> <!-- SELECT ROWNUM rn, v_reply.*--> <!-- FROM (--> <!-- SELECT *--> <!-- FROM tbl_reply--> <!-- WHERE board_no = #{boardNo}--> <!-- ORDER BY board_no DESC--> <!-- ) v_reply--> <!-- )--> <!-- WHERE rn BETWEEN (#{page.pageNum} - 1) * #{page.amount} + 1 AND (#{page.pageNum} * #{page.amount})--> SELECT * FROM tbl_reply WHERE board_no = #{boardNo} ORDER BY reply_no LIMIT #{page.start}, #{page.amount} </select> <select id="getReplyCount" resultType="int"> SELECT COUNT(*) FROM tbl_reply WHERE board_no=#{boardNo} </select> </mapper>
domain
Board
package com.project.web_prj.board.domain; import lombok.*; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Date; import java.util.List; @Setter @Getter @ToString @EqualsAndHashCode @NoArgsConstructor @AllArgsConstructor public class Board { // 테이블 컬럼 필드 private Long boardNo; private String writer; private String title; private String content; private Long viewCnt; private Date regDate; private String account; // 커스텀 데이터 필드 private String shortTitle; // 줄임 제목 private String prettierDate; // 변경된 날짜포맷 문자열 private boolean newArticle; // 신규 게시물 여부 private int replyCount; // 댓글 수 private List<String> fileNames; // 첨부파일들의 이름 목록 public Board(ResultSet rs) throws SQLException { this.boardNo = rs.getLong("board_no"); this.title = rs.getString("title"); this.writer = rs.getString("writer"); this.content = rs.getString("content"); this.viewCnt = rs.getLong("view_cnt"); this.regDate = rs.getTimestamp("reg_date"); } }Reply
package com.project.web_prj.reply.domain; import lombok.*; import java.util.Date; @Setter @Getter @ToString @NoArgsConstructor @AllArgsConstructor @Builder public class Reply { private Long replyNo; //댓글번호 private String replyText; //댓글내용 private String replyWriter; //댓글작성자 private Date replyDate; //작성일자 private Long boardNo; //원본 글번호 private String account; // 작성자 아이디 }
DTO
ValidateMemberDTO
package com.project.web_prj.board.dto; import com.project.web_prj.member.domain.Auth; import lombok.*; @Getter @Setter @ToString //@NoArgsConstructor @AllArgsConstructor public class ValidateMemberDTO { private String account; private Auth auth; // 회원제 사이트 이전에 만들었던 글 때문에 @NoArgsConstructor 날리고 밑에 넣음 public ValidateMemberDTO() { account="no"; auth = Auth.COMMON; } }
repository
BoardMapper
package com.project.web_prj.board.repository; import com.project.web_prj.board.domain.Board; import com.project.web_prj.board.dto.ValidateMemberDTO; import com.project.web_prj.common.paging.Page; import com.project.web_prj.common.search.Search; import com.project.web_prj.member.domain.Member; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface BoardMapper { // 게시글 쓰기 기능 boolean save(Board board); // 게시글 전체 조회 List<Board> findAll(); // 게시글 전체 조회 with paging List<Board> findAll(Page page); // 게시글 전체 조회 with searching List<Board> findAll2(Search search); // 게시글 상세 조회 Board findOne(Long boardNo); // 게시글 삭제 boolean remove(Long boardNo); // 게시글 수정 boolean modify(Board board); // 전체 게시물 수 조회 int getTotalCount(); int getTotalCount2(Search search); // 조회수 상승 처리 void upViewCount(Long boardNo); // 파일 첨부 기능 처리 void addFile(String fileName); // 게시물에 붙어있는 첨부파일경로명 전부 조회하기 List<String> findFileNames(Long bno); // 게시물 번호로 게시글 작성자의 계정명과 권한 가져오기 ValidateMemberDTO findMemberByBoardNo(Long boardNo); }
Service
BoardService
package com.project.web_prj.board.service; import com.project.web_prj.board.domain.Board; import com.project.web_prj.board.dto.ValidateMemberDTO; import com.project.web_prj.board.repository.BoardMapper; import com.project.web_prj.common.paging.Page; import com.project.web_prj.common.search.Search; import com.project.web_prj.reply.repository.ReplyMapper; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.util.WebUtils; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @Service @Log4j2 @RequiredArgsConstructor public class BoardService { // private final BoardRepository repository; private final BoardMapper boardMapper; private final ReplyMapper replyMapper; // 게시물 등록 요청 중간 처리 @Transactional public boolean saveService(Board board) { log.info("save service start - {}", board); // 게시물 내용 DB에 저장 boolean flag = boardMapper.save(board); List<String> fileNames = board.getFileNames(); if (fileNames != null && fileNames.size() > 0) { for (String fileName : fileNames) { // 첨부파일 내용 DB에 저장 boardMapper.addFile(fileName); } } return flag; } // 게시물 전체 조회 요청 중간 처리 public List<Board> findAllService() { log.info("findAll service start"); List<Board> boardList = boardMapper.findAll(); // 목록 중간 데이터처리 processConverting(boardList); return boardList; } // 게시물 전체 조회 요청 중간 처리 with paging public Map<String, Object> findAllService(Page page) { log.info("findAll service start"); Map<String, Object> findDataMap = new HashMap<>(); List<Board> boardList = boardMapper.findAll(page); // 목록 중간 데이터 처리 processConverting(boardList); findDataMap.put("bList", boardList); findDataMap.put("tc", boardMapper.getTotalCount()); return findDataMap; } // 게시물 전체 조회 요청 중간 처리 with searching public Map<String, Object> findAllService(Search search) { log.info("findAll service start"); Map<String, Object> findDataMap = new HashMap<>(); List<Board> boardList = boardMapper.findAll2(search); // 목록 중간 데이터 처리 processConverting(boardList); findDataMap.put("bList", boardList); findDataMap.put("tc", boardMapper.getTotalCount2(search)); return findDataMap; } private void processConverting(List<Board> boardList) { for (Board b : boardList) { convertDateFormat(b); substringTitle(b); checkNewArticle(b); setReplyCount(b); } } private void setReplyCount(Board b) { b.setReplyCount(replyMapper.getReplyCount(b.getBoardNo())); } // 신규 게시물 여부 처리 private void checkNewArticle(Board b) { // 게시물의 작성일자와 현재 시간을 대조 // 게시물의 작성일자 가져오기 - 16억 5초 long regDateTime = b.getRegDate().getTime(); // 현재 시간 얻기 (밀리초) - 16억 10초 long nowTime = System.currentTimeMillis(); // 현재시간 - 작성시간 long diff = nowTime - regDateTime; // 신규 게시물 제한시간 long limitTime = 60 * 5 * 1000; if (diff < limitTime) { b.setNewArticle(true); } } private void convertDateFormat(Board b) { Date date = b.getRegDate(); SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd a hh:mm"); b.setPrettierDate(sdf.format(date)); } private void substringTitle(Board b) { // 만약에 글제목이 5글자 이상이라면 // 5글자만 보여주고 나머지는 ...처리 String title = b.getTitle(); if (title.length() > 5) { String subStr = title.substring(0, 5); b.setShortTitle(subStr + "..."); } else { b.setShortTitle(title); } } // 게시물 상세 조회 요청 중간 처리 @Transactional public Board findOneService(Long boardNo, HttpServletResponse response, HttpServletRequest request) { log.info("findOne service start - {}", boardNo); Board board = boardMapper.findOne(boardNo); // 해당 게시물 번호에 해당하는 쿠키가 있는지 확인 // 쿠키가 없으면 조회수를 상승시켜주고 쿠키를 만들어서 클라이언트에 전송 makeViewCount(boardNo, response, request); return board; } private void makeViewCount(Long boardNo, HttpServletResponse response, HttpServletRequest request) { // 쿠키를 조회 - 해당 이름의 쿠키가 있으면 쿠키가 들어오고 없으면 null이 들어옴 Cookie foundCookie = WebUtils.getCookie(request, "b" + boardNo); if (foundCookie == null) { boardMapper.upViewCount(boardNo); Cookie cookie = new Cookie("b" + boardNo, String.valueOf(boardNo));// 쿠키 생성 cookie.setMaxAge(60); // 쿠키 수명 설정 cookie.setPath("/board/content"); // 쿠키 작동 범위 response.addCookie(cookie); // 클라이언트에 쿠키 전송 } } // 게시물 삭제 요청 중간 처리 @Transactional public boolean removeService(Long boardNo) { log.info("remove service start - {}", boardNo); // 댓글 먼저 모두 삭제 replyMapper.removeAll(boardNo); // 원본 게시물 삭제 boolean remove = boardMapper.remove(boardNo); return remove; } // 게시물 수정 요청 중간 처리 public boolean modifyService(Board board) { log.info("modify service start - {}", board); return boardMapper.modify(board); } // 첨부파일 목록 가져오는 중간처리 public List<String> getFiles(Long bno) { return boardMapper.findFileNames(bno); } // 게시물 번호로 글쓴이 회원정보 가져오기 public ValidateMemberDTO getMember(Long boardNo) { ValidateMemberDTO member = boardMapper.findMemberByBoardNo(boardNo); // 비회원일 경우에 넣었던 글 출력하도록 하는 거 if (member == null) member = new ValidateMemberDTO(); return member; } }
Controller
BoardController
package com.project.web_prj.board.controller; import com.project.web_prj.board.domain.Board; import com.project.web_prj.board.service.BoardService; import com.project.web_prj.common.paging.Page; import com.project.web_prj.common.paging.PageMaker; import com.project.web_prj.common.search.Search; import com.project.web_prj.util.LoginUtils; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 게시물 목록요청: /board/list: GET * 게시물 상세조회요청: /board/content: GET * 게시글 쓰기 화면요청: /board/write: GET * 게시글 등록요청: /board/write: POST * 게시글 삭제요청: /board/delete: GET * 게시글 수정화면요청: /board/modify: GET * 게시글 수정요청: /board/modify: POST */ @Controller @Log4j2 @RequiredArgsConstructor @RequestMapping("/board") public class BoardController { private final BoardService boardService; // 게시물 목록 요청 @GetMapping("/list") public String list(@ModelAttribute("s") Search search, Model model) { log.info("controller request /board/list GET! - search: {}", search); Map<String, Object> boardMap = boardService.findAllService(search); log.debug("return data - {}", boardMap); // 페이지 정보 생성 PageMaker pm = new PageMaker( new Page(search.getPageNum(), search.getAmount()) , (Integer) boardMap.get("tc")); model.addAttribute("bList", boardMap.get("bList")); model.addAttribute("pm", pm); return "board/board-list"; } // 게시물 상세 조회 요청 @GetMapping("/content/{boardNo}") public String content(@PathVariable Long boardNo , Model model, HttpServletResponse response, HttpServletRequest request , @ModelAttribute("p") Page page //받은걸 바로 p에 담는다 ) { log.info("controller request /board/content GET! - {}", boardNo); Board board = boardService.findOneService(boardNo, response, request); log.info("return data - {}", board); model.addAttribute("b", board); return "board/board-detail"; } // 게시물 쓰기 화면 요청 @GetMapping("/write") public String write(HttpSession session, RedirectAttributes ra) { // if (session.getAttribute("loginUser") == null) { // ra.addFlashAttribute("warningMsg", "forbidden"); // return "redirect:/member/sign-in"; // } log.info("controller request /board/write GET!"); return "board/board-write"; } // 게시물 등록 요청 @PostMapping("/write") public String write(Board board, @RequestParam("files") List<MultipartFile> fileList, RedirectAttributes ra, HttpSession session ) { log.info("controller request /board/write POST! - {}", board); /*if (fileList != null) { List<String> fileNames = new ArrayList<>(); for (MultipartFile f : fileList) { log.info("attachmented file-name: {}", f.getOriginalFilename()); fileNames.add(f.getOriginalFilename()); } // board객체에 파일명 추가 board.setFileNames(fileNames); }*/ // 현재 로그인 사용자 계정명 추가, jsp에서 hidden으로 안하는 이유 board.setAccount(LoginUtils.getCurrentMemberAccount(session)); boolean flag = boardService.saveService(board); // 게시물 등록에 성공하면 클라이언트에 성공메시지 전송 if (flag) ra.addFlashAttribute("msg", "reg-success"); return flag ? "redirect:/board/list" : "redirect:/"; } // 게시물 삭제 확인 요청 @GetMapping("/delete") public String delete(@ModelAttribute("boardNo") Long boardNo, Model model) { log.info("controller request /board/delete GET! - bno: {}", boardNo); model.addAttribute("validate", boardService.getMember(boardNo)); return "board/process-delete"; } // 게시물 삭제 확정 요청 @PostMapping("/delete") public String delete(Long boardNo) { log.info("controller request /board/delete POST! - bno: {}", boardNo); return boardService.removeService(boardNo) ? "redirect:/board/list" : "redirect:/"; } // 수정 화면 요청 @GetMapping("/modify") public String modify(Long boardNo, Model model, HttpServletRequest request, HttpServletResponse response) { log.info("controller request /board/modify GET! - bno: {}", boardNo); Board board = boardService.findOneService(boardNo, response, request); log.info("find article: {}", board); model.addAttribute("board", board); model.addAttribute("validate", boardService.getMember(boardNo)); return "board/board-modify"; } // 수정 처리 요청 @PostMapping("/modify") public String modify(Board board) { log.info("controller request /board/modify POST! - {}", board); boolean flag = boardService.modifyService(board); return flag ? "redirect:/board/content/" + board.getBoardNo() : "redirect:/"; } // 특정 게시물에 붙은 첨부파일경로 리스트를 클라이언트에게 비동기 전송 @GetMapping("/file/{bno}") @ResponseBody public ResponseEntity<List<String>> getFiles(@PathVariable Long bno) { List<String> files = boardService.getFiles(bno); log.info("/board/file/{} GET! ASYNC - {}", bno, files); return new ResponseEntity<>(files, HttpStatus.OK); } }
Util
LoginUtils
package com.project.web_prj.util; import com.project.web_prj.member.domain.Member; import javax.servlet.http.HttpSession; public class LoginUtils { public static final String LOGIN_FLAG = "loginUser"; // 로그인했는지 알려주기~~ public static boolean isLogin(HttpSession session) { return session.getAttribute(LOGIN_FLAG) != null; } // 로그인한 사용자 계정 가져오기 public static String getCurrentMemberAccount(HttpSession session) { Member member = (Member) session.getAttribute(LOGIN_FLAG); return member.getAccount(); } // 로그인한 사용자 권한 가져오기 public static String getCurrentMemberAuth(HttpSession session) { Member member = (Member) session.getAttribute(LOGIN_FLAG); return member.getAuth().toString(); } }
View
board-write.jsp
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html lang="ko"> <head> <%@ include file="../include/static-head.jsp" %> <style> .write-container { width: 50%; margin: 200px auto 150px; font-size: 1.2em; } .fileDrop { width: 600px; height: 200px; border: 1px dashed gray; display: flex; justify-content: center; align-items: center; font-size: 1.5em; } .uploaded-list { display: flex; } .img-sizing { display: block; width: 100px; height: 100px; } </style> </head> <body> <div class="wrap"> <%@ include file="../include/header.jsp" %> <div class="write-container"> <form id="write-form" action="/board/write" method="post" autocomplete="off" enctype="multipart/form-data"> <%-- <input type="hidden" name="account" value="#{loginUser.account}"/> 이렇게 하면 글 정보를 클라이언트에서 받을 수 있다. --%> <div class="mb-3"> <label for="writer-input" class="form-label">작성자</label> <input type="text" class="form-control" id="writer-input" placeholder="이름" name="writer" maxlength="20" readonly value="${loginUser.name}"><!--읽을 수만 있도록 함--> </div> <div class="mb-3"> <label for="title-input" class="form-label">글제목</label> <input type="text" class="form-control" id="title-input" placeholder="제목" name="title"> </div> <div class="mb-3"> <label for="exampleFormControlTextarea1" class="form-label">내용</label> <textarea name="content" class="form-control" id="exampleFormControlTextarea1" rows="10"></textarea> </div> <!-- 첨부파일 드래그 앤 드롭 영역 --> <div class="form-group"> <div class="fileDrop"> <span>Drop Here!!</span> </div> <div class="uploadDiv"> <input type="file" name="files" id="ajax-file" style="display:none;"> </div> <!-- 업로드된 파일의 썸네일을 보여줄 영역 --> <div class="uploaded-list"> </div> </div> <div class="d-grid gap-2"> <button id="reg-btn" class="btn btn-dark" type="button">글 작성하기</button> <button id="to-list" class="btn btn-warning" type="button">목록으로</button> </div> </form> </div> <%@ include file="../include/footer.jsp" %> </div> <script> // 게시물 등록 입력값 검증 함수 function validateFormValue() { // 이름입력태그, 제목 입력태그 const $writerInput = document.getElementById('writer-input'); const $titleInput = document.getElementById('title-input'); let flag = false; // 입력 제대로하면 true로 변경 console.log('w: ', $writerInput.value); console.log('t: ', $titleInput.value); if ($writerInput.value.trim() === '') { alert('작성자는 필수값입니다~'); } else if ($titleInput.value.trim() === '') { alert('제목은 필수값입니다~'); } else { flag = true; } console.log('flag:', flag); return flag; } // 게시물 입력값 검증 const $regBtn = document.getElementById('reg-btn'); $regBtn.onclick = e => { // 입력값을 제대로 채우지 않았는지 확인 if (!validateFormValue()) { return; } // 필수 입력값을 잘 채웠으면 폼을 서브밋한다. const $form = document.getElementById('write-form'); $form.submit(); }; //목록버튼 이벤트 const $toList = document.getElementById('to-list'); $toList.onclick = e => { location.href = '/board/list'; }; </script> <script> // start JQuery $(document).ready(function () { function isImageFile(originFileName) { //정규표현식 const pattern = /jpg$|gif$|png$/i; return originFileName.match(pattern); } // 파일의 확장자에 따른 렌더링 처리 function checkExtType(fileName) { //원본 파일 명 추출 let originFileName = fileName.substring(fileName.indexOf("_") + 1); // hidden input을 만들어서 변환파일명을 서버로 넘김 const $hiddenInput = document.createElement('input'); $hiddenInput.setAttribute('type', 'hidden'); $hiddenInput.setAttribute('name', 'fileNames'); $hiddenInput.setAttribute('value', fileName); $('#write-form').append($hiddenInput); //확장자 추출후 이미지인지까지 확인 if (isImageFile(originFileName)) { // 파일이 이미지라면 const $img = document.createElement('img'); $img.classList.add('img-sizing'); $img.setAttribute('src', '/loadFile?fileName=' + fileName); $img.setAttribute('alt', originFileName); $('.uploaded-list').append($img); } // 이미지가 아니라면 다운로드 링크를 생성 else { const $a = document.createElement('a'); $a.setAttribute('href', '/loadFile?fileName=' + fileName); const $img = document.createElement('img'); $img.classList.add('img-sizing'); $img.setAttribute('src', '/img/file_icon.jpg'); $img.setAttribute('alt', originFileName); $a.append($img); $a.innerHTML += '<span>' + originFileName + '</span'; $('.uploaded-list').append($a); } } // 드롭한 파일을 화면에 보여주는 함수 function showFileData(fileNames) { // 이미지인지? 이미지가 아닌지에 따라 구분하여 처리 // 이미지면 썸네일을 렌더링하고 아니면 다운로드 링크를 렌더링한다. for (let fileName of fileNames) { checkExtType(fileName); } } // drag & drop 이벤트 const $dropBox = $('.fileDrop'); // drag 진입 이벤트 $dropBox.on('dragover dragenter', e => { e.preventDefault(); $dropBox .css('border-color', 'red') .css('background', 'lightgray'); }); // drag 탈출 이벤트 $dropBox.on('dragleave', e => { e.preventDefault(); $dropBox .css('border-color', 'gray') .css('background', 'transparent'); }); // drop 이벤트 $dropBox.on('drop', e => { e.preventDefault(); // console.log('드롭 이벤트 작동!'); // 드롭된 파일 정보를 서버로 전송 // 1. 드롭된 파일 데이터 읽기 // console.log(e); const files = e.originalEvent.dataTransfer.files; // console.log('drop file data: ', files); // 2. 읽은 파일 데이터를 input[type=file]태그에 저장 const $fileInput = $('#ajax-file'); $fileInput.prop('files', files); // console.log($fileInput); // 3. 파일 데이터를 비동기 전송하기 위해서는 FormData객체가 필요 const formData = new FormData(); // 4. 전송할 파일들을 전부 FormData안에 포장 for (let file of $fileInput[0].files) { formData.append('files', file); } // 5. 비동기 요청 전송 const reqInfo = { method: 'POST', body: formData }; fetch('/ajax-upload', reqInfo) .then(res => { //console.log(res.status); return res.json(); }) .then(fileNames => { console.log(fileNames); showFileData(fileNames); }); }); }); // end jQuery </script> </body> </html>board-detail.jsp
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html lang="ko"> <head> <%@ include file="../include/static-head.jsp" %> <style> .content-container { width: 60%; margin: 150px auto; position: relative; } .content-container .main-title { font-size: 24px; font-weight: 700; text-align: center; border-bottom: 2px solid rgb(75, 73, 73); padding: 0 20px 15px; width: fit-content; margin: 20px auto 30px; } .content-container .main-content { border: 2px solid #ccc; border-radius: 20px; padding: 10px 25px; font-size: 1.1em; text-align: justify; min-height: 400px; } .content-container .custom-btn-group { position: absolute; bottom: -10%; left: 50%; transform: translateX(-50%); } /* 페이지 액티브 기능 */ .pagination .page-item.p-active a { background: #333 !important; color: #fff !important; cursor: default; pointer-events: none; } .pagination .page-item:hover a { background: #888 !important; color: #fff !important; } .uploaded-list { display: flex; } .img-sizing { display: block; width: 100px; height: 100px; } </style> </head> <body> <div class="wrap"> <%@ include file="../include/header.jsp" %> <div class="content-container"> <h1 class="main-title">${b.boardNo}번 게시물</h1> <div class="mb-3"> <label for="exampleFormControlInput1" class="form-label">작성자</label> <input type="text" class="form-control" id="exampleFormControlInput1" placeholder="이름" name="writer" value="${b.writer}" disabled> </div> <div class="mb-3"> <label for="exampleFormControlInput2" class="form-label">글제목</label> <input type="text" class="form-control" id="exampleFormControlInput2" placeholder="제목" name="title" value="${b.title}" disabled> </div> <div class="mb-3"> <label for="exampleFormControlTextarea1" class="form-label">내용</label> <p class="main-content"> ${b.content} </p> </div> <!-- 파일 첨부 영역 --> <div class="form-group"> <ul class="uploaded-list"></ul> </div> <div class="btn-group btn-group-lg custom-btn-group" role="group"> <%-- 권한 확인--%> <c:if test="${loginUser.account == b.account || loginUser.auth == 'ADMIN'}"> <button id="mod-btn" type="button" class="btn btn-warning">수정</button> <button id="del-btn" type="button" class="btn btn-danger">삭제</button> </c:if> <button id="list-btn" type="button" class="btn btn-dark">목록</button> </div> <!-- 댓글 영역 --> <div id="replies" class="row"> <div class="offset-md-1 col-md-10"> <!-- 댓글 쓰기 영역 --> <div class="card"> <div class="card-body"> <%-- ${loginUser == null}과 같음 }--%> <c:if test="${empty loginUser}"> <a href="/member/sign-in">댓글은 로그인 후 작성 가능합니다.</a> </c:if> <c:if test="${not empty loginUser}"> <div class="row"> <div class="col-md-9"> <div class="form-group"> <label for="newReplyText" hidden>댓글 내용</label> <textarea rows="3" id="newReplyText" name="replyText" class="form-control" placeholder="댓글을 입력해주세요."></textarea> </div> </div> <div class="col-md-3"> <div class="form-group"> <label for="newReplyWriter" hidden>댓글 작성자</label> <input id="newReplyWriter" name="replyWriter" type="text" value="${loginUser.name}" class="form-control" placeholder="작성자 이름" readonly style="margin-bottom: 6px;"> <button id="replyAddBtn" type="button" class="btn btn-dark form-control">등록</button> </div> </div> </div> </c:if> </div> </div> <!-- end reply write --> <!--댓글 내용 영역--> <div class="card"> <!-- 댓글 내용 헤더 --> <div class="card-header text-white m-0" style="background: #343A40;"> <div class="float-left">댓글 (<span id="replyCnt">0</span>)</div> </div> <!-- 댓글 내용 바디 --> <div id="replyCollapse" class="card"> <div id="replyData"> <!-- < JS로 댓글 정보 DIV삽입 > --> </div> <!-- 댓글 페이징 영역 --> <ul class="pagination justify-content-center"> <!-- < JS로 댓글 페이징 DIV삽입 > --> </ul> </div> </div> <!-- end reply content --> </div> </div> <!-- end replies row --> <!-- 댓글 수정 모달 --> <div class="modal fade bd-example-modal-lg" id="replyModifyModal"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <!-- Modal Header --> <div class="modal-header" style="background: #343A40; color: white;"> <h4 class="modal-title">댓글 수정하기</h4> <button type="button" class="close text-white" data-bs-dismiss="modal">X</button> </div> <!-- Modal body --> <div class="modal-body"> <div class="form-group"> <input id="modReplyId" type="hidden"> <label for="modReplyText" hidden>댓글내용</label> <textarea id="modReplyText" class="form-control" placeholder="수정할 댓글 내용을 입력하세요." rows="3"></textarea> </div> </div> <!-- Modal footer --> <div class="modal-footer"> <button id="replyModBtn" type="button" class="btn btn-dark">수정</button> <button id="modal-close" type="button" class="btn btn-danger" data-bs-dismiss="modal">닫기</button> </div> </div> </div> </div> <!-- end replyModifyModal --> </div> <%@ include file="../include/footer.jsp" %> </div> <!-- 게시글 상세보기 관련 script --> <script> // const [$modBtn, $delBtn, $listBtn] = [...document.querySelector('div[role=group]').children]; const $modBtn = document.getElementById('mod-btn'); const $delBtn = document.getElementById('del-btn'); const $listBtn = document.getElementById('list-btn'); if ($modBtn !== null) { //수정버튼 $modBtn.onclick = e => { location.href = '/board/modify?boardNo=${b.boardNo}'; }; } if ($delBtn !== null) { //삭제버튼 $delBtn.onclick = e => { if (!confirm('정말 삭제하시겠습니까?')) { return; } location.href = '/board/delete?boardNo=${b.boardNo}'; }; } //목록버튼 $listBtn.onclick = e => { console.log('목록버튼 클릭!'); location.href = '/board/list?pageNum=${p.pageNum}&amount=${p.amount}'; }; </script> <!-- 댓글관련 script --> <script> // 로그인한 회원 계정명 -> 세션을 통해 얻고 있음 const currentAccount = '${loginUser.account}'; const auth = '${loginUser.auth}'; //원본 글 번호 const bno = '${b.boardNo}'; // console.log('bno:', bno); // 댓글 요청 URL const URL = '/api/v1/replies'; //날짜 포맷 변환 함수 function formatDate(datetime) { //문자열 날짜 데이터를 날짜객체로 변환 const dateObj = new Date(datetime); // console.log(dateObj); //날짜객체를 통해 각 날짜 정보 얻기 let year = dateObj.getFullYear(); //1월이 0으로 설정되어있음. let month = dateObj.getMonth() + 1; let day = dateObj.getDate(); let hour = dateObj.getHours(); let minute = dateObj.getMinutes(); //오전, 오후 시간체크 let ampm = ''; if (hour < 12 && hour >= 6) { ampm = '오전'; } else if (hour >= 12 && hour < 21) { ampm = '오후'; if (hour !== 12) { hour -= 12; } } else if (hour >= 21 && hour <= 24) { ampm = '밤'; hour -= 12; } else { ampm = '새벽'; } //숫자가 1자리일 경우 2자리로 변환 (month < 10) ? month = '0' + month: month; (day < 10) ? day = '0' + day: day; (hour < 10) ? hour = '0' + hour: hour; (minute < 10) ? minute = '0' + minute: minute; return year + "-" + month + "-" + day + " " + ampm + " " + hour + ":" + minute; } // 댓글 페이지 태그 생성 렌더링 함수 function makePageDOM(pageInfo) { let tag = ""; const begin = pageInfo.beginPage; const end = pageInfo.endPage; //이전 버튼 만들기 if (pageInfo.prev) { tag += "<li class='page-item'><a class='page-link page-active' href='" + (begin - 1) + "'>이전</a></li>"; } //페이지 번호 리스트 만들기 for (let i = begin; i <= end; i++) { let active = ''; if (pageInfo.page.pageNum === i) { active = 'p-active'; } tag += "<li class='page-item " + active + "'><a class='page-link page-custom' href='" + i + "'>" + i + "</a></li>"; } //다음 버튼 만들기 if (pageInfo.next) { tag += "<li class='page-item'><a class='page-link page-active' href='" + (end + 1) + "'>다음</a></li>"; } // 페이지태그 렌더링 const $pageUl = document.querySelector('.pagination'); $pageUl.innerHTML = tag; // ul에 마지막페이지 번호 저장. $pageUl.dataset.fp = pageInfo.finalPage; } // 댓글 목록 DOM을 생성하는 함수 function makeReplyDOM({ replyList, count, maker }) { // 각 댓글 하나의 태그 let tag = ''; if (replyList === null || replyList.length === 0) { tag += "<div id='replyContent' class='card-body'>댓글이 아직 없습니다! ㅠㅠ</div>"; } else { for (let rep of replyList) { tag += "<div id='replyContent' class='card-body' data-replyId='" + rep.replyNo + "'>" + " <div class='row user-block'>" + " <span class='col-md-3'>" + " <b>" + rep.replyWriter + "</b>" + " </span>" + " <span class='offset-md-6 col-md-3 text-right'><b>" + formatDate(rep.replyDate) + "</b></span>" + " </div><br>" + " <div class='row'>" + " <div class='col-md-6'>" + rep.replyText + "</div>" + " <div class='offset-md-2 col-md-4 text-right'>"; //권한 확인 if (currentAccount === rep.account || auth === 'ADMIN') { tag += " <a id='replyModBtn' class='btn btn-sm btn-outline-dark' data-bs-toggle='modal' data-bs-target='#replyModifyModal'>수정</a> " + " <a id='replyDelBtn' class='btn btn-sm btn-outline-dark' href='#'>삭제</a>"; } tag += " </div>" + " </div>" + " </div>"; } } // 댓글 목록에 생성된 DOM 추가 document.getElementById('replyData').innerHTML = tag; // 댓글 수 배치 document.getElementById('replyCnt').textContent = count; // 페이지 렌더링 makePageDOM(maker); } // 댓글 목록을 서버로부터 비동기요청으로 불러오는 함수 function showReplies(pageNum = 1) { fetch(URL + '?boardNo=' + bno + '&pageNum=' + pageNum) .then(res => res.json()) .then(replyMap => { // console.log(replyMap.replyList); makeReplyDOM(replyMap); }); } // 페이지 버튼 클릭이벤트 등록 함수 function makePageButtonClickEvent() { // 페이지 버튼 클릭이벤트 처리 const $pageUl = document.querySelector('.pagination'); $pageUl.onclick = e => { if (!e.target.matches('.page-item a')) return; e.preventDefault(); // 누른 페이지 번호 가져오기 const pageNum = e.target.getAttribute('href'); // console.log(pageNum); // 페이지 번호에 맞는 목록 비동기 요청 showReplies(pageNum); }; } // 댓글 등록 이벤트 처리 핸들러 등록 함수 function makeReplyRegisterClickEvent() { document.getElementById('replyAddBtn').onclick = makeReplyRegisterClickHandler; } // 댓글 등록 이벤트 처리 핸들러 함수 function makeReplyRegisterClickHandler(e) { const $writerInput = document.getElementById('newReplyWriter'); const $contentInput = document.getElementById('newReplyText'); // 서버로 전송할 데이터들 const replyData = { replyWriter: $writerInput.value, replyText: $contentInput.value, boardNo: bno }; // POST요청을 위한 요청 정보 객체 const reqInfo = { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyData) }; fetch(URL, reqInfo) .then(res => res.text()) .then(msg => { if (msg === 'insert-success') { alert('댓글 등록 성공'); // 댓글 입력창 리셋 $writerInput.value = ''; $contentInput.value = ''; // 댓글 목록 재요청 showReplies(document.querySelector('.pagination').dataset.fp); } else { alert('댓글 등록 실패'); } }); } // 댓글 수정화면 열기 상세처리 function processModifyShow(e, rno) { // console.log('수정버튼 클릭함!! after'); // 클릭한 버튼 근처에 있는 댓글 내용텍스트를 얻어온다. const replyText = e.target.parentElement.parentElement.firstElementChild.textContent; //console.log('댓글내용:', replyText); // 모달에 해당 댓글내용을 배치한다. document.getElementById('modReplyText').textContent = replyText; // 모달을 띄울 때 다음 작업(수정완료처리)을 위해 댓글번호를 모달에 달아두자. const $modal = document.querySelector('.modal'); $modal.dataset.rno = rno; } // 댓글 삭제 상세처리 function processRemove(rno) { if (!confirm('진짜로 삭제합니까??')) return; fetch(URL + '/' + rno, { method: 'DELETE' }) .then(res => res.text()) .then(msg => { if (msg === 'del-success') { alert('삭제 성공!!'); showReplies(); // 댓글 새로불러오기 } else { alert('삭제 실패!!'); } }); } // 댓글 수정화면 열기, 삭제 처리 핸들러 정의 function makeReplyModAndDelHandler(e) { const rno = e.target.parentElement.parentElement.parentElement.dataset.replyid; e.preventDefault(); // console.log('수정버튼 클릭함!! before'); if (e.target.matches('#replyModBtn')) { processModifyShow(e, rno); } else if (e.target.matches('#replyDelBtn')) { processRemove(rno); } } // 댓글 수정 화면 열기, 삭제 이벤트 처리 function openModifyModalAndRemoveEvent() { const $replyData = document.getElementById('replyData'); $replyData.onclick = makeReplyModAndDelHandler; } // 댓글 수정 비동기 처리 이벤트 function replyModifyEvent() { const $modal = $('#replyModifyModal'); document.getElementById('replyModBtn').onclick = e => { // console.log('수정 완료 버튼 클릭!'); // 서버에 수정 비동기 요청 보내기 const rno = e.target.closest('.modal').dataset.rno; // console.log(rno); const reqInfo = { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ replyText: $('#modReplyText').val(), replyNo: rno }) }; fetch(URL + '/' + rno, reqInfo) .then(res => res.text()) .then(msg => { if (msg === 'mod-success') { alert('수정 성공!!'); $modal.modal('hide'); // 모달창 닫기 showReplies(); // 댓글 새로불러오기 } else { alert('수정 실패!!'); } }); }; } // 메인 실행부 (function () { // 초기 화면 렌더링시 댓글 1페이지 렌더링 showReplies(); // 댓글 페이지 버튼 클릭이벤트 처리 makePageButtonClickEvent(); // 댓글 등록 버튼 클릭이벤트 처리 makeReplyRegisterClickEvent(); // 댓글 수정 모달 오픈, 삭제 이벤트 처리 openModifyModalAndRemoveEvent(); // 댓글 수정 완료 버튼 이벤트 처리 replyModifyEvent(); })(); </script> <script> // start JQuery $(document).ready(function () { function isImageFile(originFileName) { //정규표현식 const pattern = /jpg$|gif$|png$/i; return originFileName.match(pattern); } // 파일의 확장자에 따른 렌더링 처리 function checkExtType(fileName) { //원본 파일 명 추출 let originFileName = fileName.substring(fileName.indexOf("_") + 1); //확장자 추출후 이미지인지까지 확인 if (isImageFile(originFileName)) { // 파일이 이미지라면 const $img = document.createElement('img'); $img.classList.add('img-sizing'); $img.setAttribute('src', '/loadFile?fileName=' + fileName); $img.setAttribute('alt', originFileName); $('.uploaded-list').append($img); } // 이미지가 아니라면 다운로드 링크를 생성 else { const $a = document.createElement('a'); $a.setAttribute('href', '/loadFile?fileName=' + fileName); const $img = document.createElement('img'); $img.classList.add('img-sizing'); $img.setAttribute('src', '/img/file_icon.jpg'); $img.setAttribute('alt', originFileName); $a.append($img); $a.innerHTML += '<span>' + originFileName + '</span'; $('.uploaded-list').append($a); } } // 드롭한 파일을 화면에 보여주는 함수 function showFileData(fileNames) { // 이미지인지? 이미지가 아닌지에 따라 구분하여 처리 // 이미지면 썸네일을 렌더링하고 아니면 다운로드 링크를 렌더링한다. for (let fileName of fileNames) { checkExtType(fileName); } } // 파일 목록 불러오기 function showFileList() { fetch('/board/file/' + bno) .then(res => res.json()) .then(fileNames => { showFileData(fileNames); }); } showFileList(); }); // end jQuery </script> </body> </html>process-delete.jsp
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <form id="del-form" action="/board/delete" method="post"> <input type="hidden" name="boardNo" value="${boardNo}"> </form> <script> const $form = document.getElementById('del-form') $form.submit(); </script> </body> </html>
Interceptor
BoardInterceptor
package com.project.web_prj.interceptor; import com.project.web_prj.board.dto.ValidateMemberDTO; import lombok.extern.log4j.Log4j2; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Arrays; import java.util.List; import java.util.Map; import static com.project.web_prj.util.LoginUtils.*; // 인터셉터: 컨트롤러에 요청이 들어가기 전, 후에 공통처리할 // 일들을 정의해놓는 클래스 @Configuration @Log4j2 public class BoardInterceptor implements HandlerInterceptor { /* 인터셉터의 전처리 메서드. 리턴값이 true일 경우 컨트롤러 진입을 허용하고 false일 경우 진입을 허용하지 않는다. */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); // 포워드 // RequestDispatcher dispatcher // = request.getRequestDispatcher("/WEB-INF/views/member/sign-in.jsp"); log.info("board interceptor preHandle()"); if (!isLogin(session)) { log.info("this request deny!! 집에 가"); // dispatcher.forward(request, response); response.sendRedirect("/member/sign-in?message=no-login"); return false; } return true; } // 후처리 메서드 //수정,삭제를 할 때 글 정보를 읽기 위해 DB에 접근하는데 DB의 계정 정보와 권한을 알아온 후 session의 정보를 대조해야 수정을 할 수 있기 때문에 postHandle로 넣는다. @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // postHandle이 작동해야 하는 URI 목록 List<String> uriList = Arrays.asList("/board/modify", "/board/delete"); // 현재 요청 URI 정보 알아내기 String requestURI = request.getRequestURI(); log.info("requestURI - {}", requestURI); // 현재 요청 메서드 정보 확인 String method = request.getMethod(); // postHandle은 uriList 목록에 있는 URI에서만 작동하게 함 if (uriList.contains(requestURI) && method.equalsIgnoreCase("GET")) { log.info("board interceptor postHandle() ! "); HttpSession session = request.getSession(); // 컨트롤러의 메서드를 처리한 후 모델에 담긴 데이터의 맵 Map<String, Object> modelMap = modelAndView.getModel(); // log.info("modelMap.size() - {}", modelMap.size()); // log.info("modelMap - {}", modelMap); ValidateMemberDTO dto = (ValidateMemberDTO) modelMap.get("validate"); // 수정하려는 게시글의 계정명 정보와 세션에 저장된 계정명 정보가 // 일치하지 않으면 돌려보내라 // log.info("게시물의 계정명 - {}", dto.getAccount()); // log.info("로그인한 계정명 - {}", getCurrentMemberAccount(request.getSession())); if (isAdmin(session)) return; if (!isMine(session, dto.getAccount())) { response.sendRedirect("/board/list"); } } } private boolean isAdmin(HttpSession session) { return getCurrentMemberAuth(session).equals("ADMIN"); } private static boolean isMine(HttpSession session, String account) { return account.equals(getCurrentMemberAccount(session)); } }
'Program > Spring' 카테고리의 다른 글
| Clone_Project - 5(자동 로그인, KAKAO Login) (0) | 2022.12.30 |
|---|---|
| Clone_Project - 3(파일 업로드, 비동기 파일 업로드) (0) | 2022.12.30 |
| Clone_Project - 2(게시물 목록 요청 ,동적 SQL, 댓글) (0) | 2022.12.30 |
| Clone_Project -1 (Cookie, 페이징 처리, 글 상세 → 이전 글 목록 페이지) (0) | 2022.12.30 |
| VO/DTO, @ToString, 생성자/빌더, 단위 테스트, 생성자주입VS필드 주입, Controller VS Service, Log4j2, JSP 분리 (0) | 2022.12.30 |