반응형

v55_1 -> JavaEE의 Servlet 컨테이너 적용하기

        // Java Enterprise Edition // 일반 개발할때 사용하는 기술

        // Java Standard Edition // Dsektop App 개발 기술

        // Java Micro Edition // IoT(Embedded) 개발 기술

JavaEE와 Servlet 컨테이너와의 관계이다. // JavaEE를 확인하고 Servlet 버전을 확인하자.

즉, 버전에 맞는 Servlet 메소드를 사용하여야 한다. // 회사마다 다르다. // 학원에선 9버전 사용

JavaEE 5 // 2006년 // Tomcat 6 // Servlet 2.5 // JSP 2.1 // EJB 3.0

JavaEE 6 // 2009년 // Tomcat 7 // Servlet 3.0 // JSP 2.2 // EJB 3.1

JavaEE 7 // 2013년 // Tomcat 8.5 // Servlet 3.1 // JSP 2.3 // EJB 3.2

JavaEE 8 // 2017년 // Tomcat 9 // Servlet 4.0 // JSP 2.3 // EJB 3.2

        // http://tomcat.apache.org/// 왼쪽 Download에서 버전 클릭 // Binary Distributions // Core // zip 다운

        // Source Code Distributions // zip 다운 // User 밑에 server폴더 생성 // 다운받은 zip server 폴더 밑에 압출풀기

        // tomcat 포트 변경 //

        // VSC로 tomcat 폴더 가져오기 // conf - server.xml 열기 // Connector 태그 port 원하는 값으로 변경

    <Connector port="9999" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

        // tomcat 관리자 아이디 등록 // 

        // conf - tomcat-users.xml // role 주석 풀기 // rolename 변경

  <role rolename="tomcat"/>
  <role rolename="manager-gui"/>
  <user username="tomcat" password="1111" roles="tomcat, manager-gui"/>

        // conf - Catalina - localhost 폴더 생성 // manager.xml 파일 생성

<?xml version="1.0" encoding="UTF-8"?>
<Context privileged="true" antiResourceLocking="false"
         docBase="${catalina.home}/webapps/manager">
  <Valve className="org.apache.catalina.valves.RemoteAddrValve"
         allow="^.*$" />
</Context>

         // 톰캣 서버 실행 //

         // bin - startup.bat 실행 // bat가 window, sh는 unix에서 사용하는 파일이다.

        // mvnmvnrepository.com 또는 search.maven.org 접속 - 'javax.servlet' 검색

        // org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사

        // - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'

        // ContextLoaderListener 변경 // ServletContextListener를 구현하도록 한다.

        // @WebListener 애노테이션을 붙혀, 서블릿 컨테이너가 관리하게끔 한다.

     // ServletContextListener는 ApplicationContextListener처럼 context에 객체를 담아 관리하는 목적이다.

        // Servlet에 RequestMapping 애노테이션을 붙혀 IoC 컨테이너가 객체관리를 했었다.

        // 이제부터 Servlet은 ServletContextLisner가 관리한다.

        // 따라서 RequestMappingHandlerMapping관련 코드가 필요가 없다. 

        // 기존에는 Context를 관리하는 ApplicationContextListener에 IoC 컨테이너가 들어가 있었다.

        // 여기서 Servlet와 Context를 Servlet에서 관리하고, 그 외의 객체는 IoC 컨테이너가 관리한다.

        // Context 폴더 삭제 // ApplicationContextListener를 사용하지 않기 때문에

        // *Servelt 변경 // 

//@Component
@WebServlet("/board/addForm")
public class BoardAddFormServlet extends GenericServlet {

  private static final long serialVersionUID = 1L;

