국비교육(22-23)

60일차(1)/Spring Boot(8) : Spring 프로젝트 Spring Boot로 이식하기 / Boot에서 war파일 생성(2)

서리/Seori 2023. 1. 2. 18:19

60일차(1)/Spring Boot(8) : Spring 프로젝트 Spring Boot로 이식하기 / Boot에서 war파일 생성(2)

 

 

* Spring 프로젝트 Boot 로 이식 중!

- 갤러리 기능 가져오기

- 카페 기능 가져오기

- 자료실(File) 기능 가져오기

- Transaction(shop) 기능 가져오기

 

* gitHub : 링크


 

- Member 기능 이식해왔던 내용 리뷰

 

* application.properties 에 설정해야 하는 값

- 포트번호 설정

- path 에 경로 명시

3) DB 로그인정보

 

- mapper폴더 만들어서 MemberMapper 만들기

 

- resources/ 폴더의 위치는 classpath: 이다

 

- application.properties에 mapper의 위치 설정한 곳으로 명시!

 

- DataSource 설정과 mapper 설정을 마치면 mybatis를 사용할 준비가 된 것이다.

→ 즉 dao에 sqlSession의 값이 들어온다는 것이다.

 

- pom.xml 에 mybatis 설정 +  application.properties에 DataSource 설정 + mapper 경로 기록

 

 

- spring boot에서도 앞에서 만들었던 프로젝트와 똑같은 방식으로 만들 예정이다.

 

- 그러려면 src-main 폴더 안에 webapp-WEB-INF 폴더를 생성

- webapp, WEB-INF는 폴더명을 바꿀 수 없다. 대,소문자 구분 정확하게!

- views 폴더는 이름을 마음대로 지을 수 있다.

 

 

- get방식 요청 컨트롤러에서 작성하는 법

- Spring 5.0 (boot) 를 사용하면서 @GetMapping을 쓸 수 있게 되었다.

- 좀더 편리하게 사용할 수 있도록 새 어노테이션이 추가되었다.

- request 인자를 전달받는 것

 

- 뷰 페이지 정보를 리턴해준 것인데,

 해당 문자열이 포워드 이동할 뷰 페이지의 위치를 알려준다. 접두어, 접미어가 알아서 붙는다.

 

- 기존 Legacy Project에서는 이 설정을 servlet-context.xml에서 했는데,

 

- Boot에서는 application.properties 에서 한다.

 

- 레거시 프로젝트 방식으로 응답하려면 위 내용이 application.properties에 있고

 뷰페이지 폴더 설정을 해주어야 한다.

- webapp이 없으므로, 직접 폴더를 만들어주고 구성해주어야 한다.

 

- 웹페이지에서 응답할 수 있도록 해준 것이다.

 

 

- jsp는 결국 class로 바뀐다.

- jsp에서 사용하는 taglib도 spring boot에서 사용하려면 라이브러리가 있어야 한다.

 

 

- 그런데 바꾸어주는 기본 기능이 spring boot에는 없다.

- 이 기능이 기본으로 들어있는 것이 아니기 때문에 이 의존 dependency 추가가 필요하다.

 

- Run 해보면 localhost:9000/acorn/ 이 경로에 대해서 HomeController 가 동작하는 것을 볼 수 있다.

 


 

- sqlSession 주입받기

 

- resources 폴더 안에 있는 내용은 컨트롤러를 거치지 않도록 하는 설정

 

WebConfig (전체)

package com.sy.boot07.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.sy.boot07.interceptor.LoginInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer{
	//로그인 인터셉터 주입 받기
	@Autowired LoginInterceptor loginInterceptor;

	//인터셉터 동작하도록 등록하기
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(loginInterceptor)
		.addPathPatterns("/users/*","/gallery/*","/cafe/*","/file/*")
		.excludePathPatterns("/users/signup_form", "/users/signup", "/users/loginform", "/users/login",
				"/gallery/list", "/gallery/detail",
				"/cafe/list","cafe/detail","/cafe/ajax_comment_list",
				"/file/list","/file/download");
	}
	// resources 폴더안에 있는 자원을 spring 컨트롤러를 거치지 않고 응답되도록 설정
	// webapp 안에 resources 폴더를 만들어야 한다.
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
	}
}

 

 

