본문 바로가기

Program/Spring

Clone_Project - 4(로그인 처리, 인터셉터(Interceptor), 사용자 권한)

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>&nbsp;&nbsp;&nbsp;
                                                  <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>&nbsp;&nbsp;&nbsp;<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>&nbsp;&nbsp;&nbsp;<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>&nbsp;&nbsp;&nbsp;<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>&nbsp;&nbsp;&nbsp;<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>&nbsp;&nbsp;&nbsp;<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>&nbsp;&nbsp;&nbsp;<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>&nbsp;" +
                                  "         <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));
          }
      }