  @Override
  public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {

    res.setContentType("text/html;charset=UTF-8");
    PrintWriter out = res.getWriter();

        // 이전에 작성해둔 html을 사용하려면 out객체가 필요한데, 파라미터로 받아 사용할 수 없다.

        // 대신 파라미터로 받은 ServletResponse에게 PrintWriter를 받아 사용할 수 있다 // getWriter() 호출

       // PrintWriter를 호출하기 전에, ContentType // 즉 어떤 언어를 사용해야 하는지 미리 알려줘야 한다.

        // 또한 serialVersionUID를 설정하여야 한다 // 직렬화가 필요하다.

        // BoardAddServlet 변경 // Service를 호출하는 Servlet들 예시 //

        // 프로세스는 고객이 입력하고, 그 입력을 Servlet이 Service에게, Service가 Dao에게 전달했다.

        // 기존에는 Servlet은 Service를 생성자로 받아 사용했는데, 이는 IoC 컨테이너를 사용하여,

        // 생성자에 필요한 파라미터를 IoC 컨테이너에서 알아서 생성하여 넣어줬었다.

        // 이제 Servlet와 IoC 컨테이너는 ServletContextLisner가, *Service 객체는 IoC컨테이너가 관리한다.

        // Service를 호출하려면 ServletContext에서 IoC컨테이너를 꺼내고, IoC컨테이너에서 꺼내야 한다.

@WebServlet("/board/add")
public class BoardAddServlet extends GenericServlet {
  private static final long serialVersionUID = 1L;

  @Override
  public void service(ServletRequest req, ServletResponse res)
      throws ServletException, IOException {
    try {
      res.setContentType("text/html;charset=UTF-8");
      PrintWriter out = res.getWriter();

      ServletContext servletContext = req.getServletContext();
      ApplicationContext iocContainer =
          (ApplicationContext) servletContext.getAttribute("iocContainer");
      BoardService boardService = iocContainer.getBean(BoardService.class);

        // ServerApp 삭제 // Tomcat Server를 사용할 것이기 때문에 필요가 없다.

        // Tomcat Server로 기존의 만든 자바 class 파일들을 가져가기 위해 gradle을 사용한다.

        // 1. war 플러그인 추가 // 웹 애플리케이션 배치 파일을 생성하기 위함.

        // build.gradle // plugins // id 'war' 추가 // $ gradle bulid // server 경로에서 실행

        // war를 플러그인에 추가하고 gradle build를 하여야 한다. // 안하면 war파일이 생성되지 않는다.

        // 생성된 war 파일을 tomcat 경로 // webapps 폴더 안에 넣고 서버 실행 // 

        // 톰캣 서버는 해당 war파일과 동일한 이름으로 폴더를 만들고 압축을 푼다. 

        // ex) webapp/bitcamp-project-server

        // WebBrower에서 해당 Servlet 실행 // 

     // ex) board/list // http://localhost:9999/bitcamp-project-server/board/list // 이 경로로 들어가야 한다.

        // eclipse에서 tomcat server 실행하는 방법 //

        // preferences - Server - Runtime Encironments // add

        // C:\Users\heusw\server\apache-tomcat-9.0.33 // 경로입력

        // 웹애플리케이션 폴더 구조 //

        // 1. Web-INF // 웹 App의 설정 파일 두는 폴더

        // Web.xml // 설정파일, Deployment Description File(DD File)

        // classes // Java class file(.class)이나 실행관련파일(.xml, .properties)을 둔다.

        // lib // 의존 라이브러리를 둔다(.jar)

        // 추가적으로 HTML, CSS, JavaScript, GIF 등 정적 웹 자원을 둔다. 

        // 2. JSP 파일 // 추후 자세히 설명

        // 3. 기타 폴더

        // 웹애플리케이션과 URL //

        // 웹애플리케이션/a.html // ex) http://localhost:9999/myweb/a.html

        // 웹애플리케이션/aaa/b.html // ex) http://localhost:9999/myweb/aaa/b.html

        // http://서버주소:포트번호/웹애플리케이션폴더명/자원의경로

        // 웹애플리케이션폴더명 // 별도로 별명이 부여되어있다면 별명을 사용한다.

        // 자원의 경로 // resource path // 파일의 경로나 서블릿 경로를 말함

        // 파일의 경로 // 실제 폴더나 파일이 존재 // 서블릿 경로 // servlet path // 가상의 경로를 말한다.

        // ex) http://localhost:9999/myweb/aaa/bbb/c.html // myweb/aaa/bbb/c.html이 resource path이다.

        // myweb/board/list // 만약 뒤가 이렇게 servlet path를 가리키기도 한다.

        // '/'의 의미 // 

        // server root // 포트번호와 웹애플리케이션폴더명 사이의 '/'

        // web app root (=context root) // 웹애플리케이션폴더명과 자원의 경로 사이의 '/'

        // ex) <a href="/board/list"> </a> // board 앞에 '/'는 server root를 의미한다.

        // 웹 애플리케이션 배포 절차 //

        // src/main/java/안의 파일은 WEB-INF/classes/.../에 파일(*.class)로 두어야 하고,

        // src/main/resources/안의 파일은 WEB-INF/classes/.../에 파일(*.properties, *.xml)로 두어야 한다.

        // 의존라이브러리는 lib/에 파일(*.jar)로 두어야 한다.

        // 원래는 각각 해줘야 하는데, gradle build로 war파일을 만들어 넣어두면 된다.

        // $톰캣서버(servlet container)/webapps/에 gradle로 만든 파일(war)을 넣어두고

        // 서버를 실행시키면 알아서 압축이 풀린다. // 그러니깐 꼭 gradle 쓰자

v55_2 -> 이클립스 웹 프로젝트로 전환하기

        // 이클립스IDE에 웹애플리케이션 테스트 환경 구축하기 // 

        // 1. Preferences - Server - Runtime Encironments // add

        // C:\Users\heusw\server\apache-tomcat-9.0.33 // 경로입력 //

        // 2. Server 뷰 / 테스트 서버 환경 추가 

        // Server view에서 보여주는 Server는 C:/Users/heusw/eclipse-workspace/Servers/밑에 실재한다.

        // Tomcat v9.0 Server at localhost-config 폴더 내용은 톰캣서버/conf/* 밑에서 복사해온 것이다.

        // 3. 테스트 서버의 배치 폴더 준비 // Tomcat의 경우

        // 테스트 서버에 대해 최초로 실행할 때, 폴더가 생성된다. // 파일은 실행할 때마다 복사 해옴

        // C:\Users\heusw\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0

        // bin // 원본 서버의 bin 폴더 사용

        // conf // 2번에서 설명한 그 conf이다. // 톰캣서버/conf/* 밑에서 복사해온 것

        // lib // 원본 서버의 lib 폴더 사용

        // logs // 테스트 서버의 로그 파일

        // tmp // 테스트 서버에서 실행중에 중간 작업물을 보관할 용도로 사용

        // webapps // 테스트 서버에서 사용하지 않는다.

        // work // JSP를 서블릿으로 변환한 자바파일을 두는 폴더

        // wtpwebapps // 테스트 서버에 배치하는 애플리케이션을 두는 폴더 // 웹애플리케이션 폴더

        // 자동배치 //

       // 프로젝트 // src/main/java(*.java) /resource(*properties, *.xml) /webapp(*.html, *.css, *js, *.gif, *.jsp)

       // /webapp/WEB-INF/(* 기타)

       // 배치폴더 // tmp0/wtpwebapps/.../WEB-INF/classes // *.java 파일

       // tmp0/wtpwebapps/.../WEB-INF // *.html, *.css, *js, *.gif, *.jsp 파일

       // tmp0/wtpwebapps/... // * 기타 파일

       // 프로젝트 // Servers/Tomcat v9.0 Server at localhost-config/.../*.xml // Server view에 보이는 폴더

       // 배치폴더 // tmp0/conf/ // *.xml 파일

       // 이클립스에서 프로젝트를 변경하면, Eclipse-workspace 밑 배치폴더에 적용이 된다.

       // 실제로 실행이 되는 것은 배치폴더의 내용이 실행된다.

       // 즉, 실제 작성하는 위치와 실행하는 폴더의 위치가 다르다.

       // 어차피 프로젝트에 변경하면 배치폴더에 적용되니깐 절대로 배치폴더를 건드리지는 말자.

       // 개발자로써 알아야될 소양이기 때문에 짚고 넘어간 것이다. // 배치폴더를 건드리진 말자

       // build.gradle 변경 // 

       // id 'eclipse-wtp' // 이클립스 플러그인 기능 + 이클립스 웹 프로젝트용 설정 파일을 생성

       // id 'war' // 배치 관련 기능을 처리한다.

       // 프로젝트 아이콘에 지구본 모양이 추가된다. // 빨간 느낌표가 뜨면 cleanEclipse eclipse 다시 해보자

       // Server view - 마우스 오른쪽 클릭 - Add and Remove - Server Add - finish

       // Server view - 마우스 오른쪽 클릭 - Publish - tmp0/wtpwebapps/bitcamp-project-server 폴더 확인

       // 정적자원 폴더 추가하기 //

       // src/main/webapp/ 생성

       // eclipse 프로젝트 설정 파일을 생성한 후에 이 디렉토리를 만들면, 설정파일에 이 디렉토리 정보가 포함되지 않는다.

       // 다시 'gradle eclipse'를 실행하여 설정파일을 만들자.

       // src/main/webapp/index.html 파일 생성

v56_1 -> HttpServlet 클래스 상속

       // XxxServlet 변경 // GenericServlet을 상속받아 구현했었는데, HttpServlet를 상속받아 구현하게 변경한다.

       // XxxAddFormServlet을 GET으로 처리하게 XxxAddServlet doGet() 메서드로 옮긴다.

       // Service() 메서드를 doPost()로 옮긴다.

       // <form action='add' method='post'> // HTML코드 추가 // doPost()를 가지고 있는 Add, Update

       //  doGET메서드 다음에 POST메서드로 넘어갈 수 있게 설정해줘야 한다.

       // Add와 Update의 경우 추가되어야 하는 data가 url에 보이면 안되기 때문에, POST 요청으로 실행(데이터를 담아 올 때)하고

       // delete / Update, Add(data 입력할 폼 요청) // 번호나 입력할 폼 요청이기 때문에 GET으로 실행하도록 한다.

v56_2 -> Servlet을 이용하여 Spring IoC 컨테이너 준비하기

       // ContextLoaderListener 변경 // Servlet으로 Spring IoC 컨테이너를 준비할 것이기 때문에

       // @WebListener를 주석으로 막는다. // Listener가 작용하면 안된다.

       // LoaderListener를 사용하는게 조금 더 현재 트렌드에 가깝다. // 밑에 적을 Servlet 방법이 비교적 구식

       // 그러나 둘 다 실무에서 고르게 사용하기 때문에 둘 다 알아둬야 한다.

       // lms.servlet.AppInitServlet 추가 // Servlet에서 Spring IoC 컨테이너를 준비하여 공유하게 한다.

       // AppInitServlet // 다른 Servlet이 사용할 IoC 컨테이너를 준비하는 역할을 한다.

       // static Logger logger = LogManager.getLogger(AppInitServlet.class); // 로그 출력하게 로그도 가져온다.

       // loadOnStartup을 이용 웹 애플리케이션이 시작할 때 자동 생성되게 한다.

       // @WebServlet 애노테이션에 loadOnStartup 속성을 추가한다.

       // service(), doXXX 메서드는 오버라이딩 하면 안된다. // 클라이언트 요청을 처리하는 Servlet이 아니기 때문

       // init() 오버라이딩 // init(ServletConfig config)를 오버라이딩 하지 않는 이유 //

       // HttpServlet에 들어가서 메소드를 확인해보면 알겠지만 init(ServletConfig config)가 init()를 호출하기 때문

       // getServletContext(); // ContextLoaderListener에서는 ServletContextEvent sce를 파라미터로 넘겨 받았지만,

       // HttpServlet를 상속받아 구현한 AppInitServlet에서는 GenericServlet에 getServletContext() 메서드가 구현되어있다.

       // 따라서 상속받은 클래스이기 때문에 메서드를 단순히 호출하기만 하면 된다.

       // ContextLoaderListener는 ServletContextListener를 상속받아 만들어진 클래스라 파라미터로 받았어야 했던 것이다.

       //  load-on-startup 처리 방법 // 1. 애노테이션으로 처리하는 방법 // 2. DD File(web.xml)에 설정하는 방법

       // @WebServlet(value="/AppInitServlet", loadOnStartup = 1) // 1. 애노테이션을 붙힌다. // 1번으로 객체를 생성한다.

       // value를 붙히지 않으면 실행이 안되기 때문에, URL을 지정해줘야 한다. // 실행이 안돼서 그냥 붙히는 것이다.

       // 객체를 생성하고 init를 호출하면, Spring IoC 컨테이너를 생성하고, ServletContext에 담아 보관하는 일을 한다.

       // Spring IoC 컨테이너를 생성하면, Spring IoC 컨테이너가 이제 기타 객체들을 만들어 보관한다.

       // src/main/webapp/WEB-INF/web.xml 생성 // 2. WEB-INF 폴더와 web.xml 파일을 생성한다.

	<servlet>
	  <servlet-name>AppInitServlet</servlet-name>
	  <servlet-class>com.eomcs.lms.servlet.AppInitServlet</servlet-class>
	  <load-on-startup>1</load-on-startup>
	</servlet>

       // 만든 AppInitServlet을 등록하고 load-on-startup을 1로 설정한다. // servlet-mapping은 안만든다.

       // 이 class는 client가 호출할 게 아니기 때문에 servlet-mapping을 만들지 않는다. // 만들면 안된다.

       // 프로세스 // 1. 별도로 호출하지 않아도 load-on-startup으로 AppInitServlet을 만들게 한다.

       // 2. Servlet에서 IoC 컨테이너 준비 등 초기에 필요한 작업을 담당하게 한다. // 또한 IoC 컨테이너 보관

       // localhost:9999/bitcamp-project-server/AppInitServlet 접속 // 접속시 오류창의 내용은 이런 Servlet이 없다는 게 아니다.

       // GET이나 POST 요청을 처리할 doGET, doPOST 메서드가 없다는 이야기이다. // 당연히 메서드 구현하지 않는다.

v56_3 -> redirect와 refresh 활용

       // doPost() 메서드 변경 // add, update //

       // response.sendRedirect("list"); // 작업을 완료한 후 다른 페이지를 가라고 클라이언트에게 URL을 보낸다.

       // sendRedirect("경로"); // URL이 '/'로 시작하면 서버 루트를 의미한다. // '/'로 시작하지 않으면 상대 경로를 의미한다.

       // 위에서는 '/'로 시작하지 않기 때문에 상대경로를 의미한다. 

       // 상대경로란 리다이렉트 메시지를 받기전의 URL(/bitcamp-project-server/board/add)를 기준으로

       // 계산한 경로(/bitcamp-project-server/board/list)이다.

       // 즉 /bitcamp-project-server/board/add에서 실행됐기 때문에, /bitcamp-project-server/board/까지는 가지고 있고,

       // /bitcamp-project-server/board/경로에서 list를 실행하라. // /bitcamp-project-server/board/list라는 뜻이다.

       // redirect의 단점은 바로 URL을 보내 페이지 이동을 시켜버리기 때문에,

       // add, update가 실패했는지 정상적으로 처리 됐는지 메세지를 출력하지 않는다. 

       // ErrorServlet 추가 // 에러 발생시 ErrorServlet로 보내기 위해 만든다.

       // xxxServlet 변경 // 

      if (boardService.delete(no) > 0) { // 삭제했다면,
        response.sendRedirect("list");
      } else {
        response.sendRedirect("../error");
      }

       // list는 /bitcamp-project-server/board/delete와 같은 경로내에 있고 // /bitcamp-project-server/board/list

       // error는 /bitcamp-project-server/board/delete보다 상위 경로에 있다. // /bitcamp-project-server/error

       // 따라서 ../로 상위 폴더를 지정한다.

      if (boardService.delete(no) > 0) {
        response.sendRedirect("list");
      } else {
        request.getSession().setAttribute("errorMessage", "삭제할 게시물 번호가 유효하지 않습니다.");
        request.getSession().setAttribute("url", "board/list");
        response.sendRedirect("../error");
      }

       // XxxServlet 변경 // Session을 사용해서, 각 Servlet에서 발생한 오류를 ErrorServlet에게 넘기도록 한다.

       // Session을 사용할 때 // 여러 절차로 이루어지는 한 작업을 수행할 때 // 여러 Servlet이 한가지 작업을 수행할 때,

       // 트렌젝션을 수행하는 용도로 직접 Data를 공유한다.

v56_4 -> 포워딩과 인클루딩

      if (boardService.update(board) > 0) {
        response.sendRedirect("list");
      } else {
        throw new Exception("변경할 게시물 번호가 유효하지 않습니다.");
      }

    } catch (Exception e) {
      request.setAttribute("error", e);
      request.setAttribute("url", "list");
      request.getRequestDispatcher("/error").forward(request, response);
    }

       // xxxServlet 변경 // Add, Update, delete 변경 // 등록&변경중 오류 발생시 ErrorServlet으로 무조건 포워딩하게끔 만든다.

       // 기존에는 error servlet 경로로 redirect를 시켰다. // 주소창에서도 error로 출력된다.

       // 그러나 변경 후는 주소창에서는 오류 발생한 명령어 그대로 남아있는데,

       // error가 발생했기 때문에 error에게 처리하라고 포워딩을 하는 것이다.

       // ErrorServlet 변경 // ((Exception) request.getAttribute("error")).getMessage());

       // 기존에는 트렌젝션을 사용하는 redirect 여서 Session에 보관하였으나, 

       // 이제는 처리를 아예 별도 서블릿에게 맡기는, 포워딩으로 처리하여 HttpServletRequest에 보관을 한다.

       // 원래 예외처리가 발생하면, ServletException(e) 와 같은 식으로 처리 하였었는데,

       // new Exception("변경할 게시물 번호가 유효하지 않습니다."); // 오류 메세지를 받는 객체를 만들어 그 객체를 넘긴다.

    Exception error = (Exception) request.getAttribute("error");
    out.printf("<p>%s</p>", error.getMessage());

       // ErrorServlet에서는 HttpServletRequest에서 오류객체를 꺼내고, error 객체에 들어있는 메세지를 꺼내 출력하게 한다.

       // error 객체는, Exception class의 객체이며, Exception class는 Throwable class의 자식 클래스이다.

       // Throwable에는 getMessage()라는 메서드가 구현되어 있으며, getMessage() 메서드는 처음 error 객체를 만들 때,

       // 생성자로 넘겨주었던 "변경할 게시물 번호가 유효하지 않습니다."를 리턴한다. 

       // Throwable class는 인터페이스가 아니며, setMessage() 메서드는 없다 // 생성자로만 Message를 받는다.

       // 또한 원래 포워딩시 doGet 메서드에서 넘겨준다면, doGet으로 받고, Post는 Post로 받아야 한다.

       // 그러나 ErrorServlet은 doGet에서 넘어올 수도 // delete // doPost에서 넘어올 수도 있다. // add, update

       // 따라서 ErrorServlet의 메서드 명을 service로 바꿔준다. 

v56_5 -> HttpSession을 이용하여 로그인 로그아웃 처리하기.

       // LoginServlet 변경 //

request.getSession().setAttribute("loginUser", member);

       // member 정보를 불러왔다면, HttpSession에 담아 보관한다.

       // HeaderServlet 변경 //

      out.println("  <ul class='navbar-nav mr-auto'>");
      
      
      
    out.println("  </ul>");
    Member loginUser = (Member) request.getSession().getAttribute("loginUser");
    if (loginUser != null) {
      out.printf("  <span class='navbar-text'>%s</span>\n", //
          loginUser.getName());
      out.println("  <a href='../auth/logout' class='btn btn-success btn-sm'>로그아웃</a>");
    } else {
      out.println("  <a href='../auth/login' class='btn btn-success btn-sm'>로그인</a>");
    }
    out.println("</div>");

       // navbar-nav mr-auto를 적용한다는 태그를 먼저 넣어주고 // 이전에는 navbar-nav 였다

       // ul 밑에 HttpSession에 담겨있는 member 정보가 있다면, member이름과 로그아웃을 출력하게 변경한다.

       // LogoutServlet 추가 //

@WebServlet("/auth/logout")
public class LogoutServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    request.getSession().invalidate();
    response.sendRedirect("../index.html");
  }
}

       // LogoutServlet 이 호출되면, HttpSession을 무효화시키고, index 페이지로 보낸다. 

       // index가 /auth와 같은 선상에 있기 때문에 ../ 상위 폴더로 보낸 후 index로 이동시켜야 한다.

v56_6 -> Cookie를 활용하여 사용자 정보 보관

      String email = request.getParameter("email");
      String password = request.getParameter("password");

      Cookie cookie = new Cookie("email", email);
      cookie.setMaxAge(60 * 60 * 24 * 30);
      response.addCookie(cookie);

      Member member = memberService.get(email, password);

       // doPost에서 cookie를 한 달 동안 유지되게 설정 한 후 client에게 보낸다. // client는 HDD에 보관한다.

      String email = "";
      Cookie[] cookies = request.getCookies();
      if (cookies != null) {
        for (Cookie cookie : cookies) {
          if (cookie.getName().equals("email")) {
            email = cookie.getValue();
            break;
          }
        }
      }
      
      
      
      out.println("<h1>로그인</h1>");
      out.println("<form action='login' method='post'>");
      out.printf("이메일: <input name='email' type='email' value='%s'>\n", email);
      out.println("<input type='checkbox' name='saveEmail'> 이메일 저장해두기<br>");
      out.println("암호: <input name='password' type='password'><br>");
      out.println("<button>로그인</button>");
      out.println("</form>");

       // doGet에서 email이라는 이름으로 저장된 Cookie를 찾아 있으면, email에 저장한다. // getCookies() 리턴값은 Cookie 배열이다. 

       // email 값이 있다면, email 값을 자동으로 넣는다..

       // checkbox를 체크했으면 email을 저장하고, 안했다면 저장 안하게 하기 위해 checkbox를 추가한다.

       // 위의 코드는 client가 체크박스에 체크를 했는지 안했는지를 doGet에서 doPost로 값 보내는 용도이다.

      String email = request.getParameter("email");
      String password = request.getParameter("password");
      String saveEmail = request.getParameter("saveEmail");

      Cookie cookie = new Cookie("email", email);
      if (saveEmail != null) {
        cookie.setMaxAge(60 * 60 * 24 * 30);
      } else {
        cookie.setMaxAge(0);
      }
      response.addCookie(cookie);

       // doPost에서 Client가 CheckBox를 체크했는지 알기 위해, saveEmail을 꺼내 확인한다.

       // 체크를 하지 않았다면, cookie를 삭제한다. // setMaxAge에 0을 넣으면 삭제된다.

v56_7 -> Filter를 사용하여 사용자 접근 제어

@WebFilter("/*")
public class AuthFilter implements Filter {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;
    String servletPath = httpRequest.getServletPath();
    if (servletPath.endsWith("add") || servletPath.endsWith("delete") || servletPath.endsWith("update")) {
      Member loginUser = (Member) httpRequest.getSession().getAttribute("loginUser");
      if (loginUser == null) {
        httpResponse.sendRedirect("../auth/login");
        return;
      }
    }

    chain.doFilter(request, response);
  }

       // @WebFilter("/*") // 모든 경로((/*)에 Filter를 적용한다. // AuthFilter를 거치게 한다.

       // add, delete, update로 끝날 때, loginUser정보가 없다면 login으로 이동시킨다.

v56_8 -> 파일 업로드 기능 추가

       // Servlet 3.0에 추가된 멀티파트 데이터 처리 기능을 이용하여 처리한다.

      out.println("<h1>회원 입력</h1>");
      out.println("<form action='add' method='post' enctype='multipart/form-data'>");
      out.println("이름: <input name='name' type='text'><br>");
      out.println("이메일: <input name='email' type='email'><br>");
      out.println("암호: <input name='password' type='password'><br>");
      out.println("사진: <input name='photo' type='file'><br>");
      out.println("전화: <input name='tel' type='tel'><br>");
      out.println("<button>제출</button>");
      out.println("</form>");

       // MemberAddServlet 변경 // doGet에 enctype='multipart/form-data' 코드를 추가 // photo type='file' 로 변경

       // 이 것을 적용해줘야 멀티파트를 사용할 준비가 된다.

       // @MultipartConfig(maxFileSize = 10000000) // 애노테이션 추가까지 해야, 멀티파트가 적용된다.

      Part photoPart = request.getPart("photo");
      if (photoPart.getSize() > 0) {
        String dirPath = getServletContext().getRealPath("/upload/member");
        String filename = UUID.randomUUID().toString();
        photoPart.write(dirPath + "/" + filename);
        member.setPhoto(filename);
      }

       // doPost에서 file을 저장할 때 Part를 이용한다.

       // getServletContext().getRealPath(실제경로) // 실제 사진파일을 저장할 경로를 준비한다.

       // UUID.randomUUID().toString(); // 파일명이 겹치면 안되기 때문에, UUID의 ramdom 메서드를 이용하여 파일명을 만든다.

      if (member != null) {
        out.println("<form action='update' method='post' enctype='multipart/form-data'>");
        out.printf("<img src='../upload/member/%s' height='600'><br>\n", member.getPhoto());
        out.printf("번호: <input name='no' type='text' readonly value='%d'><br>\n", //
            member.getNo());
        out.println("암호: <input name='password' type='password'><br>");
        out.printf("사진: <input name='photo' type='file'><br>\n", //
            member.getPhoto());
        out.println("<p><button>변경</button>");
        out.printf("<a href='delete?no=%d'>삭제</a></p>\n", //
            member.getNo());
        out.println("</form>");
      } else {
        out.println("<p>해당 번호의 회원이 없습니다.</p>");
      }

       // MemberDetailServlet 변경 // <img src='../upload/member/%s' 로 img 경로를 지정한다. // 실제 파일이 있는 경로

       // member.getPhoto()로 파일명을 불러온다 // height='600' // 크기를 지정한다.

       // MemberUpdateSevlet 변경 // MemberAddServlet와 같이 변경한다.

        out.println("사진: <input name='photo' type='file'><br>");
        out.println("사진: <input name='photo' type='file'><br>");
        out.println("사진: <input name='photo' type='file'><br>");
        out.println("사진: <input name='photo' type='file'><br>");
        out.println("사진: <input name='photo' type='file'><br>");

       // PhotoBoardAddServlet 변경 // doGet에서 enctype='multipart/form-data' 설정하고, name 이름을 같은 것으로 변경한다.

      Collection<Part> parts = request.getParts();
      String dirPath = getServletContext().getRealPath("/upload/photoboard");

      for (Part part : parts) {
        if (!part.getName().equals("photo") || part.getSize() <= 0) {
          continue;
        }

        String filename = UUID.randomUUID().toString();
        part.write(dirPath + "/" + filename);
        photoFiles.add(new PhotoFile().setFilepath(filename));
      }

       // doPost에서 getParts() // Part 배열로 받는다. 

v57_1 -> JSP 적용하기

       // JSP // java Servlet Page // 응답 HTML 출력 담당

       // JSP는 HTML 양식을 그대로 가지고 있으면서, Java 코드를 작성할 수 있다. // HTML을 자동으로 out.print문을 붙혀서 출력해준다.

       // Java 코드를 가지고 JSP에서 실행 시켜주는 것이 아니다. // 작성한 HTML 코드를 out.print를 붙혀 HTML에게 전달하는 역할이다.

<%@ page import="com.eomcs.lms.domain.Board"%>
<%@ page import="java.util.List"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    trimDirectiveWhitespaces="true"%>

<jsp:include page="/header.jsp"/>

  <h1>게시글(JSP2)</h1>
  <a href='add'>새 글</a><br>
  <table border='1'>
  <tr>
    <th>번호</th>
    <th>제목</th>
    <th>등록일</th>
    <th>조회수</th>
  </tr>
<% 
  List<Board> list = (List<Board>) request.getAttribute("list");
  for(Board item : list) {
%>
  <tr>
    <td><%=item.getNo()%></td> 
    <td><a href='detail?no=<%=item.getNo()%>'>=> <%=item.getTitle()%></a></td> 
    <td><%=item.getDate()%></td> 
    <td><%=item.getViewCount()%></td>
  </tr>
<%
  }
%>
</table>

<jsp:include page="/footer.jsp"/>

       // <%@ page import="com.eomcs.lms.domain.Board"%> // import 방식은 이렇게 선언한다

       // trimDirectiveWhitespaces="true"%> // trim은 불필요한 enter들을 다 제거해준다.

       // Java 코드를 삽입할 때에는 <% %> 사이에 집어 넣으면 된다. // 값을 집어 넣을때는 공백이 없어야 한다.

       // ex)<%=item.getDate()%> // %>와 Java코드가 딱 붙어있다 (O) //<%=item.getDate() %> // 한 칸 떨어져있다 (X) // 실행안댐.

       // 모든 jsp 파일에 공통적으로 stylesheet와 script 코드 등 을 작성하지 않기 위해, header.jsp와 footer.jsp로 구분한다.

       // header.jsp와 footer.jsp를 include로 처리한다. // header는 본문 시작 하기 전에 넣어야 한다.

       // getbootstrap.com 접속 // getStarted 클릭 // 보통 CSS는 </head> 앞에, JS는 </body> 앞에 넣는다.

        // 클래스와 상관 없이 필터를 삽입하고 싶을 때에는 AOP 기술을 사용한다.

        // JSP와 Page Controller를 사용전, 필터를 삽입 할 때는 Interceptor 작성 기술을 사용한다.

        // 1. 요청 핸들러 정의   2. 파라미터 선언   3. 값 리턴   4. 의존객체 주입 // 작성 방법

        // 서블릿 전체나, 특정 서블릿에 대해 필터를 사용하고 싶을 때는 ServletFilter를 사용한다.

v57_2 -> JSP에 EL 적용하기

<%@ page import="com.eomcs.lms.domain.Board"%>
<%@ page import="java.util.List"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    trimDirectiveWhitespaces="true"%>

<jsp:include page="/header.jsp"/>

  <h1>게시글(JSP + EL)</h1>
  <a href='add'>새 글</a><br>
  <table border='1'>
  <tr>
    <th>번호</th>
    <th>제목</th>
    <th>등록일</th>
    <th>조회수</th>
  </tr>
<jsp:useBean id="list" 
  type="java.util.List<Board>"
  class="java.util.ArrayList"
  scope="request"/>
<% 
  for(Board item : list) {
    pageContext.setAttribute("item", item);
%>
  <tr>
    <td>${item.no}</td> 
    <td><a href='detail?no=${item.no}'>=> ${item.title}</a></td> 
    <td>${item.date}</td> 
    <td>${item.viewCount}</td>
  </tr>
<%
  }
%>
</table>

<jsp:include page="/footer.jsp"/>
    

       // pageContext에서 item을 꺼내, EL을 no, title 등 해당하는 항목들의 값을 꺼낸다.

v57_3 -> JSP에 JSTL 적용하기

<%@ page import="com.eomcs.lms.domain.Board"%>
<%@ page import="java.util.List"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    trimDirectiveWhitespaces="true"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<jsp:include page="/header.jsp"/>

  <h1>게시글(JSP + EL + JSTL)</h1>
  <a href='add'>새 글</a><br>
  <table border='1'>
  <tr>
    <th>번호</th>
    <th>제목</th>
    <th>등록일</th>
    <th>조회수</th>
  </tr>
  
<c:forEach items="${list}" var="item">
  <tr>
    <td>${item.no}</td> 
    <td><a href='detail?no=${item.no}'>=> ${item.title}</a></td> 
    <td>${item.date}</td> 
    <td>${item.viewCount}</td>
  </tr>
</c:forEach>

</table>

<jsp:include page="/footer.jsp"/>
    

       // JSP에 JSTL을 적용하여 자바 반복문을 c:forEach로 바꾸었다. // JSP에서 자바코드를 모두 제거하였다.

v58_1 -> Front Controller 설계 기법 적용하기

       // Front Controller // 컨트롤러들의 공통 기능을 가져와서 통합 처리한다.

       // 외부의 접점을 하나로 줄임으로써 요청을 제어하기가 쉬워진다.

       // 원래는 Client의 요청에 따라 다이렉트로 Servlet에게 갔다면, Front Controller를 적용하여,

       // Client의 요청이 Front Controller에게 모두 전달되게 한다. 

       // Front Controller에서 요청에 맞는 Servlet을 호출하도록 한다.

        // DispatcherServlet 추가 // /app/* 요청을 처리한다.

        // 모든 Servlet이 /app/을 거친 후에 요청을 받게 한다.

        // Servlet이 직접 JSP를 인클루딩 하는 대신에, JSP URL을 ServletRequest에 저장한다.

        // 직접 리다이렉트 하는 대신에, 리다이렉트 URL을 ServletRequest에 저장한다.

        // 직접 예외처리 하는 서블릿으로 포워딩 하는 대신에, ServletRequest에 저장한다.

  @Override
  protected void service(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {

    String pathInfo = request.getPathInfo();
    response.setContentType("text/html;charset=UTF-8");
    ArrayList<Cookie> cookies = new ArrayList<>();
    request.setAttribute("cookies", cookies);
    request.getRequestDispatcher(pathInfo).include(request, response);

    if (cookies.size() > 0) {
      for (Cookie cookie : cookies) {
        response.addCookie(cookie);
      }
    }

    if (request.getAttribute("error") != null) {
      Exception error = (Exception) request.getAttribute("error");
      StringWriter out = new StringWriter();
      error.printStackTrace(new PrintWriter(out));
      request.setAttribute("errorDetail", out.toString());
      request.getRequestDispatcher("/error.jsp").forward(request, response);
      return;
    }

    String refreshUrl = (String) request.getAttribute("refreshUrl");
    if (refreshUrl != null) {
      response.setHeader("Refresh", refreshUrl);
    }

    String viewUrl = (String) request.getAttribute("viewUrl");
    if (viewUrl.startsWith("redirect:")) {
      response.sendRedirect(viewUrl.substring(9));
    } else {
      request.getRequestDispatcher(viewUrl).include(request, response);
    }
  }

        // DispatchcerServlet에서 모든 요청을 처리해야 되기 때문에 Service 메서드를 오버라이딩 한다.

        // request.getPathInfo() // 서블릿 경로(/app) 다음에 오는 자원의 경로를 알아낸다.

        // getServletPath()는 서블릿 경로를 알려주는 것이라, /app을 리턴한다. // /app/자원의 경로로 변경을 하였기 때문에

        // 서블렛 경로이기는 하지만 자원의 경로에 해당된다. // /app적용 이전에는 서블렛 경로였다.

        // 서블릿을 인크루딩 하는 경우에, 쿠키 적용이 되지 않는다.

        // SetAttribute("cookies", cookies); // 인클루딩 서블릿에서 쿠키 정보를 담을 수 있도록 리스트 객체를 준비

        // request.getRequestDispatcher(pathInfo).include(request, response);

        // Dispatcher에게 Servlet(자원의 경로)을 알려주고 include하여 Servlet을 실행시킨다.

        // response.addCookie(cookie); // include 실행 후 쿠키가 있다면 쿠키를 응답헤더에 추가한다

        // 그 후, Error 객체가 있으면 Error를 실행하고, refreshUrl을 전달했다면 refresh를 실행하고,

        // 둘 다 null이라면, Servlet이 알려준 Url을 실행하도록 한다. // 단, Servlet에서 넘겨주는 Url은 redirect가 포함되어 있다.

        // 따라서 redirect:를 제거한 나머지 Url을 사용하여야 한다.

      boardService.add(board);
      request.setAttribute("viewUrl", "redirect:list");

    } catch (Exception e) {
      request.setAttribute("error", e);
      request.setAttribute("url", "list");
    }

        // 원래 담당하던 jsp 경로를 알려주던 코드와, Error 경로에게 보내는 코드를 삭제하고 Url을 담게만 한다. 

v58_2 -> Page Controller를 POJO로 전환하고 Spring IoC 컨테이너에서 관리하기

       // POJO(Plain Old Java Object) // 

       // Page Controller가 Servlet을 관리하게 하던 것을 IoC 컨테이너가 해당 객체를 관리하게 한다.

       // Page Controller는 HttpServlet을 상속받아 사용하여야 했다.

       // IoC 컨테이너가 관리하게 되면, 해당 Servlet을 상속 받을 일도 없고, 객

       // com.eomcs.util.RequestMapping, RequestHandler, RequestMappingHandlerMapping 추가 //

       // 원래는 기존에 삭제 되어있어야 했으나, 안지우고 계속 살려두고 와서 그 전 버전에도 계속 있던 것이다.

       // ContextLoaderListener 변경 //

      RequestMappingHandlerMapping handlerMapper = //
          new RequestMappingHandlerMapping();
      String[] beanNames = iocContainer.getBeanNamesForAnnotation(Component.class);
      for (String beanName : beanNames) {
        Object component = iocContainer.getBean(beanName);

        Method method = getRequestHandler(component.getClass());
        if (method != null) {
          RequestHandler requestHandler = new RequestHandler(method, component);

          handlerMapper.addHandler(requestHandler.getPath(), requestHandler);
        }
      }
      servletContext.setAttribute("handlerMapper", handlerMapper);

       // new RequestMappingHandlerMapping(); // 원래 handlerMapper.addHandler 위에 있는게 맞다.

       // getBeanNamesForAnnotation(Component.class); // Component 애노테이션이 붙은 객체의 이름을 String 배열에 담는다.

       // iocContainer.getBean(beanName); // 해당하는 이름을 가진 객체를 component에 담는다.

       // getRequestHandler(component.getClass()); // 꺼낸 component의 정보에서 원하는 Method를 추출한다.

  private Method getRequestHandler(Class<?> type) {
    Method[] methods = type.getMethods();
    for (Method m : methods) {
      RequestMapping anno = m.getAnnotation(RequestMapping.class);
      if (anno != null) {
        return m;
      }
    }
    return null;
  }

        // 원하는 Method를 추출하기 위해, 해당 메서드를 추가한다. 

        // 이 메서드의 목적은 component가 가지고 있는 메서드 중, RequestMapping이 붙은 1개의 메서드를 추출하는 역할이다.

       // new RequestHandler(method, component); // method와 component를 Handler에 담는다.

        // handlerMapper.addHandler(requestHandler.getPath(), requestHandler);

        // handlerMapper에 해당 Handler의 경로(getPath())와 Handler를 담는다.

        // 즉 위의 메서드는 RequestMapping이 붙은 메서드를 찾아서, RequestMappingHandlerMapping에 보관하는 것이다.

        // 만든 RequestMappingHandlerMapping를 ServletContext에 담는다. // 다른데서도 사용하기 위함.

        // DispatcherServlet 변경 // 

  @Override
  public void init() throws ServletException {
    handlerMapper = (RequestMappingHandlerMapping) getServletContext()//
        .getAttribute("handlerMapper");
  }

        // getServletContext()에서 RequestMappingHandlerMapping를 꺼내서 저장한다.

        // 자주 사용하기 때문에 변수에 미리 담아두는 것이다.

        // 안담아두면 사용할 때마다 ServletContext을 호출하고 꺼내서 사용해야 함.

      RequestHandler requestHandler = handlerMapper.getHandler(pathInfo);

      String viewUrl = null;

      if (requestHandler != null) {
        try {
          viewUrl = (String) requestHandler.getMethod().invoke(
              requestHandler.getBean(),
              request,
              response
          );
        } catch (Exception e) {
          StringWriter out = new StringWriter();
          e.printStackTrace(new PrintWriter(out));
          request.setAttribute("errorDetail", out.toString());
          request.getRequestDispatcher("/error.jsp").forward(request, response);
          return;
        }
      } else {
        throw new Exception("해당 명령을 지원하지 않습니다.");
      }

        // handlerMapper에서 Client로부터 넘어온 pathInfo에 해당하는 Handler를 달라고 한다.

        // handler가 들어있다면, requestHandler.getMethod().invoke로, 해당 메서드를 호출하고

        // 해당 메서드의 객체와 해당 메서드에 넘겨줄 파라미터 request, response를 넘겨준다.

        // request와 reponse는 당연히 HttpServlet을 의미한다. // 파라미터에 선언된 것이기 때문

        // 해당하는 작업 실행중 오류가 발생하면, 오류를 받아 오류 jsp로 포워딩시킨다.

        // viewUrl이 정상적으로 들어있다면, 해당 Url로 include한다.

        // xxxServlet 변경 // Controller로 변경

@Component
public class BoardAddController {

  @Autowired
  BoardService boardService;

  @RequestMapping("/board/add")
  public String add(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (request.getMethod().equals("GET")) {
      return "/board/form.jsp";
    }

    Board board = new Board();
    board.setTitle(request.getParameter("title"));
    boardService.add(board);
    return "redirect:list";
  }
}

        // 모든 작업은 이제 jsp가 실행하므로, Servlet으로 존재할 이유가 없다. // 일반 클래스로 있어도 된다.

        // 따라서 해당 작업에 해당하는 jsp을 리턴하게 변경한다. // 이게 위에서 viewUrl이다.

        // com.eomcs.lms.filter.CharacterEncodingFilter 추가 // 요청 데이터에 한글 처리 필터를 붙힌다.

        // 붙히지 않으면 Client가 작성한 한글을 정상적으로 불러올 수 없다.

v58_3 -> CRUD 페이지 컨트롤러를 한 클래스로 합친다.

@Component
public class BoardController {

  @Autowired
  BoardService boardService;

  @RequestMapping("/board/add")
  public String add(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (request.getMethod().equals("GET")) {
      return "/board/form.jsp";
    }

    Board board = new Board();
    board.setTitle(request.getParameter("title"));
    boardService.add(board);
    return "redirect:list";
  }

  @RequestMapping("/board/delete")
  public String delete(HttpServletRequest request, HttpServletResponse response) throws Exception {
    int no = Integer.parseInt(request.getParameter("no"));
    if (boardService.delete(no) > 0) {
      return "redirect:list";
    } else {
      throw new Exception("삭제할 게시물 번호가 유효하지 않습니다.");
    }
  }

  @RequestMapping("/board/detail")
  public String detail(HttpServletRequest request, HttpServletResponse response) throws Exception {
    int no = Integer.parseInt(request.getParameter("no"));
    Board board = boardService.get(no);
    request.setAttribute("board", board);
    return "/board/detail.jsp";
  }

  @RequestMapping("/board/list")
  public String list(HttpServletRequest request, HttpServletResponse response) throws Exception {
    List<Board> boards = boardService.list();
    request.setAttribute("list", boards);
    return "/board/list.jsp";
  }

  @RequestMapping("/board/update")
  public String update(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (request.getMethod().equals("GET")) {
      int no = Integer.parseInt(request.getParameter("no"));
      Board board = boardService.get(no);
      request.setAttribute("board", board);
      return "/board/updateform.jsp";
    }

    Board board = new Board();
    board.setNo(Integer.parseInt(request.getParameter("no")));
    board.setTitle(request.getParameter("title"));

    if (boardService.update(board) > 0) {
      return "redirect:list";
    } else {
      throw new Exception("변경할 게시물 번호가 유효하지 않습니다.");
    }
  }
}

       // ***Controller 변경 // 나뉘어 있던 기능을 게시판에 맞게 합친다.

       // ContextLoaderListener 변경 //

      String[] beanNames = iocContainer.getBeanNamesForAnnotation(Component.class);
      for (String beanName : beanNames) {
        Object component = iocContainer.getBean(beanName);

        Iterator<Method> handlers = getRequestHandlers(component.getClass());
        while (handlers.hasNext()) {
          RequestHandler requestHandler = //
              new RequestHandler(handlers.next(), component);

          handlerMapper.addHandler(requestHandler.getPath(), requestHandler);

       // 원래 RequestMapping 애노테이션이 붙은 Method가 한개씩있었던 Controller들을,

       // 하나로 합쳐져서, Method가 여러개 존재하게 되었다.

        // 따라서 Iterater<Method>를 리턴하게 하여, handler에 hasNext()로 다음 Method가 있는지 확인하고,

       // 있다면, 다음 Method를 next()로 가져와서 저장하도록 한다.

  private Iterator<Method> getRequestHandlers(Class<?> type) {
    ArrayList<Method> handlers = new ArrayList<>();
    Method[] methods = type.getMethods();
    for (Method m : methods) {
      RequestMapping anno = m.getAnnotation(RequestMapping.class);
      if (anno != null) {
        handlers.add(m);
      }
    }
    return handlers.iterator();
  }

       // 파라미터로 넘어온 Class에 Method를 추출하여, 해당 메서드에 RequestMapping 애노테이션이 붙었는지 확인하고

        // 붙은 메서드를 ArrayList에 담고, ArrayList를 리턴하지 않고, Iterator를 리턴한다.

v58_4 -> CRUD 페이지 컨트롤러를 한 클래스로 합친다.

  public Map<String, Object> invoke(HttpServletRequest request, HttpServletResponse response)
      throws Exception {

    // 페이지 컨트롤러가 작업한 결과를 받을 바구니를 준비한다.
    HashMap<String, Object> model = new HashMap<>();

    // 메서드의 파라미터 목록을 꺼낸다.
    Parameter[] params = method.getParameters();
    String viewUrl = (String) method.invoke(//
        bean, // 메서드를 호출할 때 사용하는 인스턴스
        getArguments(params, request, response, model) // 메서드에 넘겨 줄 값들
    );

    // request handler를 호출하면,
    // model 객체에는 request handler가 담은 값이 보관되어 있다.
    // 여기에 request handler의 리턴 값(JSP URL)도 함께 보관한다.
    model.put("viewUrl", viewUrl);

    // request handler의 작업 결과물과 JSP URL을 담은 맵 객체를 프론트 컨트롤러에게 리턴한다.
    return model;
  }

  private List<String> getParameterNames(Class<?> type, Method method) throws Exception {
    // => 컴파일할 때 옵션으로 파라미터 이름을 포함하지 않는 한에는
    // 자바 reflection API로는 알아낼 수 없다.
    // => 그러나 .class 파일에는 분명히 파라미터 이름을 들어 있다.
    // => 그것을 꺼내려면 외부 라이브러리를 사용해야 한다.
    //
    Reflections reflections = new Reflections(type, // 클래스 타입
        new MethodParameterNamesScanner() // 파라미터 이름 탐색 플러그인 장착
    );
    List<String> paramNames = reflections.getMethodParamNames(method);

    logger.debug(String.format("%s.%s(", bean.getClass().getName(), method.getName()));
    for (int i = 0; i < method.getParameterCount(); i++) {
      logger.debug(String.format("   %s,", paramNames.get(i)));
    }
    logger.debug(")");

    return paramNames.subList(0, method.getParameterCount());
  }

  private Object[] getArguments(Parameter[] params, HttpServletRequest request,
      HttpServletResponse response, Map<String, Object> model) throws Exception {

    // 파라미터 이름 알아내기
    List<String> paramNames = getParameterNames(bean.getClass(), method);

    // 파라미터 값을 담을 배열을 준비한다.
    Object[] args = new Object[params.length];

    // 각 파라미터에 대한 값을 준비한다.
    for (int i = 0; i < params.length; i++) {
      args[i] = getArgument(paramNames.get(i), params[i], request, response, model);
    }

    return args;
  }

  private Object getArgument(String paramName, Parameter param, HttpServletRequest request,
      HttpServletResponse response, Map<String, Object> model) throws Exception {

    Class<?> paramType = param.getType();
    if (paramType == ServletRequest.class || paramType == HttpServletRequest.class) {
      return request;

    } else if (paramType == ServletResponse.class || paramType == HttpServletResponse.class) {
      return response;

    } else if (paramType == HttpSession.class) {
      return request.getSession();

    } else if (paramType == Map.class) {
      return model;

    } else if (paramType == Part.class) {
      return request.getPart(paramName);

    } else if (paramType == Part[].class) {
      ArrayList<Part> values = new ArrayList<>();
      Collection<Part> parts = request.getParts();
      for (Part part : parts) {
        if (part.getName().equals(paramName)) {
          values.add(part);
        }
      }
      return values.toArray(new Part[values.size()]);

    } else if (isPrimitiveType(paramType)) {
      return getPrimitiveValue(paramName, paramType, request);

    } else {
      return getPojoValue(paramName, paramType, request);
    }
  }

  @SuppressWarnings("unchecked")
  private Object getPojoValue(String paramName, Class<?> paramType, HttpServletRequest request)
      throws Exception {

    // 클라이언트가 보낸 데이터를 담을 POJO 객체를 생성한다.
    // => 기본 생성자를 호출하여 인스턴스를 초기화시킨다.
    Object pojo = paramType.getConstructor().newInstance();

    // 세터 메서드를 추출한다.
    Set<Method> methods = ReflectionUtils.getMethods(paramType, ReflectionUtils.withPrefix("set"));
    logger.debug(String.format("%s 의 세터 메서드:", paramType.getName()));

    // 클라이언트가 보낸 데이터 중에서 세터 메서드에 넘겨줄 데이터가 있다면 호출한다.
    for (Method m : methods) {

      // 메서드 이름에서 프로퍼티 명을 추출한다.
      // => 예: setCreatedDate() => "c" + "reatedDate" = createdDate
      String propName = m.getName().substring(3, 4).toLowerCase() + m.getName().substring(4);
      logger.debug("    " + propName);

      // 세터를 호출할 때 넘겨 줄 값을 담을 변수를 준비.
      Object value = null;

      if (isPrimitiveType(m.getParameters()[0].getType())) {
        // 클라이언트가 보낸 데이터 중에서 프로퍼티 이름과 일치하는 데이터가 있다면 꺼낸다.
        value = getPrimitiveValue(propName, m.getParameters()[0].getType(), request);
      }

      // 세터에 넘겨 줄 값을 준비하지 못했드면 다음 메서드로 넘어간다.
      if (value == null) {
        continue;
      }

      logger.debug(String.format("    %s()", m.getName()));
      m.invoke(pojo, value);
    }

    return pojo;
  }

  private Object getPrimitiveValue(String paramName, Class<?> paramType,
      HttpServletRequest request) {

    // 자바 원시 타입일 경우 요청 파라미터에서 값을 찾는다.
    String value = request.getParameter(paramName);
    if (value == null) {
      return null;
    }

    if (paramType == byte.class || paramType == Byte.class) {
      try {
        return Byte.parseByte(value);
      } catch (Exception e) {
        return (byte) 0;
      }
    } else if (paramType == short.class || paramType == Short.class) {
      try {
        return Short.parseShort(value);
      } catch (Exception e) {
        return (short) 0;
      }
    } else if (paramType == int.class || paramType == Integer.class) {
      try {
        return Integer.parseInt(value);
      } catch (Exception e) {
        return 0;
      }
    } else if (paramType == long.class || paramType == Long.class) {
      try {
        return Long.parseLong(value);
      } catch (Exception e) {
        return (long) 0;
      }
    } else if (paramType == float.class || paramType == Float.class) {
      try {
        return Float.parseFloat(value);
      } catch (Exception e) {
        return 0.0f;
      }
    } else if (paramType == double.class || paramType == Double.class) {
      try {
        return Double.parseDouble(value);
      } catch (Exception e) {
        return 0.0;
      }
    } else if (paramType == char.class || paramType == Character.class) {
      try {
        return value.charAt(0);
      } catch (Exception e) {
        return (char) 0;
      }
    } else if (paramType == boolean.class || paramType == Boolean.class) {
      try {
        return Boolean.parseBoolean(value);
      } catch (Exception e) {
        return false;
      }
    } else if (paramType == java.util.Date.class || paramType == java.sql.Date.class) {
      try {
        return java.sql.Date.valueOf(value); // 문자열 형식: "yyyy-MM-dd" 이어야 한다.
      } catch (Exception e) {
        return null;
      }
    }
    return value;
  }

  private boolean isPrimitiveType(Class<?> paramType) {
    if (paramType == byte.class || paramType == Byte.class || paramType == short.class
        || paramType == Short.class || paramType == int.class || paramType == Integer.class
        || paramType == long.class || paramType == Long.class || paramType == float.class
        || paramType == Float.class || paramType == double.class || paramType == Double.class
        || paramType == char.class || paramType == Character.class || paramType == boolean.class
        || paramType == Boolean.class || paramType == String.class
        || paramType == java.util.Date.class || paramType == java.sql.Date.class) {
      return true;
    }
    return false;
  }

        // RequestHandler에 위 메서드들을 추가한다. // invoke 메서드를 만든다.

  @Override
  protected void service(//
      HttpServletRequest request, //
      HttpServletResponse response) throws ServletException, IOException {
    try {
      String pathInfo = request.getPathInfo();
      response.setContentType("text/html;charset=UTF-8");
      ArrayList<Cookie> cookies = new ArrayList<>();
      request.setAttribute("cookies", cookies);
      RequestHandler requestHandler = handlerMapper.getHandler(pathInfo);

      String viewUrl = null;

      if (requestHandler != null) {
        try {
          Map<String, Object> model = requestHandler.invoke(request, response);
          viewUrl = (String) model.get("viewUrl");

          // 요청 핸들러의 작업 결과를 꺼내서 JSP가 사용할 수 있도록
          // ServletRequest 보관소에 저장한다.
          Set<Entry<String, Object>> entrySet = model.entrySet();
          for (Entry<String, Object> entry : entrySet) {
            request.setAttribute(entry.getKey(), entry.getValue());
          }
        } catch (Exception e) {
          StringWriter out = new StringWriter();
          e.printStackTrace(new PrintWriter(out));
          request.setAttribute("errorDetail", out.toString());
          request.getRequestDispatcher("/error.jsp").forward(request, response);
          return;
        }
      } else {
        logger.info("해당 명령을 지원하지 않습니다.");
        throw new Exception("해당 명령을 지원하지 않습니다.");
      }

      if (cookies.size() > 0) {
        for (Cookie cookie : cookies) {
          response.addCookie(cookie);
        }
      }

      String refreshUrl = (String) request.getAttribute("refreshUrl");
      if (refreshUrl != null) {
        response.setHeader("Refresh", refreshUrl);
      }

      if (viewUrl.startsWith("redirect:")) {
        response.sendRedirect(viewUrl.substring(9));
      } else {
        request.getRequestDispatcher(viewUrl).include(request, response);
      }
    } catch (Exception e) {
      throw new ServletException(e);
    }
  }
}

        // RequestHandler의 invoke를 호출하고 저장한 viewUrl과

        // 해당 Controller의 method 결과를 담은 map 객체를 리턴한다.

  @RequestMapping("/board/form")
  public String form() throws Exception {
    return "/board/form.jsp";
  }

  @RequestMapping("/board/add")
  public String add(Board board) throws Exception {
    boardService.add(board);
    return "redirect:list";
  }

        // 프론트 컨트롤러(우리 프로젝트에서는 DispatcherServlet)의 변경에 맞춰 페이지 컨트롤러(xxxController)를 변경한다.

v59_1 -> Spring WebMVC 적용

        // mvnmvnrepository.com 또는 search.maven.org 접속 - 'spring-webmvc' 검색

        // org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사

        // - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'

        // lms.ContextLoaderListener, filter.CharacterEncodingFilter, servlet.DispatcherServlet 삭제

        // util.RequestMapping, RequestHandler, RequestMappingHandlerMapping 삭제

        // 기존에 임시로 만든 Spring class는 삭제한다.

  <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/app/*</url-pattern>
  </filter-mapping>

  <servlet>
      <servlet-name>app</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <!-- DispatcherSerlvet이 사용할 IoC 컨테이너를 지정한다. -->
      <init-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
      </init-param>
      <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>com.eomcs.lms.AppConfig</param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
      <multipart-config>
        <max-file-size>5000000</max-file-size>
      </multipart-config>
  </servlet>

  <servlet-mapping>
      <servlet-name>app</servlet-name>
      <url-pattern>/app/*</url-pattern>
  </servlet-mapping>

        // src/main/webapp/WEB-INF/web.xml 변경 //

        // springframework 안에 DispatcherServlet 클래스, CharacterEncodingFilter 클래스를 등록한다. //

        // 또한 파일첨부 , 사진 첨부 등을 사용하기 위한 multipart-config를 등록한다.

@Controller
public class BoardController {

  static Logger logger = LogManager.getLogger(BoardController.class);

  @Autowired
  BoardService boardService;

  public BoardController() {
    logger.debug("BoardController 생성됨!");
  }

  @RequestMapping("/board/form")
  public String form() throws Exception {
    return "/board/form.jsp";
  }

  @RequestMapping("/board/add")
  public String add(Board board) throws Exception {
    boardService.add(board);
    return "redirect:list";
  }

        // xxxController 변경 // 

        // @Controller 애노테이션을 붙힌다. // springframework에서는 Controller임을 알려주기 위한 애노테이션을 설정해야 한다.

v59_2 -> Spring WebMVC 적용

public class AppWebApplicationInitializer
    extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] {AppConfig.class};
  }

  @Override
  protected Filter[] getServletFilters() {
    return new Filter[] { //
        new CharacterEncodingFilter("UTF-8") //
    };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] {"/app/*"};
  }

  @Override
  protected String getServletName() {
    return "app1";
  }


  @Override
  protected void customizeRegistration( //
  javax.servlet.ServletRegistration.Dynamic registration) {
  registration.setMultipartConfig(//
  new MultipartConfigElement(uploadTmpDir, // 업로드 한 파일을 임시 보관할 위치
  10000000, // 최대 업로드할 수 있는 파일들의 총 크기
  15000000, // 요청 전체 데이터의 크기
  2000000 // 업로드 되고 있는 파일을 메모리에 임시 임시 보관하는 크기
  ));
  }

}

        // web.AppWebApplicationInitializer 추가

        // customizeRegistration() //

        // Servlet 3.0 API의 파일 업로드를 사용하려면 DispatcherServlet에 설정을 추가해야 한다.

        // 즉 멀티파트 데이터를 Part 객체로 받을 때는 설정을 추가해야 한다.

        // DispatcherServlet 이 multipart/form-data 으로 전송된 데이터를 처리하려면 해당 콤포넌트를 등록해야 한다.

        // 단 이 설정을 추가하면 Spring WebMVC의 MultipartResolver가 작동되지 않는다.

@ComponentScan(value = "com.eomcs.lms")
@EnableWebMvc
public class AppConfig {

  static Logger logger = LogManager.getLogger(AppConfig.class);

  public AppConfig() {
    logger.debug("AppConfig 객체 생성!");
  }


  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver vr = new InternalResourceViewResolver(//
        "/WEB-INF/jsp/", // prefix
        ".jsp" // suffix
    );
    return vr;
  }

  @Bean
  public MultipartResolver multipartResolver() {
    CommonsMultipartResolver mr = new CommonsMultipartResolver();
    mr.setMaxUploadSize(10000000);
    mr.setMaxInMemorySize(2000000);
    mr.setMaxUploadSizePerFile(5000000);
    return mr;
  }

        // AppConfig 변경 //

        // multipartResolver() //

        // Spring의 방식으로 파일 업로드를 처리하고 싶다면, AppConfig.java에 MultipartResolver를 추가해야 한다.

        // @EnableWebMvc //

        // SpringMvc를 구성할때 필요한 Bean설정들을 자동으로 해주는 애노테이션

        // WebMVC 관련 애노테이션을 처리할 객체 등록

        // .jsp 파일 이동 // JSP 파일을 /WEB-INF/jsp/ 폴더로 옮긴다.

@Controller
@RequestMapping("/board")
public class BoardController {

  static Logger logger = LogManager.getLogger(BoardController.class);

  @Autowired
  BoardService boardService;

  public BoardController() {
    logger.debug("BoardController 생성됨!");
  }

  @GetMapping("form")
  public void form() throws Exception {}

  @PostMapping("add")
  public String add(Board board) throws Exception {
    boardService.add(board);
    return "redirect:list";
  }

        // xxxController 변경 //

        // @RequestMapping("경로") 설정 // 클래스에 선언할 수도, 메서드에 선언할 수도 있다.

        // @GetMapping("메서드명"), @PostMapping // 클래스에 @RequestMapping을 설정한 경우,

        // Get과 Post 요청을 구분하여 해당 메서드를 호출 할 수 있게 애노테이션을 부여한다.

 

@ControllerAdvice
public class GlobalControllerAdvice {

  @InitBinder
  public void initBinder(WebDataBinder binder) {

    // String 날짜 ==> java.sql.Date 객체
    binder.registerCustomEditor( //
        java.util.Date.class, //
        new PropertyEditorSupport() { //
          @Override
          public void setAsText(String text) throws IllegalArgumentException {
            setValue(java.sql.Date.valueOf(text));
          }
        });
  }
}

        // GlobalControllerAdvice 추가 //

        // 페이지 컨트롤러에 보조할 객체를 등록하기 위해 @ControllerAdvice 클래스를 정의한다.

        // 날짜 파라미터를 처리하기 위해 @InitBinder 메서드를 정의한다.

v59_3 -> DispatcherServlet 여러 개 설정하기

        // Spring의 WebApplicationInitializer 구현체를 통해 여러 개의 DispatcherServlet을 설정할 수 있다.

        // 공통 컴포넌트와 DispatcherServlet 전용 컴포넌트를 분리하여 관리할 수 있다.

@ComponentScan( //
    value = "com.eomcs.lms", //
    excludeFilters = {//
        @Filter(//
            type = FilterType.REGEX, //
            pattern = "com.eomcs.lms.web.*"), //
        @Filter(//
            type = FilterType.REGEX, //
            pattern = "com.eomcs.lms.admin.*")//
    })
public class AppConfig {

  static Logger logger = LogManager.getLogger(AppConfig.class);

  public AppConfig() {
    logger.debug("AppConfig 객체 생성!");
  }

        // FilterType.REGEX // 해당 pattern에 일치하는 부분을 @Filter한다고 생각하면 된다.

        // 단 excludeFilters이기 때문에, web 및, admin 및의 폴더는 제외된,

        // 나머지에서 component 애노테이션을 붙은 class를 적용한다 생각하면 된다.

@ComponentScan(value = "com.eomcs.lms.web")
@EnableWebMvc
public class AppWebConfig {

  static Logger logger = LogManager.getLogger(AppWebConfig.class);

  public AppWebConfig() {
    logger.debug("AppWebConfig 객체 생성!");
  }
@ComponentScan(value = "com.eomcs.lms.admin")
@EnableWebMvc
public class AdminWebConfig {

  static Logger logger = LogManager.getLogger(AdminWebConfig.class);

  public AdminWebConfig() {
    logger.debug("AdminWebConfig 객체 생성!");
  }

        // AppWebConfig, AdminWebConfig 생성 //

        // AppConfig에서 제외한 web 및 폴더와, admin 및 폴더에서 Component를 찾을 AppConfig 파일을 생성한다.

public class AppWebApplicationInitializer
    extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] {AppConfig.class};
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] {AppWebConfig.class};
  }

  @Override
  protected Filter[] getServletFilters() {
    return new Filter[] { //
        new CharacterEncodingFilter("UTF-8") //
    };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] {"/app/*"};
  }

  @Override
  protected String getServletName() {
    return "app";
  }
}
public class AdminWebApplicationInitializer
    extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] {AdminWebConfig.class};
  }

  @Override
  protected Filter[] getServletFilters() {
    return new Filter[] { //
        new CharacterEncodingFilter("UTF-8") //
    };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] {"/admin/*"};
  }

  @Override
  protected String getServletName() {
    return "admin";
  }
}

        // admin/AdminWebApplicationInitializer.java 추가, web/AppWebApplicationInitializer.java 변경 //

v60_1 -> 뷰 컴포넌트에 Tiles 기술 적용하기

        // Tiles를 JSP와 결합하여 뷰 컴포넌트를 구성할 수 있다.

        // Tiles 템플릿의 레이아웃을 구성할 수 있다.

        // mvnmvnrepository.com 또는 search.maven.org 접속 - 'tiles-jsp' 검색

        // org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사

        // - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'

        // web.AppWebConfig 변경 //

@ComponentScan(value = "com.eomcs.lms.web")
@EnableWebMvc
public class AppWebConfig {

  static Logger logger = LogManager.getLogger(AppWebConfig.class);

  public AppWebConfig() {
    logger.debug("AppWebConfig 객체 생성!");
  }

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver vr = new InternalResourceViewResolver(//
        "/WEB-INF/jsp/", // prefix
        ".jsp" // suffix
    );
    vr.setOrder(2);
    return vr;
  }

  @Bean
  public ViewResolver tilesViewResolver() {
    UrlBasedViewResolver vr = new UrlBasedViewResolver();

    // Tiles 설정에 때라 템플릿을 실행할 뷰 처리기를 등록한다.
    vr.setViewClass(TilesView.class);

    // 뷰리졸버의 우선 순위를 InternalResourceViewResolver 보다 우선하게 한다.
    vr.setOrder(1);
    return vr;
  }

  @Bean
  public TilesConfigurer tilesConfigurer() {
    TilesConfigurer configurer = new TilesConfigurer();
    configurer.setDefinitions("/WEB-INF/defs/tiles.xml");
    return configurer;
  }

  @Bean
  public MultipartResolver multipartResolver() {
    CommonsMultipartResolver mr = new CommonsMultipartResolver();
    mr.setMaxUploadSize(10000000);
    mr.setMaxInMemorySize(2000000);
    mr.setMaxUploadSizePerFile(5000000);
    return mr;
  }

        // 원래는 ViewResolver를 두가지를 사용하지는 않지만, setOrder를 설정하여 어떤 View를 선택하게 할 수 있다.

        // ViewResolver()를 vr.setOrder(1); // jsp 파일 경로가 실행된다.

        // TilesView // TilesView 템플릿 엔진을 추가한다.

        // TilesConfigurer // Tiles의 템플릿을 설정한다.

        // setDefinitions("경로") // Tiles의 템플릿을 정의해둔 파일을 알려준다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
  <!-- 여러 템플릿에서 공통으로 사용할 레이아웃을 정의한다. -->
  <definition name="base" template="/WEB-INF/tiles/template.jsp">
    <!-- template.jsp 안에서 사용할 JSP 파일의 이름을 설정한다. -->
    <put-attribute name="header" value="/WEB-INF/tiles/header.jsp" />
    <put-attribute name="footer" value="/WEB-INF/tiles/footer.jsp" />
  </definition>
  
  <!-- request handler가 리턴한 JSP의 경로가 'board/*' 일 경우
       TilesView 템플릿 엔진이 사용할 레이아웃을 정의한다.  -->
  <definition name="*/*" extends="base">
    <put-attribute name="body" value="/WEB-INF/views/{1}/{2}.jsp" />
  </definition>
  
</tiles-definitions>

        // WEB-INF/defs/tiles.xml 생성 // 템플릿의 레이아웃을 정의한다.

        // name은 기본적으로 request handler가 리턴한 값을 의미한다. 

        // 그러나 request handler가 리턴할 수 없는 값 // 보통 base, definition, default 등등 자유롭게 준다.

        // name으로 설정하게 되면, 이 파일은 tiles 적용을 안받는 파일이라는 것을 의미한다.

        // 주로, tiles 적용은 안받지만, 다른 파일에 적용할 수 있게 header, footer, left 등을 주로 넣는다.

        // 그래야 template 틀(template.jsp)에서 사용이 가능하기 때문이다. //

        // template="파일" // 다른 파일에 이 틀을 적용한다고 알려주는 것이다. // 바로 아래에 나옴

        // name="*/*" // 이 프로젝트에서는 request handler가 리턴하는 값이 board/list, lesson/list 등 이렇게 되기 때문에, 

        // *(와일드 카드) 순서에 따라 {1} , {2} 이런식으로 구분을 한다.

        // 즉, board/list 처럼 */* 이런 형식을 리턴하면, /WEB-INF/views/board/list.jsp를 "body"라는 이름으로 저장한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    trimDirectiveWhitespaces="true"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<c:if test="${not empty refreshUrl}">
<meta http-equiv="Refresh" content="${refreshUrl}">
</c:if>
<title>Bitcamp-LMS</title>
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
<style>
body {
  background-color: LightGray;
}
div.container {
  background: white;
  border: 1px solid gray;
  width: 600px;
}
footer {
  margin-top: 20px;
  padding: 10px;
  background-color: navy;
  color: white;
  text-align: center;
}
</style>
</head>
<body>

<tiles:insertAttribute name="header"/>

<div class='container'>
<tiles:insertAttribute name="body"/>
</div>

<tiles:insertAttribute name="footer"/>

</body>
</html>

        // /webapp/WEB-INF/tiles/template.jsp 생성 //

        // tiles.xml에서 저장한 header, body, footer가 들어갈 위치를 설정한다.

        // 단, css의 경우 head 태그 안에만 선언이 되어야 하기 때문에 css 적용하는데에는 여러 방법이 있다.

        // 1. 적용할 css를 한 파일에 선언하고, template.jsp 파일에 link하는 방식

        // 2. 만약 css를 사용하는 데마다 별도로 지정해야 한다면,

        // template.jsp에 request handler의 리턴값을 통해 css파일의 위치를 선언하여, 넣는 방법이 있다.

        // 그 외에도 여러가지 방법이 있다. // 그러나 body안에 css 태그를 넣을 수는 없다.

        // 또한 WEB-INF안에 jsp 파일 외에 다른 파일을 두면 인식을 하지 못하기 때문에 CSS파일과 JS파일을 별도로 둔다면,

        // WEB-INF 밖에 webapp 밑에 별도로 둬야 한다. // 당연히 경로를 지정해줘야 한다. // 경로는 절대경로도 되고, 상대경로도 된다.

  <definition name="*/*" extends="base">
    <put-attribute name="body" value="/WEB-INF/views/{1}/{2}.jsp" />
    <put-attribute name="css.page" value="../../css/{1}.css" />
    <put-attribute name="css.common" value="../../css/common.css" />
  </definition>

        // tiles.xml 파일에 이렇게 css를 설정한다. // common.css는 모든 페이지가 공통으로 적용 받을 값이기 때문에 고정 값으로 넣는다.

        // css.page는 page별로 다르게 css를 적용해야 하는 경우에 저런 형식으로 불러온다는 의미이다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    trimDirectiveWhitespaces="true"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<c:if test="${not empty refreshUrl}">
<meta http-equiv="Refresh" content="${refreshUrl}">
</c:if>
<title>Bitcamp-LMS</title>
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
<link rel='stylesheet' href='<tiles:getAsString name="css.common"/>'>
<link rel='stylesheet' href='<tiles:getAsString name="css.page"/>'>
</head>
<body>

<tiles:insertAttribute name="header"/>

<div class='container'>
<tiles:insertAttribute name="body"/>
</div>

<tiles:insertAttribute name="footer"/>

</body>
</html>

        // template.jsp // 

        // 공통적으로 적용되는 css.common을 css.page보다 위에 두어야 한다. // 그래야 css.page가 나중에 덮어쓸 수 있기 때문이다.

        // tilles를 사용하려면  /webapp/WEB-INF/jsp 폴더를 복사하여 /WEB-INF/views 이름에 옮긴다. // 경로를 그렇게 설정했기 때문

        // 템플릿 사용에 맞춰서 JSP 파일을 편집한다. // include 코드를 삭제한다. // tiles에서 넣어주기 때문

 

$ scoop install nodejs // scoop으로 설치 후 gitbash를 껏다가 켜야 한다.

// $ npm -v // 설치 여부를 확인할 수 있다. 버전이 뜨면 다운로드가 된 것이다.

// $ npm init // src/main/webapp에 들어가서 커맨드 입력해야 한다. // 다 엔터치면 된다.

// init가 끝났다면 webapp안에 package.json이 생긴다.

// $ npm install bootstrap --save //

"dependencies": {
    "bootstrap": "^4.5.0"
  }

// package json에 이렇게 bootstrap이 추가 된 것을 알 수 있다.

<link rel='stylesheet' href='${pageContext.getServletContext().getContextPath()}/node_modules/bootstrap/dist/css/bootstrap.min.css'>
<link rel='stylesheet' href='<tiles:getAsString name="css.common"/>'>
<link rel='stylesheet' href='<tiles:getAsString name="css.page"/>'>

// 이렇게 경로를 지정해 준다.

<script src='${pageContext.getServletContext().getContextPath()}/node_modules/jquery/dist/jquery.min.js'></script>
<script src='${pageContext.getServletContext().getContextPath()}/node_modules//@popperjs/core/dist/umd/popper.min.js'></script>
<script src='${pageContext.getServletContext().getContextPath()}/node_modules/bootstrap/dist/js/bootstrap.min.js'></script>
<script src='${pageContext.getServletContext().getContextPath()}/node_modules/swweetalert/dist/sweetalert.

// 만약 git에 올리게 되면 package.json만 올라가게 된다. // 다른 팀원이 받은 외부 라이브러리를 사용하려면,

// webapp 경로에까지 들어가서 $ npm install 입력하면 package.json에 정의 된 대로 다운이 받아진다.

 

반응형

+ Recent posts