- @Configutation 어노테이션을 붙이고 + 메소드 override하기

- registry 객체를 전달받아서 사용하는 addResourceHandler 메소드 사용

 

 

- 레거시 프로젝트에서는 이 설정에 해당된다.

- 레거시 프로젝트와 작성방식을 비교해보기!(servlet-context사용)

 

- 리소스 폴더 안에 있는 내용은 스프링 컨트롤러를 거치지 않고 바로 응답되도록 하기!

 


 

- 파일 업로드 기능

- 사이즈 제한이 있으므로 application.properties에 미리 설정해주기

 

 

- 값을 받아올 때, 변수명을 input의 name값과 같게 해주기

- @RequestParam 은 생략 가능하다.

 

 

upload까지의 realPath를 얻어와서  resource 폴더에 원하는 파일명으로 업로드하도록 할수있다.

 


 

- Spring boot는 업로드 폴더에 파일이 들어가 있다.

- boot에서는 프로젝트상에서 보이는 이 upload 폴더를 실제로 사용한다.

 

- 하지만 이 upload 폴더의 파일이 git의 변경사항으로 잡히면 좋을게없다.

 

- 기존 레거시 프로젝트: 외부 tomcat으로 돌리기때문에, 외부 tomcat의 위치에서 잡힌다.

(실제로 그 upload 폴더를 사용하지는 않았다.)

- 하지만 boot에서는 실제로 사용한다.

 

 

- 해당 폴더에 파일이 들어가면 git에서도 변동사항으로 잡힌다.

- 하지만 커밋하지 않는 것이 좋다(개별 파일은 필요없으므로)

- 따라서 .gitignore에서 upload 폴더의 내용은 무시하라고 설정해주어야 한다.

 

- 이렇게 추가하면 upload 폴더 하위의 모든 내용은 git의 변동사항 추적에서 무시된다.

- 이클립스를 다시 열어보면 무시되는 것으로 나온다.

 


 

새 프로젝트생성

 

- Boot07_Final / 패키지명 spring.sy.boot07

- web/mybatis/devtools/oracle : 4개 체크하기

 

application.properties (전체)

# port setting
server.port=9000

# context path setting
server.servlet.context-path=/boot07

# oracle DataSource Setting (Connection Pool)
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=scott
spring.datasource.password=tiger

# mapper location
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.sy.boot07.**

# view page  prefix suffix setting
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# file size, request size setting
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB

 

- 여러개만들어도 인식되도록 * 로 작성할 수 있다.

 

- home.jsp, homecontroller, pom.xml에 jsp 라이브러리 추가

 

<!-- jsp 페이지 사용과 taglib 를 사용하기 위해 -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
    </dependency>

 

 

- interceptor 패키지 생성

 

- 메인 패키지가 있으면 하위패키지에는 자동으로 componet scan 이 일어나므로, 

  @Component 어노테이션만 붙여주면 된다.

 

- interceptor 설정: WebConfig에서 DI 주입받을 수 있도록 bean으로 만들기!

 

 

- WebConfig 생성 (전체)

package com.sy.boot07.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.sy.boot07.interceptor.LoginInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer{
	//로그인 인터셉터 주입 받기
	@Autowired LoginInterceptor loginInterceptor;

	//인터셉터 동작하도록 등록하기
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(loginInterceptor)
		.addPathPatterns("/users/*","/gallery/*","/cafe/*","/file/*")
		.excludePathPatterns("/users/signup_form", "/users/signup", "/users/loginform", "/users/login",
				"/gallery/list", "/gallery/detail",
				"/cafe/list","cafe/detail","/cafe/ajax_comment_list",
				"/file/list","/file/download");
	}
	// resources 폴더안에 있는 자원을 spring 컨트롤러를 거치지 않고 응답되도록 설정
	// webapp 안에 resources 폴더를 만들어야 한다.
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
	}
}

 

- interceptor를 등록할 때 여러개의 경로를 , 로 이어서 등록 가능하다.

 

 

@Autowired LoginInterceptor loginInterceptor;

- component 스캔으로 bean 으로 만들고, WebConfig 에서 인터셉터를 주입받아 설정한다.

 

- exclude 매핑도 여기서 설정한다.

 

- bean이 되는 방법, mapping 설정 두가지 / Spring과 Boot를 비교해서 보기!

 

 

- addResourseHandler도 override 해준다.

 

 

- webapp 안에 resources, upload 폴더를 생성

 

- users 관련 패키지 4종 복사해와서 이름 boot07로 바꿔주기

 

- 로그인 검증을 위한 라이브러리. security 관련 디펜던시 추가

 

        <!-- Spring Security 관련 라이브러리 -->
        <dependency>
          <groupId>org.springframework.security</groupId>
          <artifactId>spring-security-web</artifactId>
          <version>4.0.0.RELEASE</version>
      </dependency>

 

- mapper 추가

 

- 그런데 현재 타입알리어스를 설정할 곳이 없다.

- 일단 그대로 사용하는 것으로 고쳐주기

 


 

* 갤러리 기능 boot로 수정하기

 

- 갤러리 뷰페이지 views에 추가 

- GalleryMapper추가

- 4개 패키지 복사해서 리네이밍(alt+shift+R)

 

- typealias 인식시키기

- dto 상단에 @Alias 어노테이션을 붙이고,

이것을 어떤 타입알리어스로 쓸 것인지 뒤에 직접 작성해서 지정 가능

 

- com.sy.boot07.gallery.dto.GalleryDto라는 type을 galleryDto type alias로 사용하기 위한 어노테이션 붙이기
- application.properties 파일에 해당 dto가 존재하는 패키지를 명시하기
 (mybatis.type-aliases-package=패키지명1, 패키지명2, ...)

 

 

mybatis.type-aliases-package= mapper명(패키지명까지)

 

- 이렇게 적어주는 것이 좋다.

- 등록할 mapper alias가 여러 개인 경우, *을 이용하면 한번만 입력해서 전체에 다 적용할 수도 있다.

 

- 이전과 저장한 경로가 다르기 때문에 사진은 뜨지 않지만, 연결된다.

 

 

- interceptor에 갤러리 경로추가

list, detail 페이지는 로그인하면 으로 이동하도록 인터셉터 설정

 

[ mapper의 typealias 설정방법 ]
- application.properties 에 위치 알려주기
- DTO에 @Alias 어노테이션 붙여서 알려주기

 

- Spring Boot도 크게 다르지 않다. interceptor, resource 설정이 좀 다르고

 application.properties 에다가 datasource, mapper location, view 페이지 설정을 한다는 점이 다를 뿐!

 

 

- 파일이 정상적으로 업로드된다.

 


 

* cafe, file 기능도 옮겨오기

 

- view page, mapper를 추가해주기

 

- CafeServiceImpl을 복사하면 오류가생기는데, exception 때문

 

- 4개의 패키지와 exception을 같이 복사해온다.

 

@Alias("cafeDto")

@Alias("cafeCommentDto")

어노테이션을 붙여준다.

 

- application.properties에 cafeDto도 추가

 

- resources 폴더에 SmartEditor 폴더도 가져와주기

 

- webconfig 에 interceptor에서 제외할 내용 추가

 

 

- 정상적으로 글, 댓글이 작성된다.

 


 

*기존 버그 수정

- 댓글 전체 페이지 개수(totalPageNum) 는 댓글이 하나도 없을 때 0으로 나온다.

 

- 그런데 0일 때에도, 스크롤 창이 작을때는 저 화살표가 나오는 오류가 있다.

 

detail.jsp에서 수정해주기

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/views/cafe/detail.jsp</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
<style>
   .content{
      border: 1px dotted gray;
   }
   
   /* 댓글 프로필 이미지를 작은 원형으로 만든다. */
   .profile-image{
      width: 50px;
      height: 50px;
      border: 1px solid #cecece;
      border-radius: 50%;
   }
   /* ul 요소의 기본 스타일 제거 */
   .comments ul{
      padding: 0;
      margin: 0;
      list-style-type: none;
   }
   .comments dt{
      margin-top: 5px;
   }
   .comments dd{
      margin-left: 50px;
   }
   .comment-form textarea, .comment-form button{
      float: left;
   }
   .comments li{
      clear: left;
   }
   .comments ul li{
      border-top: 1px solid #888;
   }
   .comment-form textarea{
      width: 84%;
      height: 100px;
   }
   .comment-form button{
      width: 14%;
      height: 100px;
   }
   /* 댓글에 댓글을 다는 폼과 수정폼은 일단 숨긴다. */
   .comments .comment-form{
      display: none;
   }
   /* .reply_icon 을 li 요소를 기준으로 배치 하기 */
   .comments li{
      position: relative;
   }
   .comments .reply-icon{
      position: absolute;
      top: 1em;
      left: 1em;
      color: red;
   }
   pre {
     display: block;
     padding: 9.5px;
     margin: 0 0 10px;
     font-size: 13px;
     line-height: 1.42857143;
     color: #333333;
     word-break: break-all;
     word-wrap: break-word;
     background-color: #f5f5f5;
     border: 1px solid #ccc;
     border-radius: 4px;
   }   
   
   .loader{
      /* 로딩 이미지를 가운데 정렬하기 위해 */
      text-align: center;
      /* 일단 숨겨 놓기 */
      display: none;
   }   
   
   .loader svg{
      animation: rotateAni 1s ease-out infinite;
   }
   
   @keyframes rotateAni{
      0%{
         transform: rotate(0deg);
      }
      100%{
         transform: rotate(360deg);
      }
   }
</style>
</head>
<body>
   <div class="container">
      
      <%-- 만일 이전글(더 옛날글)의 글번호가 0 가 아니라면(이전글이 존재 한다면) --%>
      <c:if test="${dto.prevNum ne 0}">
         <a href="detail?num=${dto.prevNum }&condition=${condition}&keyword=${encodedK}">이전글</a>
      </c:if>
      
      <%-- 만일 다음글(더 최신글)의 글번호가 0 가 아니라면(다음글이 존재 한다면) --%>
      <c:if test="${dto.nextNum ne 0 }">
         <a href="detail?num=${dto.nextNum }&condition=${condition}&keyword=${encodedK}">다음글</a>
      </c:if>
      
      <%-- 만일 검색 키워드가 있다면 --%>
      <c:if test="${not empty keyword }">
         <p>
            <strong>${condition }</strong> 조건 
            <strong>${keyword }</strong> 검색어로 검색된 내용 자세히 보기
         </p>
      </c:if>
      <h3>글 상세 보기</h3>
      <table>
         <tr>
            <th>글번호</th>
            <td>${dto.num }</td>
         </tr>
         <tr>
            <th>작성자</th>
            <td>${dto.writer }</td>
         </tr>
         <tr>
            <th>제목</th>
            <td>${dto.title }</td>
         </tr>
         <tr>
            <th>조회수</th>
            <td>${dto.viewCount }</td>   
         </tr>
         <tr>
            <th>작성일</th>
            <td>${dto.regdate }</td>
         </tr>
         <tr>
            <td colspan="2">
               <div>${dto.content }</div>
            </td>
         </tr>   
      </table>
      <c:if test="${sessionScope.id eq dto.writer }">
         <a href="updateform?num=${dto.num }">수정</a>
         <a href="javascript:" onclick="deleteConfirm()">삭제</a>
         <script>
            function deleteConfirm(){
               const isDelete=confirm("이 글을 삭제 하겠습니까?");
               if(isDelete){
                  location.href="delete?num=${dto.num}";
               }
            }
         </script>
      </c:if>
      <h4>댓글을 입력해주세요</h4>
      <!-- 원글에 댓글을 작성할 폼 -->
      <form class="comment-form insert-form" action="comment_insert" method="post">
         <!-- 원글의 글번호가 댓글의 ref_group 번호가 된다. -->
         <input type="hidden" name="ref_group" value="${dto.num }"/>
         <!-- 원글의 작성자가 댓글의 대상자가 된다. -->
         <input type="hidden" name="target_id" value="${dto.writer }"/>
   
         <textarea name="content">${empty id ? '댓글 작성을 위해 로그인이 필요 합니다.' : '' }</textarea>
         <button type="submit">등록</button>
      </form>
      
      <!-- 댓글 목록 -->
      <div class="comments">      
      
         <ul>
            <c:forEach var="tmp" items="${commentList }">
               <c:choose>
                  <c:when test="${tmp.deleted eq 'yes' }">
                     <li>삭제된 댓글 입니다.</li>
                  </c:when>
                  <c:otherwise>
                     <c:if test="${tmp.num eq tmp.comment_group }">
                        <li id="reli${tmp.num }">
                     </c:if>
                     <c:if test="${tmp.num ne tmp.comment_group }">
                        <li id="reli${tmp.num }" style="padding-left:50px;">
                           <svg class="reply-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-return-right" viewBox="0 0 16 16">
                                <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/>
                           </svg>
                     </c:if>
                           <dl>
                              <dt>
                                 <c:if test="${ empty tmp.profile }">
                                    <svg class="profile-image" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
                                      <path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
                                      <path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
                                    </svg>
                                 </c:if>
                                 <c:if test="${not empty tmp.profile }">
                                    <img class="profile-image" src="${pageContext.request.contextPath}${tmp.profile }"/>
                                 </c:if>
                                 <span>${tmp.writer }</span>
                                 <c:if test="${tmp.num ne tmp.comment_group }">
                                    @<i>${tmp.target_id }</i>
                                 </c:if>
                                 <span>${tmp.regdate }</span>
                                 <a data-num="${tmp.num }" href="javascript:" class="reply-link">답글</a>
                                 <c:if test="${ (id ne null) and (tmp.writer eq id) }">
                                    <a data-num="${tmp.num }" class="update-link" href="javascript:">수정</a>
                                    <a data-num="${tmp.num }" class="delete-link" href="javascript:">삭제</a>
                                 </c:if>
                              </dt>
                              <dd>
                                 <pre id="pre${tmp.num }">${tmp.content }</pre>                  
                              </dd>
                           </dl>
                           <form id="reForm${tmp.num }" class="animate__animated comment-form re-insert-form" action="comment_insert" method="post">
                              <input type="hidden" name="ref_group" value="${dto.num }"/>
                              <input type="hidden" name="target_id" value="${tmp.writer }"/>
                              <input type="hidden" name="comment_group" value="${tmp.comment_group }"/>
                              <textarea name="content"></textarea>
                              <button type="submit">등록</button>
                           </form>
                        <c:if test="${tmp.writer eq id }">
                           <form id="updateForm${tmp.num }" class="comment-form update-form" action="comment_update" method="post">
                              <input type="hidden" name="num" value="${tmp.num }" />
                              <textarea name="content">${tmp.content }</textarea>
                              <button type="submit">수정</button>
                           </form>
                        </c:if>
                        </li>      
                  </c:otherwise>
               </c:choose>
            </c:forEach>
         </ul>
      </div>      
      <div class="loader">
         <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
              <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
              <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
         </svg>
      </div>
      
   </div>
   
   <script src="${pageContext.request.contextPath}/resources/js/sy_util.js"></script>
   <script>
      
      //클라이언트가 로그인 했는지 여부
      let isLogin=${ not empty id };
      
      document.querySelector(".insert-form")
         .addEventListener("submit", function(e){
            //만일 로그인 하지 않았으면 
            if(!isLogin){
               //폼 전송을 막고 
               e.preventDefault();
               //로그인 폼으로 이동 시킨다.
               location.href=
                  "${pageContext.request.contextPath}/users/loginform?url=${pageContext.request.contextPath}/cafe/detail?num=${dto.num}";
            }
         });
      
      /*
         detail
          페이지 로딩 시점에 만들어진 1 페이지에 해당하는 
         댓글에 이벤트 리스너 등록 하기 
      */
      addUpdateFormListener(".update-form");
      addUpdateListener(".update-link");
      addDeleteListener(".delete-link");
      addReplyListener(".reply-link");
      
      
      //댓글의 현재 페이지 번호를 관리할 변수를 만들고 초기값 1 대입하기
      let currentPage=1;
      //마지막 페이지는 totalPageCount 이다.
      <%-- 댓글의 개수가 0일때 오류를 방지하기 위해 --%>
      let lastPage=${ totalPageCount eq 0 ? 1 : totalPageCount};
      
      //추가로 댓글을 요청하고 그 작업이 끝났는지 여부를 관리할 변수 
      let isLoading=false; //현재 로딩중인지 여부 
      
      /*
         window.scrollY => 위쪽으로 스크롤된 길이
         window.innerHeight => 웹브라우저의 창의 높이
         document.body.offsetHeight => body 의 높이 (문서객체가 차지하는 높이)
      */
      window.addEventListener("scroll", function(){
         //바닥 까지 스크롤 했는지 여부 
         const isBottom = 
            window.innerHeight + window.scrollY  >= document.body.offsetHeight;
         //현재 페이지가 마지막 페이지인지 여부 알아내기
         let isLast = currentPage == lastPage;   
         //현재 바닥까지 스크롤 했고 로딩중이 아니고 현재 페이지가 마지막이 아니라면
         if(isBottom && !isLoading && !isLast){
            //로딩바 띄우기
            document.querySelector(".loader").style.display="block";
            
            //로딩 작업중이라고 표시
            isLoading=true;
            
            //현재 댓글 페이지를 1 증가 시키고 
            currentPage++;
            
            /*
               해당 페이지의 내용을 ajax 요청을 통해서 받아온다.
               "pageNum=xxx&num=xxx" 형식으로 GET 방식 파라미터를 전달한다. 
            */
            ajaxPromise("ajax_comment_list","get",
                  "pageNum="+currentPage+"&num=${dto.num}")
            .then(function(response){
               //json 이 아닌 html 문자열을 응답받았기 때문에  return response.text() 해준다.
               return response.text();
            })
            .then(function(data){
               //data 는 html 형식의 문자열이다. 
               console.log(data);
               // beforebegin | afterbegin | beforeend | afterend
               document.querySelector(".comments ul")
                  .insertAdjacentHTML("beforeend", data);
               //로딩이 끝났다고 표시한다.
               isLoading=false;
               //새로 추가된 댓글 li 요소 안에 있는 a 요소를 찾아서 이벤트 리스너 등록 하기 
               addUpdateListener(".page-"+currentPage+" .update-link");
               addDeleteListener(".page-"+currentPage+" .delete-link");
               addReplyListener(".page-"+currentPage+" .reply-link");
               //새로 추가된 댓글 li 요소 안에 있는 댓글 수정폼에 이벤트 리스너 등록하기
               addUpdateFormListener(".page-"+currentPage+" .update-form");
               
               //로딩바 숨기기
               document.querySelector(".loader").style.display="none";
            });
         }
      });
      
      //인자로 전달되는 선택자를 이용해서 이벤트 리스너를 등록하는 함수 
      function addUpdateListener(sel){
         //댓글 수정 링크의 참조값을 배열에 담아오기 
         // sel 은  ".page-xxx  .update-link" 형식의 내용이다 
         let updateLinks=document.querySelectorAll(sel);
         for(let i=0; i<updateLinks.length; i++){
            updateLinks[i].addEventListener("click", function(){
               //click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다. 
               const num=this.getAttribute("data-num"); //댓글의 글번호
               document.querySelector("#updateForm"+num).style.display="block";
               
            });
         }
      }
      function addDeleteListener(sel){
         //댓글 삭제 링크의 참조값을 배열에 담아오기 
         let deleteLinks=document.querySelectorAll(sel);
         for(let i=0; i<deleteLinks.length; i++){
            deleteLinks[i].addEventListener("click", function(){
               //click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다. 
               const num=this.getAttribute("data-num"); //댓글의 글번호
               const isDelete=confirm("댓글을 삭제 하시겠습니까?");
               if(isDelete){
                  // sy_util.js 에 있는 함수들 이용해서 ajax 요청
                  ajaxPromise("comment_delete.do", "post", "num="+num)
                  .then(function(response){
                     return response.json();
                  })
                  .then(function(data){
                     //만일 삭제 성공이면 
                     if(data.isSuccess){
                        //댓글이 있는 곳에 삭제된 댓글입니다를 출력해 준다. 
                        document.querySelector("#reli"+num).innerText="삭제된 댓글입니다.";
                     }
                  });
               }
            });
         }
      }
      function addReplyListener(sel){
         //댓글 링크의 참조값을 배열에 담아오기 
         let replyLinks=document.querySelectorAll(sel);
         //반복문 돌면서 모든 링크에 이벤트 리스너 함수 등록하기
         for(let i=0; i<replyLinks.length; i++){
            replyLinks[i].addEventListener("click", function(){
               
               if(!isLogin){
                  const isMove=confirm("로그인이 필요 합니다. 로그인 페이지로 이동 하시겠습니까?");
                  if(isMove){
                     location.href=
                        "${pageContext.request.contextPath}/users/loginform?url=${pageContext.request.contextPath}/cafe/detail?num=${dto.num}";
                  }
                  return;
               }
               
               //click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다. 
               const num=this.getAttribute("data-num"); //댓글의 글번호
               
               const form=document.querySelector("#reForm"+num);
               
               //현재 문자열을 읽어온다 ( "답글" or "취소" )
               let current = this.innerText;
               
               if(current == "답글"){
                  //번호를 이용해서 댓글의 댓글폼을 선택해서 보이게 한다. 
                  form.style.display="block";
                  form.classList.add("animate__fadeInLeft");
                  this.innerText="취소";   
                  form.addEventListener("animationend", function(){
                     form.classList.remove("animate__fadeInLeft");
                  }, {once:true});
               }else if(current == "취소"){
                  form.classList.add("animate__fadeOut");
                  this.innerText="답글";
                  form.addEventListener("animationend", function(){
                     form.classList.remove("animate__fadeOut");
                     form.style.display="none";
                  },{once:true});
               }
            });
         }
      }
      
      function addUpdateFormListener(sel){
         //댓글 수정 폼의 참조값을 배열에 담아오기
         let updateForms=document.querySelectorAll(sel);
         for(let i=0; i<updateForms.length; i++){
            //폼에 submit 이벤트가 일어 났을때 호출되는 함수 등록 
            updateForms[i].addEventListener("submit", function(e){
               //submit 이벤트가 일어난 form 의 참조값을 form 이라는 변수에 담기 
               const form=this;
               //폼 제출을 막은 다음 
               e.preventDefault();
               //이벤트가 일어난 폼을 ajax 전송하도록 한다.
               ajaxFormPromise(form)
               .then(function(response){
                  return response.json();
               })
               .then(function(data){
                  if(data.isSuccess){
                     /*
                        document.querySelector() 는 html 문서 전체에서 특정 요소의 
                        참조값을 찾는 기능
                        
                        특정문서의 참조값.querySelector() 는 해당 문서 객체의 자손 요소 중에서
                        특정 요소의 참조값을 찾는 기능
                     */
                     const num=form.querySelector("input[name=num]").value;
                     const content=form.querySelector("textarea[name=content]").value;
                     //수정폼에 입력한 value 값을 pre 요소에도 출력하기 
                     document.querySelector("#pre"+num).innerText=content;
                     form.style.display="none";
                  }
               });
            });
         }
      }
   </script>
</body>
</html>

- 스크롤+창 크기를 비교한 결과로 화살표가 돌아가는 모양을 출력하는데,

 창의 상하 폭이 일정 크기 이하일 경우 이 if문을 통과하게 되어 문제 발생!

 

- totalPageCount를 출력하는 내용 수정하기

- 0이면 1을 찍어두고, 그 외의 경우는 totalPageCount를 출력하도록 하기.

 


 

* 자료실(File) 기능 가져오기

 

- view 페이지, mapper를 가져오고,

 패키지 5개 복사해서 패키지명을 rename 해주기

 

- fileService에 경로가 resources 폴더아래 upload 있는것을 확인!

- webconfig 에 interceptor 설정

 

- FileDto → @Alias("fileDto") 추가

- application.properties → mybatis.type-aliases-package 에 fileDto 패키지명추가

 

 

- 똑같이 File 기능 추가 가능

 


 

- 트랜잭션 기능 Boot로 사용하기! (shop)

 

- error, shop 뷰페이지 가져오기

- shop 관련 패키지 4개 가져와서 패키지명 변경

 

- ShopMapper 가져오기

 

 

- 2개 Dto에 각각 alias 달아주기

 

 

- application.propertiers 들어가서 alias 명 추가해주기

mybatis.type-aliases-package= com.sy.boot07.gallery.dto, com.sy.boot07.users.dto, 
							com.sy.boot07.cafe.dto, com.sy.boot07.file.dto, com.sy.boot07.shop.dto

- 엔터치면 인식되지 않는다 ㅠ 한줄로 적어주어야 한다.

 

 

- 하나하나 추가해주기 귀찮으면 그냥 이렇게 작성할 수도 있다!

- Spring이 알아서 하위 패키지를 전부 뒤져서 @Alias를 찾는다.

 

 

- 트랜잭션 사용 시, spring Boot 에서는 이 2가지 설정을 할 필요가 없다. 이미 설정이 들어가 있다.

- @Transactional 어노테이션만 들어가면 된다.

 

ShopServiceImpl

package com.sy.boot07.shop.service;

import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.ModelAndView;

import com.sy.boot07.exception.DeliveryException;
import com.sy.boot07.shop.dao.OrderDao;
import com.sy.boot07.shop.dao.ShopDao;
import com.sy.boot07.shop.dto.OrderDto;
import com.sy.boot07.shop.dto.ShopDto;

@Service
public class ShopServiceImpl implements ShopService {

	@Autowired private ShopDao shopDao;
	@Autowired private OrderDao orderDao;
	
	@Override
	public void getList(ModelAndView mView) {
		//상품 목록
		List<ShopDto> list=shopDao.getList();
		//ModelAndView객체에 list라는 키 값으로 담는다.
		mView.addObject("list", list);		
	}
	/*
	 * - Spring Boot 트랜잭션 설정 방법
	 * 
	 * 트랜잭션을 관리할 서비스의 메소드에 @Transactional 어노테이션 붙이기
	 * 
	 * - 프로그래머의 의도 하에서 트랜잭션에 영향을 주는 Exception을 발생시키는 방법
	 * 
	 * DataAccessException 클래스를 상속받은 클래스를 정의하고
	 * 예) class MyException extends DataAccessException{ }
	 * 
	 * 		throw new MyException("예외 메시지");
	 * 
	 * 예외를 발생시킬 조건이라면 위와 같이 예외를 발생시켜서
	 * 트랜잭션이 관리되도록 한다.
	 */
	@Transactional
	@Override
	public void buy(HttpServletRequest request, ModelAndView mView) {
		//구입자의 아이디
		String id=(String)request.getSession().getAttribute("id");
		//1. parameter로 전달되는 구입할 상품 번호
		int num=Integer.parseInt(request.getParameter("num"));
		//2. 상품의 가격을 얻어온다.
		int price=shopDao.getPrice(num);
		//3. 상품의 가격만큼 계좌 잔액을 줄인다.
		ShopDto dto=new ShopDto();
		dto.setId(id);
		dto.setPrice(price);
		shopDao.minusMoney(dto);
		//4. 가격의 10%를 포인트로 적립한다.
		shopDao.plusPoint(dto);
		//5. 재고의 개수를 1 줄인다.
		shopDao.minusCount(num);
		//6. 주문 테이블(배송)에 정보를 추가한다.
		OrderDto dto2=new OrderDto();
		dto2.setId(id); //누가
		dto2.setCode(num); //어떤 상품을
		//클라이언트가 입력한 배송 주소라고 가정
		String addr="제주도 역삼동";
		
		//가상의 테스트
		if(addr.contains("제주도")) {
			throw new DeliveryException("제주도는 배송 불가 지역입니다.");
		}
		
	    dto2.setAddr(addr);//어디로 배송할지
	    orderDao.addOrder(dto2);			
	}

}

 

- Boot에서는 pom.xml에 transaction관련 디펜던시를 따로 추가하지 않아도 된다.