`HTML 태그를 반복해서 출력하고 있다.
+
+
+### 서블릿과 JSP의 한계
+서블릿으로 개발할 때는 뷰(view)화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여 복잡했다.
+JSP를 사용한 덕분에 뷰를 생성하는 HTML 작업을 깔끔하게 정리하고, 중간중간 동적으로 변경이 필요한 부분에만
+자바 코드를 적용했다. 하지만 몇 가지 문제점이 존재한다.
+
+회원 저장 폼에서 코드의 상위 절반은 회원을 저장하는 비지니스 로직, 나머지 절반은 결과를 보여주는 HTML 뷰 영역이다. 회원 목록도 마찬가지이다.
+JAVA코드, 데이터를 조회하는 repository 등등 다양한 코드가 모두 JSP에 노출돼 있다. 만약 수백, 수천줄이 넘어간다면 유지보수에 큰 어려움이 발생한다.
+
+
+**MVC 패턴 등장**
+ 비지니스 로직은 서블릿 처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(VIEW)을 보여주는 일에 집중하도록 하게 해준다.
+
+
+
+
+## MVC 패턴 -적용
+서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해 MVC 패턴 적용
+ Model은 HttpServletRequest 객체 사용. request는 내부에 데이터 저장소를 가지고 있는데,
+'request.setAttribute()', `request.getAttribute`를 사용하면 데이터를 보관하고, 조회할 수 있다.
+
+
+### 회원 등록
+
+*MvcMemberFormServlet - 회원 등록 폼 컨트롤러*
+```java
+package hello.servlet.web.servletmvc;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
+public class MvcMemberFormServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String viewPath = "/WEB-INF/views/new-form.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+
+ }
+}
+```
+
+`dispatcher.forward()` : 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 다시 호출 발생
+
+> `/WEB-INF`
+> 이 경로 안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 즉 항상 컨트롤러를 통해서 JSP를 호출하는 것이다.
+
+> **redirect vs forward**
+> 리다이렉트는 실제 클라이언트(web)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다.
+> 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서
+> 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.
+
+
+*new-form.jsp - 회원 등록 폼 뷰*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+
+ Title
+
+
+
+
+
+
+```
+
+여기서 form의 action이 상대경로로 지정돼 있다. 이렇게 상대경로로 지정하면 폼 전송시
+현재 URL이 속한 계층 경로 + save가 호출된다.
+현재 계층 경로 : `/servlet-mvc/members/`
+결과 : `/servlet-mvc/members/save`
+
+
+### 회원 저장
+
+*MvcMemberSaveServlet - 회원 저장 컨트롤러*
+```java
+package hello.servlet.web.servletmvc;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
+public class MvcMemberSaveServlet extends HttpServlet {
+
+ MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+
+ //Model에 데이터 보관.
+ request.setAttribute("member", member);
+
+ String viewPath = "/WEB-INF/views/save-result.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+HttpServletRequest를 Model로 사용.
+request가 제공하는 setAttribute()를 사용하면 request 객체에 데이터를 보관해 뷰에 전달할 수 있다.
+뷰는 request.getAttribute()를 사용해 데이터를 꺼내면 된다.
+
+
+*save-result - 회원저장 뷰*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+
+
+
+성공
+
+ id=${member.id}
+ username=${member.username}
+ age=${member.age}
+
+메인
+
+
+```
+
+JSP는 `${}` 문법을 제공. 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다.
+
+
+### 회원 목록 조회
+
+*MvcMemberListServlet - 회원 목록 조회 컨트롤러*
+```java
+package hello.servlet.web.servletmvc;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
+public class MvcMemberListServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+
+ request.setAttribute("members", members);
+
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+
+request 객체를 사용해 `List members`를 모델에 보관한다.
+
+
+*members - 회원 목록 조회 뷰*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+
+
+
+ Title
+
+
+메인
+
+
+ id
+ username
+ age
+
+
+
+
+ ${item.id}
+ ${item.username}
+ ${item.age}
+
+
+
+
+
+
+```
+모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해 반복해서 출력한다.
+`memebers`리스트에서 `members`를 순서대로 꺼내 `item`변수에 담고, 출력하는 과정을 반복한다.
+
+``이 기능을 사용하려면 다음과 같이 선언해야 한다.
+`<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>`
+
+
+
+## MVC 패턴 - 한계
+MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다.
+뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다. 단순하게 모델에서 필요한 데이터를 수집하고, 화면을 만들면 된다.
+
+하지만 컨트롤러는 중복코드가 많고, 필요하지 않은 코드가 많이 존재한다.
+
+
+**MVC 컨트롤러의 단점**
+
+*foward 중복*
+view로 이동하는 코드가 항상 중복 호출된다.
+```
+RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+dispatcher.forward(request, response);
+```
+
+
+**ViewPath 중복**
+```java
+String viewPath = "/WEB-INF/views/save-result.jsp";
+```
+
+- prefix : `/WEB-INF/views/`
+- suffix : `.jsp`
+그리고 만약 jsp가 아닌 thymeleaf같은 뷰로 변경한다면 전체 코드를 다 변경 작업을 해야 한다.
+
+
+**사용하지 않는 코드**
+다음 코드를 사용할 때도 있고, 사용하지 않을 때도 있다.
+```
+HttpServletRequest request, HttpServletResponse response
+```
+그리고 이런 `HttpServletRequest`, `HttpServletResponse`를 사용하는 코드는 테스트 케이스를 작성하기 어렵다.
+
+
+**공통 처리 어려움**
+기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가한다. 단순히 공통 기능을 메서드로
+생성하면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 될 수도 있다. 그리고 호출하는 것 자체도 중복이다.
+
+
+**정리!**
+이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다. 소위 **수문장 역할**을 하는 기능이 필요하다.
+**Front Controller**패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.(입구를 하나로)
+스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8 WEEK/servlet/SECTION4.md b/8 WEEK/servlet/SECTION4.md
new file mode 100644
index 00000000..f27d814e
--- /dev/null
+++ b/8 WEEK/servlet/SECTION4.md
@@ -0,0 +1,1251 @@
+# 4. MVC 프레임워크 만들기
+
+---
+
+## 프론트 컨트롤러 패턴
+
+**프론트 컨트롤러 도입 전**
+
+
+**프론트 컨트롤러 도입 후**
+
+
+
+**FrontController 패턴 특징**
+- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
+- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아 호출
+- 입구를 하나로
+- 공통 처리 가능
+- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
+
+
+**스프링 웹MVC와 프론트 컨트롤러**
+- 스프링 웹 MVC의 핵심도 바로 **FrontController**
+- 스프링 웹 MVC의 **DispatcherServlet**이 FrontController 패턴으로 구현되어 있다.
+
+
+
+## 프론트 컨트롤러 도입 - v1
+
+V1 구조
+
+
+
+*ControllerV1*
+```java
+package hello.servlet.web.frontcontroller.v1;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface ControllerV1 {
+ void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
+}
+```
+
+서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입.
+각 컨트롤러들은 이 인터페이스를 구현
+프론트 컨트롤러는 이 인터페이스를 호출해 구현과 관계 없이 로직의 일관성을 가질 수 있음.
+
+
+*MemberFormControllerV1 - 회원 등록 컨트롤러*
+```java
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberFormControllerV1 implements ControllerV1 {
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String viewPath = "/WEB-INF/views/new-form.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+
+*MemberListControllerV1 - 회원 목록 컨트롤러*
+```java
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MemberListControllerV1 implements ControllerV1 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+
+ request.setAttribute("members", members);
+
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+내부 로직은 기존 서블릿과 비슷
+
+
+*FrontControllerServletV1 - 프론트 컨트롤러*
+```java
+package hello.servlet.web.frontcontroller.v1;
+
+import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
+import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
+import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
+public class FrontControllerServiceV1 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV1() {
+ controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
+ controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
+ controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV1 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ controller.process(request, response);
+ }
+}
+```
+
+**프론트 컨트롤러 분석**
+
+**urlPatterns**
+- `urlPatterns = "/front-controller/v1/*"` : `/front-controller/v1/*`를 포함한 하위 모든 요청은
+이 서블릿에서 받아들인다.
+- Ex) `/front-controller/v1`, `/front-cotroller/v1/a`, `/front-controller/v1/a/b`
+
+
+**controllerMap**
+- key : 매핑 URL
+- value : 호출된 컨트롤러
+
+
+**service()**
+먼저 `requestURI`를 조회해 실제 호출할 컨트롤러를 controllerMap에서 찾는다. 없다면 404 상태 코드 반환한다.
+ 컨트롤러를 찾고 `controller.process(request, response);`을 호출해서 해당 컨트롤러를 실행한다.
+
+
+**JSP**
+JSP는 이전 MVC에서 사용했던 파일을 그대로 사용
+
+
+
+**기존 서블릿, JSP로 만든 MVC와 동일하게 실행된다.**
+
+
+
+
+
+## View 분리 - v2
+모든 컨트롤러에서 뷰로 이동한느 부분에 중복 발생, 깔끔하지 않음
+```
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+```
+
+이를 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만든다.
+
+*V2 구조*
+
+
+이전 V1 구조에서는 controller에서 jsp를 호출했다면 V2는 view를 반환해 FrontController에서 실행하게 한다.
+
+
+*MyView*
+```java
+package hello.servlet.web.frontcontroller;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MyView {
+ private String viewPath;
+
+ public MyView(String viewPath) {
+ this.viewPath = viewPath;
+ }
+
+ public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+
+*ControllerV2*
+```java
+package hello.servlet.web.frontcontroller.v2;
+
+import hello.servlet.web.frontcontroller.MyView;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface ControllerV2 {
+ MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
+}
+```
+
+
+*MemberFormControllerV2 - 회원 등록 폼*
+```java
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberFormControllerV2 implements ControllerV2 {
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ return new MyView("/WEB-INF/views/new-form.jsp");
+ }
+}
+
+```
+
+각 컨트롤러는 복잡한 dispatcher.forward()를 직접 생성해 호출하지 않아도 됨.
+단순히 MyView 객체를 생성, 거기에 뷰 이름만 넣고 반환하면 됨.
+
+ControllerV1을 구현한 클래스와 ControllerV2를 구현한 클래스를 비교하면, 중복이 확실하게 제거됨을 확인 가능함
+
+*MemberSaveControllerV2 - 회원 저장*
+```java
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberSaveControllerV2 implements ControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ request.setAttribute("member", member);
+ return new MyView("/WEB-INF/views/save-result.jsp");
+ }
+}
+```
+
+
+*MemberListControllerV2 - 회원 목록*
+```java
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MemberListControllerV2 implements ControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+ request.setAttribute("members", members);
+ return new MyView("/WEB-INF/views/members.jsp");
+ }
+}
+```
+
+*FrontControllerV2*
+```java
+package hello.servlet.web.frontcontroller.v2;
+
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
+public class FrontControllerServiceV2 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV2() {
+ controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
+ controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
+ controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV2 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ MyView view = controller.process(request, response);
+ view.render(request, response);
+ }
+}
+```
+
+ControllerV2의 반환 타입이 `MyView`이므로 프론트 컨트롤러는 컨트롤러의 호출 결과를 `MyView`를 반환 받는다.
+그리고 `view.render()`를 호출하면 `forward` 로직을 수행해 JSP가 실행된다.
+
+`MyView.render()`
+```java
+ public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+```
+
+
+
+
+## Model 추가 - v3
+**서블릿 종속 제거**
+컨트롤러 입장에서 HttpServletRequest, HttpServletResponse가 꼭 필요할까?
+요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서 컨트롤러가 서블릿 기술을 몰라도 동작 할 수 있다.
+또한 request 객체를 Model로 사용하는 대신 별도의 Model 객체를 만들어 반환하면 된다.
+구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않고 변경 ->>
+
+**뷰 이름 종속 제거**
+컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다.
+컨트롤러의 *뷰의 논리 이름*을 반환, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화.
+이렇게 하면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다.
+
+- `/WEB-INF/views/new-form.jsp` -> **new-form**
+- `/WEB-INF/views/save-result.jsp` -> **save-result**
+- `/WEB-INF/views/members.jsp` -> **members**
+
+
+**V3 구조**
+
+
+
+**Model View**
+지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다. 그리고 Model도 `request.setAttribute()`를 통해 데이터를 저장, 뷰에 전달했다.
+ 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View이름가지 전달하는 객체를 만들어본다.
+ 참고로 `ModelView`객체는 다른 버전에서도 사용하므로 패키지를 `frontController`에 둔다.
+
+
+*ModelView*
+```java
+package hello.servlet.web.frontcontroller;
+
+import java.util.HashMap;
+import java.util.Map;
+public class ModelView {
+ private String viewName;
+ private Map model = new HashMap<>();
+ public ModelView(String viewName) {
+ this.viewName = viewName;
+ }
+ public String getViewName() {
+ return viewName;
+ }
+ public void setViewName(String viewName) {
+ this.viewName = viewName;
+ }
+ public Map getModel() {
+ return model;
+ }
+ public void setModel(Map model) {
+ this.model = model;
+ }
+}
+```
+뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있음. model은 단순히 map으로 되어 있어 컨트롤러에서 뷰에 필요한 데이터를
+key, value로 넣어주면 됨.
+
+
+*ControllerV3*
+```java
+package hello.servlet.web.frontcontroller.v3;
+
+import hello.servlet.web.frontcontroller.ModelView;
+
+import java.util.Map;
+
+public interface ControllerV3 {
+ ModelView process(Map paramMap);
+}
+```
+이 컨트롤러는 서블릿 기술을 사용하지 않음. 따라서 구현이 단순해지고, 테스트 코드를 작성하기 쉬움
+HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아 호출하면 됨
+응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 됨.
+
+
+*MemberFormControllerV3 - 회원 등록 폼*
+```java
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.Map;
+
+public class MemberFormControllerV3 implements ControllerV3 {
+ @Override
+ public ModelView process(Map paramMap) {
+ return new ModelView("new-form");
+ }
+}
+```
+`ModelView`를 생성할 때 `new-form`이라는 view의 논리적인 이름을 지정. 실제 물리적은 이름은 프론트 컨트롤러에서 처리한다.
+
+
+*MemberSaveControllerV3 - 회원 저장*
+```java
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.Map;
+
+public class MemberSaveControllerV3 implements ControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public ModelView process(Map paramMap) {
+ String username = paramMap.get("username");
+ int age = Integer.parseInt(paramMap.get("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ ModelView mv = new ModelView("save-result");
+ mv.getModel().put("member", member);
+ return mv;
+ }
+}
+```
+
+- `paramMap.get("username");`
+ - 파라미터 정보는 map에 담겨있음. map에서 필요한 요청 파라미터를 조회하면 됨.
+- `mv.getModel().put("member", member);`
+ - 모델은 단순한 map이므로 모델에 뷰에서 필요한 `member`객체를 담고 반환
+
+
+*MemberListControllerV3 - 회원 목록*
+```java
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.List;
+import java.util.Map;
+
+public class MemberListControllerV3 implements ControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public ModelView process(Map paramMap) {
+ List members = memberRepository.findAll();
+ ModelView mv = new ModelView("members");
+ mv.getModel().put("members", members);
+
+ return mv;
+
+ }
+
+}
+```
+
+*FrontControllerServletV3*
+```java
+package hello.servlet.web.frontcontroller.v3;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
+public class FrontControllerServiceV3 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV3() {
+ controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
+ controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
+ controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV3 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ String viewName = mv.getViewName();
+ MyView view = viewResolver(viewName);
+ view.render(mv.getModel(), request, response);
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
+```
+
+
+`view.render(mv.getModel(), request, response)`코드에서 컴파일 오류가 발생함. 다음 코드를 참고해 MyView 객체에 필요한 메서드 추가.
+
+`createParamMap()`
+HttpServletRequest에서 파라미터 정보를 꺼내 Map으로 변환. 그리고 해당 Map(`paramMap`)을 컨트롤러에 전달하면서 호출
+
+
+**View Resolver**
+`MyView view = viewResolver(viewName)`
+컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리 경로가 있는 MyView 객체를 반환
+한다.
+- 논리 뷰 이름 : `members`
+- 물리 뷰 경로 : `/WEB-INF/views/members.jsp`
+
+
+
+`view.render(mv.getModel(), request, response)`
+- 뷰 객체를 통해서 HTML 화면을 렌더링 한다.
+- 뷰 객체의 render() 는 모델 정보도 함께 받는다.
+- JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둔다.
+- JSP로 포워드 해서 JSP를 렌더링 한다.
+
+
+*MyView*
+```java
+package hello.servlet.web.frontcontroller;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class MyView {
+ private String viewPath;
+
+ public MyView(String viewPath) {
+ this.viewPath = viewPath;
+ }
+ public void render(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+ public void render(Map model, HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ modelToRequestAttribute(model, request);
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+ private void modelToRequestAttribute(Map model,
+ HttpServletRequest request) {
+ model.forEach((key, value) -> request.setAttribute(key, value));
+ }
+}
+```
+
+
+
+## 단순하고 실용적인 컨트롤러 - v4
+앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다.
+하지만 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 항상 ModelView 객체를 생성하고 반환해야하는 번거러움이 존재한다.
+ 좋은 프레임 워크는 아키텍쳐도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 즉 실용성이 있어야 한다.
+
+
+v4는 v3를 약간 변경해 실제 구현하는 개발자들이 매우 편리하게 개발할 수 있도록 한다.
+
+
+**V4 구조**
+
+- 기본적인 구조는 V3와 같다. 대신에 컨트롤러가 ModelView를 반환하지 않고, ViewName만 반환한다.
+
+
+*ControllerV4*
+```java
+package hello.servlet.web.frontcontroller.v4;
+
+import java.util.Map;
+
+public interface ControllerV4 {
+ String process(Map paramMap, Map model);
+}
+```
+이번 V4는 interface에 ModelView가 없다. model 객체는 파라미터로 전달되기 때문에 그냥 사용하면 되고, 결과로 뷰의 이름만 반환하면 된다.
+
+
+*MemberFormControllerV4*
+```java
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.Map;
+
+public class MemberFormControllerV4 implements ControllerV4 {
+ @Override
+ public String process(Map paramMap, Map model) {
+ return "new-form";
+ }
+}
+```
+
+
+*MemberSaveControllerV4*
+```java
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.Map;
+
+public class MemberSaveController implements ControllerV4 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public String process(Map paramMap, Map model) {
+ String username = paramMap.get("username");
+ int age = Integer.parseInt(paramMap.get("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ model.put("member", member);
+
+ return "save-result";
+ }
+}
+```
+`model.put("member", member)` : 모델이 파라미터로 전달되기 때문에 모델을 직접 생성하지 않아도 된다.
+
+
+
+*MemberListControllerV4*
+```java
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.List;
+import java.util.Map;
+
+public class MemberListControllerV4 implements ControllerV4 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public String process(Map paramMap, Map model) {
+ List members = memberRepository.findAll();
+
+ model.put("members", members);
+ return "members";
+ }
+}
+```
+
+
+*FrontControllerServletV4*
+```java
+package hello.servlet.web.frontcontroller.v4;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
+public class FrontControllerServiceV4 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV4() {
+ controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
+ controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
+ controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV4 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+
+ String viewName = controller.process(paramMap, model);
+
+ MyView view = viewResolver(viewName);
+ view.render(model, request, response);
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
+```
+`FrontControllerServletV4`는 이전 버전과 거의 동일
+
+
+**모델 객체 전달**
+`Map model = new HashMap<>();`
+모델 객체를 프론트 컨트롤러에서 생성해 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.
+
+
+**뷰의 논리 이름을 직접 반환**
+```java
+String viewName = controller.process(paramMap, model);
+MyView view = viewResolver(viewName);
+```
+컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해 실제 물리 뷰를 찾을 수 있다.
+
+**정리**
+V4는 매우 단순하고 실용적이다. 기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어를 적용했을 뿐인데, 컨트롤러를 구현하는
+개발자 입장에서 군더더기 없는 코드를 작성할 수 있다.
+
+
+
+
+
+## 유연한 컨트롤러1 - V5
+만약 `ControllerV3` or `ControllerV4` 방식으로 다양한 컨트롤러를 사용해 개발하고 싶다면 **어댑터 패턴을**
+을 사용해야 한다.
+
+**어댑터 패턴**
+지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있었다.
+`ControllerV3`, `ControllerV4`는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다. 마치 v3는 110v, v4는 220v 전기 콘셉트 같은 것이다.
+ 어댑터 패턴을 사용해 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경한다.
+
+**V5 구조**
+
+- **핸들러 어댑터** : 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름은 어댑터 핸들러이다.
+여기서 어댑터 역할을 해주기 때문에 다양한 종료의 컨트롤러를 호출할 수 있다.
+- **핸들러** : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어뎁터가 있기 때문이다.
+꼭 컨트롤러의 개념 뿐 아니라 어떠한 것이든 해당하는 종료의 어댑터만 있으면 다 처리할 수 있기 때문이다.
+
+
+*MyHandlerAdapter* 인터페이스
+```java
+package hello.servlet.web.frontcontroller.v5;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface MyHandlerAdapter {
+ boolean supports(Object handler);
+
+ ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
+}
+```
+- `boolean supports(Object handler)`
+ - handler는 컨트롤러를 말함
+ - 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 Method.
+- `ModelView handle(HttpServletRequest request, HttpServletResponse response, Object Handler)`
+ - 어댑터는 실제 컨트롤러를 호출, 그 결과로 ModelView를 반환해야 함.
+ - 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 함.
+ - 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해 실제 컨트롤러가 호출됨.
+
+
+실제 ControllerV3를 지원하는 어댑터를 구현
+*ControllerV3HandlerAdapter*
+```java
+package hello.servlet.web.frontcontroller.v5.adapter;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
+ @Override
+ public boolean supports(Object handler) {
+ return (handler instanceof ControllerV3);
+ }
+ @Override
+ public ModelView handle(HttpServletRequest request, HttpServletResponse
+ response, Object handler) {
+ ControllerV3 controller = (ControllerV3) handler;
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ return mv;
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+}
+```
+
+
+
+```java
+public boolean supports(Object handler) {
+ return (handler instanceof ControllerV3);
+}
+```
+`ControllerV3`를 처리할 수 있는 어댑터.
+
+
+```java
+public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+ ControllerV3 controller = (ControllerV3) handler;
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ return mv;
+}
+```
+handler를 컨트롤러 V3로 변환한 다음 V3 형식에 맞추도록 호출.
+`support()`를 통해 `ControllerV3`만 지원하기 때문에 타입 변환은 걱정없이 실행해도 됨.
+ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 됨.
+
+
+
+*FrontControllerServletV5*
+```java
+package hello.servlet.web.frontcontroller.v5;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/ v5/*")
+public class FrontControllerServletV5 extends HttpServlet {
+ private final Map handlerMappingMap = new HashMap<>();
+ private final List handlerAdapters = new ArrayList<>();
+ public FrontControllerServletV5() {
+ initHandlerMappingMap();
+ initHandlerAdapters();
+ }
+ private void initHandlerMappingMap() {
+ handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
+ }
+ private void initHandlerAdapters() {
+ handlerAdapters.add(new ControllerV3HandlerAdapter());
+ }
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ Object handler = getHandler(request);
+ if (handler == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ MyHandlerAdapter adapter = getHandlerAdapter(handler);
+ ModelView mv = adapter.handle(request, response, handler);
+ MyView view = viewResolver(mv.getViewName());
+ view.render(mv.getModel(), request, response);
+ }
+ private Object getHandler(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ return handlerMappingMap.get(requestURI);
+ }
+ private MyHandlerAdapter getHandlerAdapter(Object handler) {
+ for (MyHandlerAdapter adapter : handlerAdapters) {
+ if (adapter.supports(handler)) {
+ return adapter;
+ }
+ }
+ throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
+```
+
+**컨트롤러(Controller) -> 핸들러(Handler)**
+이전에는 컨트롤러를 직접 매핑해서 사용했다. 이젠느 어댑터를 사용하기 때문에, 컨트롤러 뿐 아니라 어댑터가 지원하기만 하면,
+어떤 것이라도 URL에 매핑해 사용할 수 있다. 그래서 이름을 컨트롤러에서 더 넓은 범위의 핸들러로 변경했다.
+
+
+**생성자**
+```java
+ public FrontControllerServletV5() {
+ initHandlerMappingMap();
+ initHandlerAdapters();
+ }
+```
+생성자는 핸들러 매핑과 어댑터를 초기화(등록)함
+
+
+**매핑 정보**
+`private final Map handlerMappingMap = new HashMap<>();`
+
+매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변
+경되었다
+
+
+**핸들러 매핑**
+`Object handler = getHandler(request);`
+```java
+ private Object getHandler(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ return handlerMappingMap.get(requestURI);
+ }
+```
+핸들러 매핑 정보인 `handlerMappingMap`에서 URL에 매핑된 핸들러(컨트롤러)객체를 찾아 반환
+
+
+
+**핸들러를 처리할 수 있는 어댑터 조회**
+`MyHandlerAdapter adapter = getHandlerAdapter(handler)`
+```
+for (MyHandlerAdapter adapter : handlerAdapters) {
+ if (adapter.supports(handler)) {
+ return adapter;
+ }
+}
+```
+
+`handler`를 처리활 수 있는 어댑터를 `adapter.supports(handler)`를 통해 찾음.
+handler가 `ControllerV3` 인터페이스를 구현했다면, `ControllerV3HandlerAdapter()`객체가 반환됨.
+
+
+**어댑터 호출**
+`ModelView mv = adapter.handler(request, response, handler);`
+
+어댑터의 `handler(request, response, handler)`메서드를 통해 실제 어댑터가 호출됨.
+어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환.
+`ControllerV3HandlerAdapter`의 경우 어댑터의 모양과 컨트롤러의 모양이 유사해 변환 로직이 단순함
+
+
+
+## 유연한 컨트롤러2 - v5
+
+*FrontControllerServletV5* - `Controller4` 기능 추가
+```java
+package hello.servlet.web.frontcontroller.v5;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
+import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
+import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
+public class FrontControllerServletV5 extends HttpServlet {
+ private final Map handlerMappingMap = new HashMap<>();
+ private final List handlerAdapters = new ArrayList<>();
+ public FrontControllerServletV5() {
+ initHandlerMappingMap();
+ initHandlerAdapters();
+ }
+ private void initHandlerMappingMap() {
+ handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
+
+ handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
+ handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
+ handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
+ }
+
+ private void initHandlerAdapters() {
+ handlerAdapters.add(new ControllerV3HandlerAdapter());
+ handlerAdapters.add(new ControllerV4HandlerAdapter());
+ }
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ Object handler = getHandler(request);
+ if (handler == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ MyHandlerAdapter adapter = getHandlerAdapter(handler);
+ ModelView mv = adapter.handle(request, response, handler);
+ MyView view = viewResolver(mv.getViewName());
+ view.render(mv.getModel(), request, response);
+ }
+ private Object getHandler(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ return handlerMappingMap.get(requestURI);
+ }
+ private MyHandlerAdapter getHandlerAdapter(Object handler) {
+ for (MyHandlerAdapter adapter : handlerAdapters) {
+ if (adapter.supports(handler)) {
+ return adapter;
+ }
+ }
+ throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
+```
+
+`handlerMappingMap`에 `ControllerV4`를 사용하는 컨트롤러를 추가, 해당 컨트롤러를 처리할 수 있는 어댑터인 `ControllerV4HandlerAdapter`도 추가
+
+
+*ControllerV4HandlerAdapter*
+```java
+package hello.servlet.web.frontcontroller.v5.adapter;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
+ @Override
+ public boolean supports(Object handler) {
+ return (handler instanceof ControllerV4);
+ }
+
+ @Override
+ public ModelView handle(HttpServletRequest request, HttpServletResponse
+ response, Object handler) {
+ ControllerV4 controller = (ControllerV4) handler;
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+ String viewName = controller.process(paramMap, model);
+
+ ModelView mv = new ModelView(viewName);
+ mv.setModel(model);
+
+ return mv;
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+}
+```
+
+분석
+```java
+ public boolean supports(Object handler) {
+ return (handler instanceof ControllerV4);
+ }
+```
+`handler`가 `ControllerV4`인 경우에만 처리하는 어댑터.
+
+실행로직
+```java
+ ControllerV4 controller = (ControllerV4) handler;
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+ String viewName = controller.process(paramMap, model);
+```
+handler를 ControllerV4로 케스팅, paramMap, model을 만들어 해당 컨트롤러를 호출한다.
+그리고 viewName을 반환 받는다.
+
+
+**어댑터 반환**
+```
+ ModelView mv = new ModelView(viewName);
+ mv.setModel(model);
+
+ return mv;
+```
+어댑터에서 이 부분이 중요한 부분임
+
+어댑터가 호출하는 `ControllerV4`는 뷰의 이름알 반환. 그런데 어댑터는 뷰의 이름이 아니라 `ModelView`를 만들어 반환해야함
+여기서 어댑터가 꼭 필요한 이유가 나옴.
+`ControllerV4`는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞춰 반환함.
+마치 110v 콘센트를 220v 콘센트로 변경하듯.
+
+
+**ControllerV4 & Adapter**
+```java
+public interface ControllerV4 {
+ String process(Map paramMap, Map model);
+}
+public interface MyHandlerAdapter {
+ ModelView handle(HttpServletRequest request, HttpServletResponse response,
+Object handler) throws ServletException, IOException;
+}
+```
+
+
+
+
+---
\ No newline at end of file
diff --git a/8 WEEK/servlet/build.gradle b/8 WEEK/servlet/build.gradle
new file mode 100644
index 00000000..de959ed2
--- /dev/null
+++ b/8 WEEK/servlet/build.gradle
@@ -0,0 +1,43 @@
+plugins {
+ id 'java'
+ id 'war'
+ id 'org.springframework.boot' version '3.2.1'
+ id 'io.spring.dependency-management' version '1.1.4'
+}
+
+group = 'hello'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ sourceCompatibility = '17'
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+
+ //JSP 추가 시작
+ implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
+ implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
+ implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트3.0 이상
+ implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
+ //JSP 추가 끝
+
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar b/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..d64cd491
Binary files /dev/null and b/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties b/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..1af9e093
--- /dev/null
+++ b/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/8 WEEK/servlet/gradlew b/8 WEEK/servlet/gradlew
new file mode 100644
index 00000000..1aa94a42
--- /dev/null
+++ b/8 WEEK/servlet/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/8 WEEK/servlet/gradlew.bat b/8 WEEK/servlet/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/8 WEEK/servlet/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/8 WEEK/servlet/img/S1-1.png b/8 WEEK/servlet/img/S1-1.png
new file mode 100644
index 00000000..b4382b9b
Binary files /dev/null and b/8 WEEK/servlet/img/S1-1.png differ
diff --git a/8 WEEK/servlet/img/S1-2.png b/8 WEEK/servlet/img/S1-2.png
new file mode 100644
index 00000000..ff31b6a6
Binary files /dev/null and b/8 WEEK/servlet/img/S1-2.png differ
diff --git a/8 WEEK/servlet/img/S1-3.png b/8 WEEK/servlet/img/S1-3.png
new file mode 100644
index 00000000..d0a945c4
Binary files /dev/null and b/8 WEEK/servlet/img/S1-3.png differ
diff --git a/8 WEEK/servlet/img/S1-4.png b/8 WEEK/servlet/img/S1-4.png
new file mode 100644
index 00000000..b2538fe4
Binary files /dev/null and b/8 WEEK/servlet/img/S1-4.png differ
diff --git a/8 WEEK/servlet/img/S1-5.png b/8 WEEK/servlet/img/S1-5.png
new file mode 100644
index 00000000..496dd994
Binary files /dev/null and b/8 WEEK/servlet/img/S1-5.png differ
diff --git a/8 WEEK/servlet/img/S1-6.png b/8 WEEK/servlet/img/S1-6.png
new file mode 100644
index 00000000..0f147271
Binary files /dev/null and b/8 WEEK/servlet/img/S1-6.png differ
diff --git a/8 WEEK/servlet/img/S1-7.png b/8 WEEK/servlet/img/S1-7.png
new file mode 100644
index 00000000..cc6ffda9
Binary files /dev/null and b/8 WEEK/servlet/img/S1-7.png differ
diff --git a/8 WEEK/servlet/img/S1-8.png b/8 WEEK/servlet/img/S1-8.png
new file mode 100644
index 00000000..dafb1dc7
Binary files /dev/null and b/8 WEEK/servlet/img/S1-8.png differ
diff --git a/8 WEEK/servlet/img/S2-1.png b/8 WEEK/servlet/img/S2-1.png
new file mode 100644
index 00000000..2f468be2
Binary files /dev/null and b/8 WEEK/servlet/img/S2-1.png differ
diff --git a/8 WEEK/servlet/img/S2-2.png b/8 WEEK/servlet/img/S2-2.png
new file mode 100644
index 00000000..c2d94f77
Binary files /dev/null and b/8 WEEK/servlet/img/S2-2.png differ
diff --git a/8 WEEK/servlet/img/S2-3.png b/8 WEEK/servlet/img/S2-3.png
new file mode 100644
index 00000000..15d29abf
Binary files /dev/null and b/8 WEEK/servlet/img/S2-3.png differ
diff --git a/8 WEEK/servlet/img/S2-4.png b/8 WEEK/servlet/img/S2-4.png
new file mode 100644
index 00000000..a6ce3e89
Binary files /dev/null and b/8 WEEK/servlet/img/S2-4.png differ
diff --git a/8 WEEK/servlet/img/S2-5.png b/8 WEEK/servlet/img/S2-5.png
new file mode 100644
index 00000000..5c762ffa
Binary files /dev/null and b/8 WEEK/servlet/img/S2-5.png differ
diff --git a/8 WEEK/servlet/img/S2-6.png b/8 WEEK/servlet/img/S2-6.png
new file mode 100644
index 00000000..c5924ca5
Binary files /dev/null and b/8 WEEK/servlet/img/S2-6.png differ
diff --git a/8 WEEK/servlet/img/S2-7.png b/8 WEEK/servlet/img/S2-7.png
new file mode 100644
index 00000000..3948838e
Binary files /dev/null and b/8 WEEK/servlet/img/S2-7.png differ
diff --git a/8 WEEK/servlet/img/S2-8.png b/8 WEEK/servlet/img/S2-8.png
new file mode 100644
index 00000000..76f40bbd
Binary files /dev/null and b/8 WEEK/servlet/img/S2-8.png differ
diff --git a/8 WEEK/servlet/img/S4-1.png b/8 WEEK/servlet/img/S4-1.png
new file mode 100644
index 00000000..3944b4aa
Binary files /dev/null and b/8 WEEK/servlet/img/S4-1.png differ
diff --git a/8 WEEK/servlet/img/S4-2.png b/8 WEEK/servlet/img/S4-2.png
new file mode 100644
index 00000000..6f55a519
Binary files /dev/null and b/8 WEEK/servlet/img/S4-2.png differ
diff --git a/8 WEEK/servlet/img/S4-3.png b/8 WEEK/servlet/img/S4-3.png
new file mode 100644
index 00000000..d907f0d0
Binary files /dev/null and b/8 WEEK/servlet/img/S4-3.png differ
diff --git a/8 WEEK/servlet/img/S4-4.png b/8 WEEK/servlet/img/S4-4.png
new file mode 100644
index 00000000..bd12948f
Binary files /dev/null and b/8 WEEK/servlet/img/S4-4.png differ
diff --git a/8 WEEK/servlet/img/S4-5.png b/8 WEEK/servlet/img/S4-5.png
new file mode 100644
index 00000000..beb9c402
Binary files /dev/null and b/8 WEEK/servlet/img/S4-5.png differ
diff --git a/8 WEEK/servlet/img/S4-6.png b/8 WEEK/servlet/img/S4-6.png
new file mode 100644
index 00000000..f2539420
Binary files /dev/null and b/8 WEEK/servlet/img/S4-6.png differ
diff --git a/8 WEEK/servlet/img/S4-7.png b/8 WEEK/servlet/img/S4-7.png
new file mode 100644
index 00000000..c805b5b0
Binary files /dev/null and b/8 WEEK/servlet/img/S4-7.png differ
diff --git a/8 WEEK/servlet/settings.gradle b/8 WEEK/servlet/settings.gradle
new file mode 100644
index 00000000..9c5e4d1a
--- /dev/null
+++ b/8 WEEK/servlet/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'servlet'
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java b/8 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java
new file mode 100644
index 00000000..a1c772de
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java
@@ -0,0 +1,15 @@
+package hello.servlet;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.ServletComponentScan;
+
+@ServletComponentScan //서블릿 자동 등록
+@SpringBootApplication
+public class ServletApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ServletApplication.class, args);
+ }
+
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java b/8 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java
new file mode 100644
index 00000000..057e157f
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java
@@ -0,0 +1,13 @@
+package hello.servlet;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+public class ServletInitializer extends SpringBootServletInitializer {
+
+ @Override
+ protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+ return application.sources(ServletApplication.class);
+ }
+
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java
new file mode 100644
index 00000000..c514bc33
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java
@@ -0,0 +1,11 @@
+package hello.servlet.basic;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class HelloData {
+ private String username;
+ private int age;
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java
new file mode 100644
index 00000000..4a128f82
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java
@@ -0,0 +1,26 @@
+package hello.servlet.basic;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "helloServlet", urlPatterns = "/hello")
+public class HelloServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("HelloServlet.service");
+ System.out.println("requset = " + requset);
+ System.out.println("response = " + response);
+
+ String username = requset.getParameter("username");
+ System.out.println("username = " + username);
+
+ response.setContentType("text/plain");
+ response.setCharacterEncoding("utf-8");
+ response.getWriter().write("hello " + username);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java
new file mode 100644
index 00000000..3bb95632
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java
@@ -0,0 +1,35 @@
+package hello.servlet.basic.request;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import hello.servlet.basic.HelloData;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
+public class RequestBodyJsonServlet extends HttpServlet {
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ ServletInputStream inputStream = request.getInputStream();
+ String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
+
+ System.out.println("messageBody = " + messageBody);
+
+ HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
+
+ System.out.println("helloData.username = " + helloData.getUsername());
+ System.out.println("helloData.age = " + helloData.getAge());
+
+ response.getWriter().write("ok");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java
new file mode 100644
index 00000000..660342ba
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java
@@ -0,0 +1,25 @@
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@WebServlet(name="requestBodyStringServlet", urlPatterns = "/request-body-string")
+public class RequestBodyStringServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ ServletInputStream inputStream = request.getInputStream();
+ String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
+
+ System.out.println("messageBody = " + messageBody);
+
+ response.getWriter().write("OK");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java
new file mode 100644
index 00000000..3d19efca
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java
@@ -0,0 +1,108 @@
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
+public class RequestHeaderServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ printStartLine(request);
+ printHeaders(request);
+ printHeaderUtils(request);
+ printEtc(request);
+
+ }
+
+ //start line 정보
+ private void printStartLine(HttpServletRequest request) {
+ System.out.println("--- REQUEST-LINE - start ---");
+ System.out.println("request.getMethod() = " + request.getMethod()); //GET
+ System.out.println("request.getProtocol() = " + request.getProtocol()); //HTTP/1.1
+ System.out.println("request.getScheme() = " + request.getScheme()); //http
+ // http://localhost:8080/request-header
+ System.out.println("request.getRequestURL() = " + request.getRequestURL());
+ // /request-header
+ System.out.println("request.getRequestURI() = " + request.getRequestURI());
+ //username=hi
+ System.out.println("request.getQueryString() = " +
+ request.getQueryString());
+ System.out.println("request.isSecure() = " + request.isSecure()); //https 사용유무
+ System.out.println("--- REQUEST-LINE - end ---");
+ System.out.println();
+ }
+
+ //Header 모든 정보
+ private void printHeaders(HttpServletRequest request) {
+ System.out.println("--- Headers - start ---");
+ /*
+ Enumeration headerNames = request.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ System.out.println(headerName + ": " + request.getHeader(headerName));
+ }
+ */
+ request.getHeaderNames().asIterator()
+ .forEachRemaining(headerName -> System.out.println(headerName + ": "
+ + request.getHeader(headerName)));
+ System.out.println("--- Headers - end ---");
+ System.out.println();
+ }
+
+
+ //Header 편리한 조회
+ private void printHeaderUtils(HttpServletRequest request) {
+ System.out.println("--- Header 편의 조회 start ---");
+ System.out.println("[Host 편의 조회]");
+ System.out.println("request.getServerName() = " +
+ request.getServerName()); //Host 헤더
+ System.out.println("request.getServerPort() = " +
+ request.getServerPort()); //Host 헤더
+ System.out.println();
+ System.out.println("[Accept-Language 편의 조회]");
+ request.getLocales().asIterator()
+ .forEachRemaining(locale -> System.out.println("locale = " +
+ locale));
+ System.out.println("request.getLocale() = " + request.getLocale());
+ System.out.println();
+ System.out.println("[cookie 편의 조회]");
+ if (request.getCookies() != null) {
+ for (Cookie cookie : request.getCookies()) {
+ System.out.println(cookie.getName() + ": " + cookie.getValue());
+ }
+ }
+ System.out.println();
+ System.out.println("[Content 편의 조회]");
+ System.out.println("request.getContentType() = " +
+ request.getContentType());
+ System.out.println("request.getContentLength() = " +
+ request.getContentLength());
+ System.out.println("request.getCharacterEncoding() = " +
+ request.getCharacterEncoding());
+ }
+
+ //기타 정보
+ private void printEtc(HttpServletRequest request) {
+ System.out.println("--- 기타 조회 start ---");
+ System.out.println("[Remote 정보]");
+ System.out.println("request.getRemoteHost() = " +
+ request.getRemoteHost()); //
+ System.out.println("request.getRemoteAddr() = " +
+ request.getRemoteAddr()); //
+ System.out.println("request.getRemotePort() = " +
+ request.getRemotePort()); //
+ System.out.println();
+ System.out.println("[Local 정보]");
+ System.out.println("request.getLocalName() = " + request.getLocalName()); //
+ System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); //
+ System.out.println("request.getLocalPort() = " + request.getLocalPort()); //
+ System.out.println("--- 기타 조회 end ---");
+ System.out.println();
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java
new file mode 100644
index 00000000..d90e08d1
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java
@@ -0,0 +1,39 @@
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Enumeration;
+
+@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
+public class RequestParamServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("[전체 파라미터 조회] - start");
+
+// Enumeration parameterNames = requset.getParameterNames();
+ requset.getParameterNames().asIterator().forEachRemaining(
+ paramName -> System.out.println(paramName + "=" + requset.getParameter(paramName))
+ );
+
+ System.out.println("[전체 파라미터 조회] - end");
+ System.out.println();
+
+ System.out.println("[단일 파라미터 조회]");
+ String username= requset.getParameter("username");
+ String age = requset.getParameter("age");
+
+ System.out.println("username = " + username);
+ System.out.println("age = " + age);
+
+ System.out.println("[이름이 같은 복수 파라미터 조회]");
+ String[] usernames = requset.getParameterValues("username");
+ for(String name: usernames) {
+ System.out.println("username = " + name);
+ }
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java
new file mode 100644
index 00000000..12923c0a
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java
@@ -0,0 +1,62 @@
+package hello.servlet.basic.response;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header")
+public class ResponseHeaderServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ //[status-line]
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ //[response-headers]
+ response.setHeader("Content-Type", "text/plain;charset=utf-8");
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("my-header","hello");
+
+ //[Header 편의 메서드]
+// content(response);
+// cookie(response);
+// redirect(response);
+
+ //[message body]
+ PrintWriter writer = response.getWriter();
+ writer.println("ok");
+ }
+
+ private void content(HttpServletResponse response) {
+ //Content-Type: text/plain;charset=utf-8
+ //Content-Length: 2
+ //response.setHeader("Content-Type", "text/plain;charset=utf-8");
+ response.setContentType("text/plain");
+ response.setCharacterEncoding("utf-8");
+ //response.setContentLength(2); //(생략시 자동 생성)
+ }
+
+ private void cookie(HttpServletResponse response) {
+ //Set-Cookie: myCookie=good; Max-Age=600;
+ //response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
+ Cookie cookie = new Cookie("myCookie", "good");
+ cookie.setMaxAge(600); //600초
+ response.addCookie(cookie);
+ }
+
+ private void redirect(HttpServletResponse response) throws IOException {
+ //Status Code 302
+ //Location: /basic/hello-form.html
+ //response.setStatus(HttpServletResponse.SC_FOUND); //302
+ //response.setHeader("Location", "/basic/hello-form.html");
+ response.sendRedirect("/basic/hello-form.html");
+ }
+
+}
+
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java
new file mode 100644
index 00000000..a64c8450
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java
@@ -0,0 +1,27 @@
+package hello.servlet.basic.response;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html")
+public class ResponseHtmlServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ //Content-Type: text/html;charset=utf-8
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+ PrintWriter writer = response.getWriter();
+ writer.println("");
+ writer.println("");
+ writer.println(" HI?
");
+ writer.println("");
+ writer.println("");
+ }
+}
+
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java
new file mode 100644
index 00000000..5260635b
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java
@@ -0,0 +1,30 @@
+package hello.servlet.basic.response;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import hello.servlet.basic.HelloData;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
+public class ResponseJsonServlet extends HttpServlet {
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ response.setContentType("application/json");
+ response.setCharacterEncoding("utf-8");
+
+ HelloData helloData = new HelloData();
+ helloData.setUsername("lee");
+ helloData.setAge(20);
+
+ String result = objectMapper.writeValueAsString(helloData);
+ response.getWriter().write(result);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java b/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java
new file mode 100644
index 00000000..6fe75506
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java
@@ -0,0 +1,18 @@
+package hello.servlet.domain.member;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class Member {
+
+ private Long id;
+ private String username;
+ private int age;
+
+ public Member(String username, int age) {
+ this.username = username;
+ this.age = age;
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java b/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java
new file mode 100644
index 00000000..15eaaf81
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java
@@ -0,0 +1,38 @@
+package hello.servlet.domain.member;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MemberRepository {
+
+ private static Map store = new HashMap<>();
+ private static long sequence = 0L;
+
+ //싱글톤
+ private static final MemberRepository instance = new MemberRepository();
+
+ public static MemberRepository getInstance() {
+ return instance;
+ }
+ private MemberRepository(){}
+
+ public Member save(Member member) {
+ member.setId(++sequence);
+ store.put(member.getId(), member);
+ return member;
+ }
+
+ public Member findById(Long id) {
+ return store.get(id);
+ }
+
+ public List findAll() {
+ return new ArrayList<>(store.values());
+ }
+
+ public void clearStore() {
+ store.clear();
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java
new file mode 100644
index 00000000..a6a33309
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java
@@ -0,0 +1,23 @@
+package hello.servlet.web.frontcontroller;
+
+import java.util.HashMap;
+import java.util.Map;
+public class ModelView {
+ private String viewName;
+ private Map model = new HashMap<>();
+ public ModelView(String viewName) {
+ this.viewName = viewName;
+ }
+ public String getViewName() {
+ return viewName;
+ }
+ public void setViewName(String viewName) {
+ this.viewName = viewName;
+ }
+ public Map getModel() {
+ return model;
+ }
+ public void setModel(Map model) {
+ this.model = model;
+ }
+}
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java
new file mode 100644
index 00000000..4da0c980
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java
@@ -0,0 +1,32 @@
+package hello.servlet.web.frontcontroller;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class MyView {
+ private String viewPath;
+
+ public MyView(String viewPath) {
+ this.viewPath = viewPath;
+ }
+ public void render(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+ public void render(Map model, HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ modelToRequestAttribute(model, request);
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+ private void modelToRequestAttribute(Map model,
+ HttpServletRequest request) {
+ model.forEach((key, value) -> request.setAttribute(key, value));
+ }
+}
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java
new file mode 100644
index 00000000..0e2f00a3
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java
@@ -0,0 +1,11 @@
+package hello.servlet.web.frontcontroller.v1;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface ControllerV1 {
+ void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java
new file mode 100644
index 00000000..6dc0cab1
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java
@@ -0,0 +1,40 @@
+package hello.servlet.web.frontcontroller.v1;
+
+import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
+import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
+import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
+public class FrontControllerServiceV1 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV1() {
+ controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
+ controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
+ controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV1 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ controller.process(request, response);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java
new file mode 100644
index 00000000..e4927c67
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java
@@ -0,0 +1,18 @@
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberFormControllerV1 implements ControllerV1 {
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String viewPath = "/WEB-INF/views/new-form.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java
new file mode 100644
index 00000000..da67d7b2
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java
@@ -0,0 +1,28 @@
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MemberListControllerV1 implements ControllerV1 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+
+ request.setAttribute("members", members);
+
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java
new file mode 100644
index 00000000..c089740b
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java
@@ -0,0 +1,30 @@
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberSaveControllerV1 implements ControllerV1 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ request.setAttribute("member", member);
+
+ String viewPath = "/WEB-INF/views/save-result.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java
new file mode 100644
index 00000000..6e1de8a2
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java
@@ -0,0 +1,12 @@
+package hello.servlet.web.frontcontroller.v2;
+
+import hello.servlet.web.frontcontroller.MyView;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface ControllerV2 {
+ MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java
new file mode 100644
index 00000000..8a46b0c9
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java
@@ -0,0 +1,42 @@
+package hello.servlet.web.frontcontroller.v2;
+
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
+public class FrontControllerServiceV2 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV2() {
+ controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
+ controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
+ controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV2 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ MyView view = controller.process(request, response);
+ view.render(request, response);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java
new file mode 100644
index 00000000..ffdece7a
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java
@@ -0,0 +1,16 @@
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberFormControllerV2 implements ControllerV2 {
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ return new MyView("/WEB-INF/views/new-form.jsp");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java
new file mode 100644
index 00000000..1a69f49e
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java
@@ -0,0 +1,24 @@
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MemberListControllerV2 implements ControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+ request.setAttribute("members", members);
+ return new MyView("/WEB-INF/views/members.jsp");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java
new file mode 100644
index 00000000..e56b5984
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java
@@ -0,0 +1,28 @@
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberSaveControllerV2 implements ControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ request.setAttribute("member", member);
+ return new MyView("/WEB-INF/views/save-result.jsp");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java
new file mode 100644
index 00000000..ba2edc59
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java
@@ -0,0 +1,9 @@
+package hello.servlet.web.frontcontroller.v3;
+
+import hello.servlet.web.frontcontroller.ModelView;
+
+import java.util.Map;
+
+public interface ControllerV3 {
+ ModelView process(Map paramMap);
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java
new file mode 100644
index 00000000..46e92e6d
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java
@@ -0,0 +1,56 @@
+package hello.servlet.web.frontcontroller.v3;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
+public class FrontControllerServiceV3 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV3() {
+ controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
+ controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
+ controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV3 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ String viewName = mv.getViewName();
+ MyView view = viewResolver(viewName);
+ view.render(mv.getModel(), request, response);
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java
new file mode 100644
index 00000000..4b886d83
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java
@@ -0,0 +1,13 @@
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.Map;
+
+public class MemberFormControllerV3 implements ControllerV3 {
+ @Override
+ public ModelView process(Map paramMap) {
+ return new ModelView("new-form");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java
new file mode 100644
index 00000000..897cb7fd
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java
@@ -0,0 +1,24 @@
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.List;
+import java.util.Map;
+
+public class MemberListControllerV3 implements ControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public ModelView process(Map paramMap) {
+ List members = memberRepository.findAll();
+ ModelView mv = new ModelView("members");
+ mv.getModel().put("members", members);
+
+ return mv;
+
+ }
+
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java
new file mode 100644
index 00000000..cae1ba01
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java
@@ -0,0 +1,25 @@
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.Map;
+
+public class MemberSaveControllerV3 implements ControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public ModelView process(Map paramMap) {
+ String username = paramMap.get("username");
+ int age = Integer.parseInt(paramMap.get("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ ModelView mv = new ModelView("save-result");
+ mv.getModel().put("member", member);
+ return mv;
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java
new file mode 100644
index 00000000..6a4a5803
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java
@@ -0,0 +1,7 @@
+package hello.servlet.web.frontcontroller.v4;
+
+import java.util.Map;
+
+public interface ControllerV4 {
+ String process(Map paramMap, Map model);
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java
new file mode 100644
index 00000000..065a4e8f
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java
@@ -0,0 +1,58 @@
+package hello.servlet.web.frontcontroller.v4;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
+public class FrontControllerServiceV4 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV4() {
+ controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
+ controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
+ controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV4 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+
+ String viewName = controller.process(paramMap, model);
+
+ MyView view = viewResolver(viewName);
+ view.render(model, request, response);
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java
new file mode 100644
index 00000000..288bdff0
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java
@@ -0,0 +1,12 @@
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.Map;
+
+public class MemberFormControllerV4 implements ControllerV4 {
+ @Override
+ public String process(Map paramMap, Map model) {
+ return "new-form";
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java
new file mode 100644
index 00000000..e5a43cfc
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java
@@ -0,0 +1,22 @@
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.List;
+import java.util.Map;
+
+public class MemberListControllerV4 implements ControllerV4 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public String process(Map paramMap, Map model) {
+ List members = memberRepository.findAll();
+
+ model.put("members", members);
+ return "members";
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java
new file mode 100644
index 00000000..2a4c1128
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java
@@ -0,0 +1,24 @@
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.Map;
+
+public class MemberSaveControllerV4 implements ControllerV4 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public String process(Map paramMap, Map model) {
+ String username = paramMap.get("username");
+ int age = Integer.parseInt(paramMap.get("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ model.put("member", member);
+
+ return "save-result";
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java
new file mode 100644
index 00000000..778e60db
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java
@@ -0,0 +1,71 @@
+package hello.servlet.web.frontcontroller.v5;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
+import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
+import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
+public class FrontControllerServletV5 extends HttpServlet {
+ private final Map handlerMappingMap = new HashMap<>();
+ private final List handlerAdapters = new ArrayList<>();
+ public FrontControllerServletV5() {
+ initHandlerMappingMap();
+ initHandlerAdapters();
+ }
+ private void initHandlerMappingMap() {
+ handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
+
+ handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
+ handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
+ handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
+ }
+
+ private void initHandlerAdapters() {
+ handlerAdapters.add(new ControllerV3HandlerAdapter());
+ handlerAdapters.add(new ControllerV4HandlerAdapter());
+ }
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ Object handler = getHandler(request);
+ if (handler == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ MyHandlerAdapter adapter = getHandlerAdapter(handler);
+ ModelView mv = adapter.handle(request, response, handler);
+ MyView view = viewResolver(mv.getViewName());
+ view.render(mv.getModel(), request, response);
+ }
+ private Object getHandler(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ return handlerMappingMap.get(requestURI);
+ }
+ private MyHandlerAdapter getHandlerAdapter(Object handler) {
+ for (MyHandlerAdapter adapter : handlerAdapters) {
+ if (adapter.supports(handler)) {
+ return adapter;
+ }
+ }
+ throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java
new file mode 100644
index 00000000..db175c53
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java
@@ -0,0 +1,14 @@
+package hello.servlet.web.frontcontroller.v5;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface MyHandlerAdapter {
+ boolean supports(Object handler);
+
+ ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java
new file mode 100644
index 00000000..5453bbfa
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java
@@ -0,0 +1,29 @@
+package hello.servlet.web.frontcontroller.v5.adapter;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
+ @Override
+ public boolean supports(Object handler) {
+ return (handler instanceof ControllerV3);
+ }
+ @Override
+ public ModelView handle(HttpServletRequest request, HttpServletResponse
+ response, Object handler) {
+ ControllerV3 controller = (ControllerV3) handler;
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ return mv;
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+}
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java
new file mode 100644
index 00000000..99b967a9
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java
@@ -0,0 +1,40 @@
+package hello.servlet.web.frontcontroller.v5.adapter;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
+ @Override
+ public boolean supports(Object handler) {
+ return (handler instanceof ControllerV4);
+ }
+
+ @Override
+ public ModelView handle(HttpServletRequest request, HttpServletResponse
+ response, Object handler) {
+ ControllerV4 controller = (ControllerV4) handler;
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+ String viewName = controller.process(paramMap, model);
+
+ ModelView mv = new ModelView(viewName);
+ mv.setModel(model);
+
+ return mv;
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+}
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java
new file mode 100644
index 00000000..8c127920
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java
@@ -0,0 +1,38 @@
+package hello.servlet.web.servlet;
+
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
+public class MemberFormServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+
+ PrintWriter w = response.getWriter();
+
+ w.write("\n" +
+ "\n" +
+ "\n" +
+ " \n" +
+ " Title \n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n"); }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java
new file mode 100644
index 00000000..5a3dd1e6
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java
@@ -0,0 +1,62 @@
+package hello.servlet.web.servlet;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
+public class MemberListServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+
+ List members = memberRepository.findAll();
+
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+
+ PrintWriter w = response.getWriter();
+ w.write("");
+ w.write("");
+ w.write(" ");
+ w.write(" Title ");
+ w.write("");
+ w.write("");
+ w.write("메인 ");
+ w.write("");
+ w.write(" ");
+ w.write(" id ");
+ w.write(" username ");
+ w.write(" age ");
+ w.write(" ");
+ w.write(" ");
+/*
+ w.write(" ");
+ w.write(" 1 ");
+ w.write(" userA ");
+ w.write(" 10 ");
+ w.write(" ");
+*/
+ for (Member member : members) {
+ w.write(" ");
+ w.write(" " + member.getId() + " ");
+ w.write(" " + member.getUsername() + " ");
+ w.write(" " + member.getAge() + " ");
+ w.write(" ");
+ }
+ w.write(" ");
+ w.write("
");
+ w.write("");
+ w.write("");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java
new file mode 100644
index 00000000..1c6159b7
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java
@@ -0,0 +1,47 @@
+package hello.servlet.web.servlet;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
+public class MemberSaveServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("MemberSaveServlet.service");
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+ PrintWriter w = response.getWriter();
+
+ w.write("\n" +
+ "\n" +
+ " \n" +
+ "\n" +
+ "\n" +
+ "성공\n" +
+ "\n" +
+ " id="+member.getId()+" \n" +
+ " username="+member.getUsername()+" \n" +
+ " age="+member.getAge()+" \n" +
+ " \n" +
+ "메인 \n" +
+ "\n" +
+ "");
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java
new file mode 100644
index 00000000..01ead1c9
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java
@@ -0,0 +1,21 @@
+package hello.servlet.web.servletmvc;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
+public class MvcMemberFormServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String viewPath = "/WEB-INF/views/new-form.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java
new file mode 100644
index 00000000..56ef49e9
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java
@@ -0,0 +1,30 @@
+package hello.servlet.web.servletmvc;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
+public class MvcMemberListServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+
+ request.setAttribute("members", members);
+
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java
new file mode 100644
index 00000000..31f9428d
--- /dev/null
+++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java
@@ -0,0 +1,34 @@
+package hello.servlet.web.servletmvc;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
+public class MvcMemberSaveServlet extends HttpServlet {
+
+ MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+
+ //Model에 데이터 보관.
+ request.setAttribute("member", member);
+
+ String viewPath = "/WEB-INF/views/save-result.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/8 WEEK/servlet/src/main/resources/application.properties b/8 WEEK/servlet/src/main/resources/application.properties
new file mode 100644
index 00000000..efc5ff2f
--- /dev/null
+++ b/8 WEEK/servlet/src/main/resources/application.properties
@@ -0,0 +1 @@
+logging.level.org.apache.coyote.http11=debug
diff --git a/8 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp
new file mode 100644
index 00000000..d9faff4a
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp
@@ -0,0 +1,27 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+
+
+
+ Title
+
+
+메인
+
+
+ id
+ username
+ age
+
+
+
+
+ ${item.id}
+ ${item.username}
+ ${item.age}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp
new file mode 100644
index 00000000..39d9e9b7
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp
@@ -0,0 +1,15 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+
+ Title
+
+
+
+
+
+
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp
new file mode 100644
index 00000000..d3c0ed84
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp
@@ -0,0 +1,15 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+
+
+
+성공
+
+ id=${member.id}
+ username=${member.username}
+ age=${member.age}
+
+메인
+
+
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/webapp/basic.html b/8 WEEK/servlet/src/main/webapp/basic.html
new file mode 100644
index 00000000..813a0b8e
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/basic.html
@@ -0,0 +1,40 @@
+
+
+
+
+ Title
+
+
+
+ hello 서블릿
+
+
+ HttpServletRequest
+
+
+ HttpServletResponse
+
+
+
+
+
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/webapp/basic/hello-form.html b/8 WEEK/servlet/src/main/webapp/basic/hello-form.html
new file mode 100644
index 00000000..a712358f
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/basic/hello-form.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Title
+
+
+
+
+
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/webapp/index.html b/8 WEEK/servlet/src/main/webapp/index.html
new file mode 100644
index 00000000..22f9745d
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/index.html
@@ -0,0 +1,86 @@
+
+
+
+
+ Title
+
+
+
+ 서블릿 basic
+ 서블릿
+
+
+ JSP
+
+
+ 서블릿 MVC
+
+
+ FrontController - v1
+
+
+ FrontController - v2
+
+
+ FrontController - v3
+
+
+ FrontController - v4
+
+
+ FrontController - v5 - v3
+
+
+ FrontController - v5 - v4
+
+
+ SpringMVC - v1
+
+
+ SpringMVC - v2
+
+
+ SpringMVC - v3
+
+
+
+
+
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/webapp/jsp/members.jsp b/8 WEEK/servlet/src/main/webapp/jsp/members.jsp
new file mode 100644
index 00000000..3a036b59
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/jsp/members.jsp
@@ -0,0 +1,36 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="hello.servlet.domain.member.Member" %>
+<%@ page import="java.util.List" %>
+<%@ page import="hello.servlet.domain.member.MemberRepository" %>
+<%
+ MemberRepository memberRepository = MemberRepository.getInstance();
+
+ List members = memberRepository.findAll();
+%>
+
+
+
+ Title
+
+
+메인
+
+
+ id
+ username
+ age
+
+
+ <%
+ for (Member member : members) {
+ out.write(" ");
+ out.write(" " + member.getId() + " ");
+ out.write(" " + member.getUsername() + " ");
+ out.write(" " + member.getAge() + " ");
+ out.write(" ");
+ }
+ %>
+
+
+
+
\ No newline at end of file
diff --git a/8 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp b/8 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp
new file mode 100644
index 00000000..705255aa
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp
@@ -0,0 +1,13 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+ Title
+
+
+
+
+
diff --git a/8 WEEK/servlet/src/main/webapp/jsp/members/save.jsp b/8 WEEK/servlet/src/main/webapp/jsp/members/save.jsp
new file mode 100644
index 00000000..6a102d8d
--- /dev/null
+++ b/8 WEEK/servlet/src/main/webapp/jsp/members/save.jsp
@@ -0,0 +1,27 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="hello.servlet.domain.member.Member" %>
+<%@ page import="hello.servlet.domain.member.MemberRepository" %>
+<%
+ MemberRepository memberRepository = MemberRepository.getInstance();
+ //request, response는 지원함
+ System.out.println("MemberSaveServlet.service");
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+%>
+
+
+ Title
+
+
+ 성공
+
+ id=<%=member.getId()%>
+ username=<%=member.getUsername()%>
+ age=<%=member.getAge()%>
+
+ 메인
+
+
diff --git a/8 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java b/8 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java
new file mode 100644
index 00000000..9df6b76e
--- /dev/null
+++ b/8 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java
@@ -0,0 +1,13 @@
+package hello.servlet;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class ServletApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/8 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java b/8 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java
new file mode 100644
index 00000000..f6c6f6b3
--- /dev/null
+++ b/8 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java
@@ -0,0 +1,46 @@
+package hello.servlet.domain.member;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class MemberRepositoryTest {
+
+ MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @AfterEach
+ void afterEach() {
+ memberRepository.clearStore();
+ }
+
+ @Test
+ void save() {
+ Member member = new Member("Hello", 20);
+
+ Member savedMember = memberRepository.save(member);
+
+ Member findMember = memberRepository.findById(savedMember.getId());
+ Assertions.assertThat(findMember).isEqualTo(savedMember);
+
+ }
+
+ @Test
+ void findAll() {
+ Member member1 = new Member("Hello1", 20);
+ Member member2 = new Member("Hello2", 20);
+
+ memberRepository.save(member1);
+ memberRepository.save(member2);
+
+ List memberList = memberRepository.findAll();
+
+ Assertions.assertThat(memberList.size()).isEqualTo(2);
+ Assertions.assertThat(memberList).contains(member1, member2);
+ }
+
+
+}
\ No newline at end of file
diff --git a/9 WEEK/item-service/.gitignore b/9 WEEK/item-service/.gitignore
new file mode 100644
index 00000000..c2065bc2
--- /dev/null
+++ b/9 WEEK/item-service/.gitignore
@@ -0,0 +1,37 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
diff --git a/9 WEEK/item-service/SECTION7.md b/9 WEEK/item-service/SECTION7.md
new file mode 100644
index 00000000..17275fb6
--- /dev/null
+++ b/9 WEEK/item-service/SECTION7.md
@@ -0,0 +1,767 @@
+# 7. 스프링 MVC - 웹 페이지 만들기
+
+**서비스 제공 흐름**
+
+
+
+---
+
+
+## 상품 도메인 개발
+
+*Item - 상품 객체*
+```java
+package hello.itemservice.domain.item;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class Item {
+ private long id;
+ private String itemName;
+ private Integer price; //null 가능
+ private Integer quantity; //null 가능
+
+ public Item(String itemName, Integer price, Integer quantity) {
+ this.itemName = itemName;
+ this.price = price;
+ this.quantity = quantity;
+ }
+}
+```
+
+*ItemRepository - 상품 저장소*
+```java
+package hello.itemservice.domain.item;
+
+import org.springframework.stereotype.Repository;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Repository
+public class ItemRepository {
+
+ private static final Map store = new HashMap<>();
+ private static long sequence = 0L;
+
+ public Item save(Item item) {
+ item.setId(++sequence);
+ store.put(item.getId(), item);
+ return item;
+ }
+
+ public Item findById(Long id) {
+ return store.get(id);
+ }
+
+ public List- findAll() {
+ return new ArrayList<>(store.values());
+ }
+
+ public void update(Long itemId, Item updateParam) {
+ Item findItem = findById(itemId);
+ findItem.setItemName(updateParam.getItemName());
+ findItem.setPrice(updateParam.getPrice());
+ findItem.setQuantity(updateParam.getQuantity());
+ }
+
+ public void clearStore() {
+ store.clear();
+ }
+}
+```
+
+*ItemRepositoryTest - 상품 저장소 테스트*
+```java
+package hello.itemservice.domain.item;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+
+class ItemRepositoryTest {
+
+ ItemRepository itemRepository = new ItemRepository();
+
+ @AfterEach
+ void afterEach() {
+ itemRepository.clearStore();
+ }
+
+ @Test
+ void save() {
+ //give
+ Item item = new Item("itemA", 10000, 10);
+ //when
+ Item savedItem = itemRepository.save(item);
+
+ //then
+ Item findItem = itemRepository.findById(item.getId());
+ assertThat(findItem).isEqualTo(savedItem);
+ }
+
+ @Test
+ void findAll() {
+ //give
+ Item item1 = new Item("item1", 10000, 10);
+ Item item2 = new Item("item2", 20000, 20);
+
+ itemRepository.save(item1);
+ itemRepository.save(item2);
+ //when
+ List
- result = itemRepository.findAll();
+
+ //then
+ assertThat(result.size()).isEqualTo(2);
+ assertThat(result).contains(item1, item2);
+ }
+
+ @Test
+ void update() {
+ //give
+ Item item1 = new Item("item1", 10000, 10);
+ Item savedItem = itemRepository.save(item1);
+ Long itemId = item1.getId();
+
+ //when
+ Item updateParam = new Item("item2", 20000, 20);
+ itemRepository.update(itemId, updateParam);
+
+ //then
+ Item findItem = itemRepository.findById(itemId);
+
+ assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
+ assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
+ assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
+ }
+}
+```
+
+---
+
+
+## 상품 서비스 HTML
+- 상품 목록 HTML
+- 상품 상세 HTML
+- 상품 등록 폼 HTML
+- 상품 수정 폼 HTML
+
+
+---
+
+## 상품 목록 - Thymeleaf
+
+*BasicItemController*
+```java
+package hello.itemservice.web.basic;
+
+import hello.itemservice.domain.item.Item;
+import hello.itemservice.domain.item.ItemRepository;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.util.List;
+
+@Controller
+@RequestMapping("/basic/items")
+@RequiredArgsConstructor
+public class BasicItemController {
+
+ private final ItemRepository itemRepository;
+
+ @GetMapping()
+ public String items(Model model) {
+ List
- itmes = itemRepository.findAll();
+ model.addAttribute("items",itmes);
+ return "basic/items";
+ }
+
+
+ @PostConstruct
+ public void init() {
+ itemRepository.save(new Item("itemA", 10000, 10));
+ itemRepository.save(new Item("itemB", 20000, 20));
+ }
+
+}
+```
+
+- `@RequiredArgsController`
+ - `final`이 붙은 멤버변수만 사용해 생성자들 자동으로 만듦
+
+
+*items.html*
+```html
+
+
+
+
+
+
+
+
+
+
+
상품 목록
+
+
+
+
+
+
+
+
+
+ ID
+ 상품명
+ 가격
+ 수량
+
+
+
+
+ 회원id
+ 상품명
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 타임리프 간단히 알아보기
+**타임리프 사용 선언**
+``
+
+**속성 변경 - th:href**
+`th:href="@{css/bootstrap.min.css"`
+- `href="xxx1"`을 `th:href="xxx2"`의 값으로 변경
+- HTML을 그대로 볼 때는 `href`속성이 사용, 뷰 템플릿을 거치면 `th:href`의 값이 `href`로 대체돼 동적으로 변경 가능.
+- 대부분의 HTML속성을 `th:xxx`로 변경 가능
+
+**타임리프 핵심**
+- 핵심 : `th:xxx`가 붙은 부분은 서버사이드에서 렌더링, 기존 것을 대체함.
+`th:xxx`가 없으면 기존 html의 `xxx`속성이 그대로 사용됨.
+- HTML을 파일로 직접 열었을 때, `th:xxx`가 있어도 웹 브라우저는 `th:`속성을 알지 못해 무시함.
+- 따라서 HTML을 파일 보기를 유지하면서 템플릿 기능도 사용 가능.
+
+**상품 등록 폼으로 이동**
+**속성 변경 - `th:onclick`**
+- `onclick="location.href='addForm.html'"`
+- `th:onclick="|location.href='@{/basic/items/add}'|"`
+
+**리터럴 대체 - |...|**
+- 타임리프에서 문자와 표현식 등은 분리돼 있어 더해서 사용해야함.
+ - ex) ``
+- 다음과 같이 리터럴 대체 문자를 사용하면, 더하기 없이 편리하게 사용 가능.
+ - ex) ``
+
+- 결과를 다음과 같이 만들어야 하는데
+ - `location.href='basic/items/add'`
+- 그냥 사용하면 문자와 표현식을 따로 더해서 사용해야 하므로 복잡해짐.
+ - `th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"`
+- 리터럴 대체 문법을 사용하면 다음과 같이 편리하게 사용 가능
+ - `th:onclick="|location.href='@{/basic/items/add}'|"`
+
+**반복 출력 - th:each**
+- `10000`
+- 내용의 값을 th:text 의 값으로 변경.
+- 여기서는 10000을 ${item.price} 의 값으로 변경.
+
+**URL 링크 표현식2 - @{...}**
+- `th:href="@{/basic/items/{itemId}(itemId=${item.id})}"`
+- 경로 변수 `({itemId})`뿐만 아니라 쿼리 파라미터도 생성.
+- ex) `th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"`
+ - 생성 링크: http://localhost:8080/basic/items/1?query=test
+
+**URL 링크 간단히**
+- th:href="@{|/basic/items/${item.id}|}"
+- 리터럴 대체 문법을 활용해서 간단히 사용할 수 있음.
+
+
+> 타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용 확인이 가능, 서버를 통해 뷰 템플릿을 거치면
+> 동적으로 변경된 결과를 확인할 수 있음. 하지만 JSP 파일은 웹 브러우저에서 그냥 열면 JSP 코드와
+> HTML 이 섞여 있어 정상적인 확인이 불가능. 오직 서버를 통해 JSP 를 열어야 함.
+> 이렇게 순수 **HTML 을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿**이라고 함.
+
+
+---
+
+
+## 상품 상세
+
+*item method*
+```java
+ @GetMapping("/{itemId}")
+ public String item(@PathVariable("itemId") Long itemId, Model model) {
+ Item item = itemRepository.findById(itemId);
+ model.addAttribute("item", item);
+ return "basic/item";
+ }
+```
+
+`PathVariable`로 넘어온 상품ID로 상품을 조회하고, 모델에 담아둠. 그리고 뷰 템플릿을 호출.
+
+
+*item.html*
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
상품 상세
+
+
+
+ 상품 ID
+
+
+
+ 상품명
+
+
+
+ 가격
+
+
+
+ 수량
+
+
+
+
+
+
+
+
+
+
+```
+
+**속성 변경 - th:value**
+`th:value="${item.id}"`
+- 모델이 있는 item 정보를 획득하고 프로퍼티 접근법으로 출력한다. `(item.getId())`
+- `value` 속성을 `th:value` 속성으로 변경.
+
+**상품 수정 링크**
+- `th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"`
+
+**목록으로 링크**
+- `th:onclick="|location.href='@{/basic/items}'|"`
+
+
+## 상품 등록 폼
+
+*addForm method*
+```java
+ @GetMapping("/add")
+ public String addForm() {
+ return "basic/addForm";
+ }
+```
+
+*addForm.html - 상품 등록 폼 뷰*
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
상품 등록 폼
+
+
+
상품 입력
+
+
+
+
+
+
+```
+
+**속성 변경 - th:action**
+- `th:action`
+- HTML form에서 `action` 에 값이 없으면 현재 URL에 데이터를 전송한다.
+- 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분한다.
+ - 상품 등록 폼: GET `/basic/items/add`
+ - 상품 등록 처리: POST `/basic/items/add`
+-이렇게 하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있다.
+
+**취소**
+- 취소시 상품 목록으로 이동한다.
+- `th:onclick="|location.href='@{/basic/items}'|"`
+
+
+## 상품 등록 처리 - @ModelAttribute
+
+상품 등록 폼은 다음 방식으로 서버에 데이터를 전달.
+- POST - HTML Form
+ - `content-type: application/x-www-form-urlencoded`
+ - 메시지 바디에 쿼리 파리미터 형식으로 전달 `itemName=itemA&price=10000&quantity=10`
+ - 예) 회원 가입, 상품 주문, HTML Form 사용
+
+요청 파라미터 형식을 처리하기 위해 `@RequestParam`을 사용해야 함.
+
+
+
+### 상품 등록 처리 - @RequestParam
+
+*addItemV1 method*
+```java
+@PostMapping("/add")
+public String addItemV1(@RequestParam String itemName,
+ @RequestParam int price,
+ @RequestParam Integer quantity,
+ Model model) {
+ Item item = new Item();
+ item.setItemName(itemName);
+ item.setPrice(price);
+ item.setQuantity(quantity);
+ itemRepository.save(item);
+ model.addAttribute("item", item);
+ return "basic/item";
+}
+```
+- @RequestParam String itemName` : itemName 요청 파라미터 데이터를 해당 변수에 받음.
+- `Item` 객체를 생성하고 `itemRepository` 를 통해서 저장.
+- 저장된 item 을 모델에 담아서 뷰에 전달.
+
+
+### 상품 등록 처리 - @ModelAttribute
+
+*addItemV2 method*
+```java
+@PostMapping("/add")
+public String addItemV2(@ModelAttribute("item") Item item, Model model) {
+ itemRepository.save(item);
+ //model.addAttribute("item", item); //자동 추가, 생략 가능
+ return "basic/item";
+}
+```
+
+**@ModelAttribute - 요청 파라미터 처리**
+- `@ModelAttribute` 는 Item 객체를 생성, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력.
+
+**@ModelAttribute - Model 추가**
+- `@ModelAttribute` 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 `@ModelAttribute`로 지정한 객체를 자동으로 넣어줌.
+코드를 보면 model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작하는 것을 확인 가능
+
+모델에 데이터를 담을 때는 이름이 필요. 이름은 `@ModelAttribute`에 지정한 `name(value)`속성을 사용한다.
+만약 다음과 같이 `@ModelAttribute`의 이름을 다르게 지정하면 다른 이름으로 모델에 포함.
+
+EX)
+- @ModelAttribute("hello") Item item 이름을 hello 로 지정
+- model.addAttribute("hello", item); 모델에 hello 이름으로 저장
+
+
+*addItemV3 - 상품 등록 처리 - Model Attribute 이름 생략*
+```java
+@PostMapping("/add")
+public String addItemV3(@ModelAttribute Item item) {
+ itemRepository.save(item);
+ return "basic/item";
+}
+```
+`@ModelAttribute`의 이름 생략 가능
+
+**주의**
+`@ModelAttribute` 의 이름을 생략하면 모델에 저장될 때 클래스명을 사용.
+이때 클래스의 첫글자만 소문자로 변경해서 등록.
+
+EX)
+- `@ModelAttribute`클래스명 -> 모델에 자동 추가되는 이름
+ - `Item -> item`
+ - `HelloWorld -> helloWorld`
+
+
+*addItemV4 - 상품 등록 처리 - ModelAttribute 전체 생략*
+```java
+@PostMapping("/add")
+public String addItemV4(Item item) {
+ itemRepository.save(item);
+ return "basic/item";
+}
+```
+`@ModelAttribute`자체도 생략 가능. 대상 객체는 모델에 자동 등록. 나머지 사항은 기존과 동일.
+
+
+
+## 상품 수정
+
+*editForm method - 상품 수정 폼 컨트롤러*
+```java
+ @GetMapping("/{itemId}/edit")
+ public String editForm(@PathVariable("itemId") Long itemId, Model model) {
+ Item item = itemRepository.findById(itemId);
+ model.addAttribute("item", item);
+ return "basic/editForm";
+ }
+```
+
+
+*editForm.html - 상품 수정 폼 뷰*
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+### 상품 수정 개발
+```java
+ @PostMapping("/{itemId}/edit")
+ public String edit(@PathVariable("itemId") Long itemId, @ModelAttribute Item item) {
+ itemRepository.update(itemId, item);
+
+ return "redirect:/basic/items/{itemId}";
+ }
+```
+
+- GET `/items/{itemId}/edit` : 상품 수정 폼
+- POST `/items/{itemId}/edit` : 상품 수정 처리
+
+
+**리다이렉트**
+상품 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트 호출
+- 스프링은 `redirect:/....`으로 편리하게 리다이레그를 지원.
+- `redirect:/basic/items/{itemId}`
+ - 컨트롤러에 매핑된 `@PathVariable`의 값은 `redirect` 에도 사용 가능.
+ - `redirect:/basic/items/{itemId}` -> `{itemId}`는
+ `@PathVariable("itemId") Long itemId` 의 값을 그대로 사용함.
+
+
+
+## PRG Post/Redirect/Get
+현재 코드에는 문제가 있다. 상품 등록 후 새로고침을 할 때 상품이 중복해서 등록된다.
+
+**POST 등록 후 새로고침**
+
+
+상품 등록 폼에서 데이터 입력, 저장을 선택하면 `POST /add` + 상품 데이터로 서버로 전송
+이 상태에서 새로고침을 하면 마지막에 전송한 `POST /add` + 상품 데이터로 다시 전송하게 된다.
+그래서 내용은 같고, ID만 다른 상품 데이터가 쌓인다.
+
+
+
+**POST, Redirect, GET**
+
+
+새로 고침 문제를 해결하려면 상품 저장 후 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트 호출하면 된다.
+웹 브라우저는 리다이렉트의 영향으로 사움 저장 후 실제 상품 상세 화면으로 다시 이동한다.
+따라서 마지막에 호출한 내용이 상품 상세 화면인 `GET /items/{id}`가 된다.
+
+*save method 수정*
+```java
+ @PostMapping("/add")
+ public String save(@ModelAttribute("item") Item item, Model model) {
+ itemRepository.save(item);
+ //model.addAttribute("item", item); //자동 추가, 생략 가능
+// return "basic/item";
+ return "redirect:/basic/items/" + item.getId();
+ }
+```
+
+이런 문제 해결 방식을 `PRG Post/Redirect/Get`라 한다.
+
+
+## RedirectAttributes
+상품이 저장이 잘 됐다면 "저장 완료"라는 메시지를 보여주게 수정한다.
+
+*save method 수정*
+```java
+ @PostMapping("/add")
+ public String save(@ModelAttribute("item") Item item, Model model, RedirectAttributes redirectAttributes) {
+ Item savedItem = itemRepository.save(item);
+// model.addAttribute("item", item); //자동 추가, 생략 가능
+// return "basic/item";
+ redirectAttributes.addAttribute("savedItem", savedItem.getId());
+ redirectAttributes.addAttribute("status", true);
+ return "redirect:/basic/items/{savedItem}";
+ }
+```
+실행 시 리다이렉트 결과
+`http://localhost:8080/basic/items/3?status=true`
+
+**RedirectAttributes**
+`RedirectAttributes`를 사용하면 URL인코딩도 해주고, `pathVariable`, 쿼리 파라미터도 처리함.
+- `redirect:/basic/items/{itemId}`
+- pathVariable 바인딩: `{itemId}`
+- 나머지는 쿼리 파라미터로 처리: `?status=true`
+
+
+*item.html - 뷰 템플릿 메시지 추가*
+```html
+
+
상품 상세
+
+
+
+
+
+```
+
+- `th:if` : 해당 조건이 참이면 실행
+- `${param.status}` : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
+ - 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 함.
+ 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원.
+
+
+
+
+
+---
\ No newline at end of file
diff --git a/9 WEEK/item-service/build.gradle b/9 WEEK/item-service/build.gradle
new file mode 100644
index 00000000..a65c94f1
--- /dev/null
+++ b/9 WEEK/item-service/build.gradle
@@ -0,0 +1,34 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '3.2.1'
+ id 'io.spring.dependency-management' version '1.1.4'
+}
+
+group = 'hello'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ sourceCompatibility = '17'
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar b/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..d64cd491
Binary files /dev/null and b/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties b/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..1af9e093
--- /dev/null
+++ b/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/9 WEEK/item-service/gradlew b/9 WEEK/item-service/gradlew
new file mode 100644
index 00000000..1aa94a42
--- /dev/null
+++ b/9 WEEK/item-service/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/9 WEEK/item-service/gradlew.bat b/9 WEEK/item-service/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/9 WEEK/item-service/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/9 WEEK/item-service/img/S7-1.png b/9 WEEK/item-service/img/S7-1.png
new file mode 100644
index 00000000..5df8b1ce
Binary files /dev/null and b/9 WEEK/item-service/img/S7-1.png differ
diff --git a/9 WEEK/item-service/img/S7-2.png b/9 WEEK/item-service/img/S7-2.png
new file mode 100644
index 00000000..ab27260d
Binary files /dev/null and b/9 WEEK/item-service/img/S7-2.png differ
diff --git a/9 WEEK/item-service/img/S7-3.png b/9 WEEK/item-service/img/S7-3.png
new file mode 100644
index 00000000..e301cd83
Binary files /dev/null and b/9 WEEK/item-service/img/S7-3.png differ
diff --git a/9 WEEK/item-service/settings.gradle b/9 WEEK/item-service/settings.gradle
new file mode 100644
index 00000000..df5bd80b
--- /dev/null
+++ b/9 WEEK/item-service/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'item-service'
diff --git a/9 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java b/9 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java
new file mode 100644
index 00000000..1311934b
--- /dev/null
+++ b/9 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java
@@ -0,0 +1,13 @@
+package hello.itemservice;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ItemServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ItemServiceApplication.class, args);
+ }
+
+}
diff --git a/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/Item.java b/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/Item.java
new file mode 100644
index 00000000..ada50303
--- /dev/null
+++ b/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/Item.java
@@ -0,0 +1,21 @@
+package hello.itemservice.domain.item;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class Item {
+ private long id;
+ private String itemName;
+ private Integer price; //null 가능
+ private Integer quantity; //null 가능
+
+ public Item(String itemName, Integer price, Integer quantity) {
+ this.itemName = itemName;
+ this.price = price;
+ this.quantity = quantity;
+ }
+}
diff --git a/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/ItemRepository.java b/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/ItemRepository.java
new file mode 100644
index 00000000..c4f3fbc8
--- /dev/null
+++ b/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/ItemRepository.java
@@ -0,0 +1,41 @@
+package hello.itemservice.domain.item;
+
+import org.springframework.stereotype.Repository;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Repository
+public class ItemRepository {
+
+ private static final Map
store = new HashMap<>();
+ private static long sequence = 0L;
+
+ public Item save(Item item) {
+ item.setId(++sequence);
+ store.put(item.getId(), item);
+ return item;
+ }
+
+ public Item findById(Long id) {
+ return store.get(id);
+ }
+
+ public List- findAll() {
+ return new ArrayList<>(store.values());
+ }
+
+ public void update(Long itemId, Item updateParam) {
+ Item findItem = findById(itemId);
+ findItem.setItemName(updateParam.getItemName());
+ findItem.setPrice(updateParam.getPrice());
+ findItem.setQuantity(updateParam.getQuantity());
+ }
+
+ public void clearStore() {
+ store.clear();
+ }
+}
+
diff --git a/9 WEEK/item-service/src/main/java/hello/itemservice/web/basic/BasicItemController.java b/9 WEEK/item-service/src/main/java/hello/itemservice/web/basic/BasicItemController.java
new file mode 100644
index 00000000..8c79df4e
--- /dev/null
+++ b/9 WEEK/item-service/src/main/java/hello/itemservice/web/basic/BasicItemController.java
@@ -0,0 +1,94 @@
+package hello.itemservice.web.basic;
+
+import hello.itemservice.domain.item.Item;
+import hello.itemservice.domain.item.ItemRepository;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import java.util.List;
+
+@Controller
+@RequestMapping("/basic/items")
+@RequiredArgsConstructor
+public class BasicItemController {
+
+ private final ItemRepository itemRepository;
+
+ @GetMapping()
+ public String items(Model model) {
+ List
- itmes = itemRepository.findAll();
+ model.addAttribute("items",itmes);
+ return "basic/items";
+ }
+
+
+ @GetMapping("/{itemId}")
+ public String item(@PathVariable("itemId") Long itemId, Model model) {
+ Item item = itemRepository.findById(itemId);
+ model.addAttribute("item", item);
+ return "basic/item";
+ }
+
+
+ @GetMapping("/add")
+ public String addForm() {
+ return "basic/addForm";
+ }
+
+
+// @PostMapping("/add")
+// public String save(
+// @RequestParam String itemName,
+// @RequestParam int price,
+// @RequestParam Integer quantity,
+// Model model
+// ) {
+// Item item = new Item(itemName, price, quantity);
+// itemRepository.save(item);
+// model.addAttribute("item", item);
+// return "basic/item";
+// }
+
+ @PostMapping("/add")
+ public String save(@ModelAttribute("item") Item item, Model model, RedirectAttributes redirectAttributes) {
+ Item savedItem = itemRepository.save(item);
+// model.addAttribute("item", item); //자동 추가, 생략 가능
+// return "basic/item";
+ redirectAttributes.addAttribute("savedItem", savedItem.getId());
+ redirectAttributes.addAttribute("status", true);
+ return "redirect:/basic/items/{savedItem}";
+ }
+
+
+
+
+ @GetMapping("/{itemId}/edit")
+ public String editForm(@PathVariable("itemId") Long itemId, Model model) {
+ Item item = itemRepository.findById(itemId);
+ model.addAttribute("item", item);
+ return "basic/editForm";
+ }
+
+ @PostMapping("/{itemId}/edit")
+ public String edit(@PathVariable("itemId") Long itemId, @ModelAttribute Item item) {
+ itemRepository.update(itemId, item);
+
+ return "redirect:/basic/items/{itemId}";
+ }
+
+
+
+
+ @PostConstruct
+ public void init() {
+ itemRepository.save(new Item("itemA", 10000, 10));
+ itemRepository.save(new Item("itemB", 20000, 20));
+ }
+
+}
+
+
diff --git a/9 WEEK/item-service/src/main/resources/application.properties b/9 WEEK/item-service/src/main/resources/application.properties
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/application.properties
@@ -0,0 +1 @@
+
diff --git a/9 WEEK/item-service/src/main/resources/static/css/bootstrap.min.css b/9 WEEK/item-service/src/main/resources/static/css/bootstrap.min.css
new file mode 100644
index 00000000..edfbbb03
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/static/css/bootstrap.min.css
@@ -0,0 +1,7 @@
+@charset "UTF-8";/*!
+ * Bootstrap v5.0.2 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors
+ * Copyright 2011-2021 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + (.5rem + 2px));padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + (1rem + 2px));padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + (.75rem + 2px))}textarea.form-control-sm{min-height:calc(1.5em + (.5rem + 2px))}textarea.form-control-lg{min-height:calc(1.5em + (1rem + 2px))}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1050;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#6c757d!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}
+/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/static/html/addForm.html b/9 WEEK/item-service/src/main/resources/static/html/addForm.html
new file mode 100644
index 00000000..22089e3b
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/static/html/addForm.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
상품 등록 폼
+
+
+
상품 입력
+
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/static/html/editForm.html b/9 WEEK/item-service/src/main/resources/static/html/editForm.html
new file mode 100644
index 00000000..32d21dfc
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/static/html/editForm.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/static/html/item.html b/9 WEEK/item-service/src/main/resources/static/html/item.html
new file mode 100644
index 00000000..51b57f3b
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/static/html/item.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
상품 상세
+
+
+
+ 상품 ID
+
+
+
+ 상품명
+
+
+
+ 가격
+
+
+
+ 수량
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/static/html/items.html b/9 WEEK/item-service/src/main/resources/static/html/items.html
new file mode 100644
index 00000000..73732eb4
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/static/html/items.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
상품 목록
+
+
+
+
+
+
+
+
+
+ ID
+ 상품명
+ 가격
+ 수량
+
+
+
+
+ 1
+ 테스트 상품1
+ 10000
+ 10
+
+
+ 2
+ 테스트 상품2
+ 20000
+ 20
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/static/index/index.html b/9 WEEK/item-service/src/main/resources/static/index/index.html
new file mode 100644
index 00000000..4daf829d
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/static/index/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Title
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/templates/basic/addForm.html b/9 WEEK/item-service/src/main/resources/templates/basic/addForm.html
new file mode 100644
index 00000000..8d85fec2
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/templates/basic/addForm.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
상품 등록 폼
+
+
+
상품 입력
+
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/templates/basic/editForm.html b/9 WEEK/item-service/src/main/resources/templates/basic/editForm.html
new file mode 100644
index 00000000..b35b4482
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/templates/basic/editForm.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/templates/basic/item.html b/9 WEEK/item-service/src/main/resources/templates/basic/item.html
new file mode 100644
index 00000000..acb5b463
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/templates/basic/item.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
상품 상세
+
+
+
+
+
+ 상품 ID
+
+
+
+ 상품명
+
+
+
+ 가격
+
+
+
+ 수량
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/main/resources/templates/basic/items.html b/9 WEEK/item-service/src/main/resources/templates/basic/items.html
new file mode 100644
index 00000000..186ccae8
--- /dev/null
+++ b/9 WEEK/item-service/src/main/resources/templates/basic/items.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
상품 목록
+
+
+
+
+
+
+
+
+
+ ID
+ 상품명
+ 가격
+ 수량
+
+
+
+
+ 회원id
+ 상품명
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/9 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java b/9 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java
new file mode 100644
index 00000000..e2ded1be
--- /dev/null
+++ b/9 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package hello.itemservice;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class ItemServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/9 WEEK/item-service/src/test/java/hello/itemservice/domain/item/ItemRepositoryTest.java b/9 WEEK/item-service/src/test/java/hello/itemservice/domain/item/ItemRepositoryTest.java
new file mode 100644
index 00000000..af05ff04
--- /dev/null
+++ b/9 WEEK/item-service/src/test/java/hello/itemservice/domain/item/ItemRepositoryTest.java
@@ -0,0 +1,65 @@
+package hello.itemservice.domain.item;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+
+class ItemRepositoryTest {
+
+ ItemRepository itemRepository = new ItemRepository();
+
+ @AfterEach
+ void afterEach() {
+ itemRepository.clearStore();
+ }
+
+ @Test
+ void save() {
+ //give
+ Item item = new Item("itemA", 10000, 10);
+ //when
+ Item savedItem = itemRepository.save(item);
+
+ //then
+ Item findItem = itemRepository.findById(item.getId());
+ assertThat(findItem).isEqualTo(savedItem);
+ }
+
+ @Test
+ void findAll() {
+ //give
+ Item item1 = new Item("item1", 10000, 10);
+ Item item2 = new Item("item2", 20000, 20);
+
+ itemRepository.save(item1);
+ itemRepository.save(item2);
+ //when
+ List- result = itemRepository.findAll();
+
+ //then
+ assertThat(result.size()).isEqualTo(2);
+ assertThat(result).contains(item1, item2);
+ }
+
+ @Test
+ void update() {
+ //give
+ Item item1 = new Item("item1", 10000, 10);
+ Item savedItem = itemRepository.save(item1);
+ Long itemId = item1.getId();
+
+ //when
+ Item updateParam = new Item("item2", 20000, 20);
+ itemRepository.update(itemId, updateParam);
+
+ //then
+ Item findItem = itemRepository.findById(itemId);
+
+ assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
+ assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
+ assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
+ }
+}
\ No newline at end of file
diff --git a/9 WEEK/servlet/.gitignore b/9 WEEK/servlet/.gitignore
new file mode 100644
index 00000000..c2065bc2
--- /dev/null
+++ b/9 WEEK/servlet/.gitignore
@@ -0,0 +1,37 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
diff --git a/9 WEEK/servlet/SECTION1.md b/9 WEEK/servlet/SECTION1.md
new file mode 100644
index 00000000..4f8c2930
--- /dev/null
+++ b/9 WEEK/servlet/SECTION1.md
@@ -0,0 +1,201 @@
+# 스프링 웹 MVC
+
+## 웹 서버, 웹 애플리케이션 서버
+
+### 모든 것이 HTTP
+HTTP 메시지에 모든 것을 전송
+- HTML, TEXT
+- IMAGE, 음성, 영상, 파일
+- JSON, XML(API)
+- 거의 모든 형태의 데이터 전송 가능
+- 서버간에 데이터를 주고 받을 때도 대부분 HTTP 사용
+- **지금은 HTTP 시대**
+
+### 웹 서버(Web Server)
+- HTTP 기반으로 동작
+- 정적 리소스 제공, 기타 부가기능
+- 정적(파일) HTML, CSS, JS, 이미지, 영상
+- 예) NGINX, APACHE
+
+### 웹 애플리케이션 서버(WAS - Web Application Server)
+- HTTP 기반으로 동작
+- 웹 서버 기능 포함 + (정적 리소스 제공 가능)
+- 프로그램 코드를 실행해서 애플리케이션 로직 수행
+ - 동적 HTML, HTTP API(JSON)
+ - Servlet, Jsp, Spring MVC
+- 예) Tomcat, Jetty, Undertow
+
+### 웹 서버, 웹 애플리케이션 서버(WAS) 차이
+- 웹 서버는 정적 리소스(파일), WAS는 애플리케이션 로직
+- 둘의 용어 경계가 모호함
+ - WS 서버도 프로그램을 실행하는 기능을 포함하기도 함
+ - WAS도 WS의 기능을 제공
+- 자바는 서블릿 컨테이너 기능을 제공하면 WAS
+ - 서블릿 없이 자바코드를 실행하는 서버 프레임워크도 존재
+
+
+### 웹 시스템 구성 - WAS, DB
+- WAS, DB만으로 시스템 구성 가능
+- WAS는 정적 리소스, 애플리케이션 로직 모두 제공 가능
+
+
+- WAS가 너무 많은 역할을 담당 -> 서버 과부하 우려
+- 가장 비싼 애플리케이션 로직이 정적 리소스 때문에 수행이 어려울 수 있음
+- WAS 장애시 오류 화면도 노출 불가
+
+### 웹 시스템 구성 - WEB, WAS, DB
+- 정적 리소스는 웹 서버가 처리
+- 웹 서버는 애플리케이션 로직같은 동적인 처리가 필요하면 WAS에 요청을 위임
+- WAS는 중요한 애플리케이션 로직 처리 전담
+
+>WS = HTML, CSS, JS, 이미지 처리
+>WAS = 애플리케이션 로직 처리
+
+- 효율적인 리소스 관리
+ - 정적 리소스가 많이 사용되면 WS 증설
+ - 애플리케이션 리소스가 많이 사용되면 WAS 증설
+- 정적 리소스만 제공하는 WS는 잘 죽지 않음
+- 애플리캐이션 로직이 동작하는 WAS서버는 잘 죽음
+- WAS, DB 장애시 WEB 서버가 오류 화면 제공 가능
+
+
+## Servlet
+### HTML Form 데이터 전송
+#### POST 전송 - 저장
+
+
+
+### 서버에서 처리해야 하는 업무
+#### 웹 애플리케이션 서버 직접 구현시
+ - 
+#### 서블릿을 지원하는 WAS 사용시
+ - 
+
+### 서블릿
+#### 특징
+```java
+@WebServlet(name = "helloServlet", urlPatterns = "/hello")
+public class HelloServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response){
+ //애플리케이션 로직
+ }
+```
+- urlPatterns(/hello)의 URL 호출되면 서블릿 코드 실행
+- HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest
+- HTTP 응답 정보를 편리하게 제공할 수 있는 HttpServletResponse
+
+
+#### HTTP 요청, 응답 흐름
+- HTTP 요청시
+ - WAS는 Request, Response 객체를 새로 만들어 서블릿 객체 호출
+ - 개발자는 Request객체에서 HTTP요청 정보를 편리하게 꺼내 사용
+ - 개발자는 Response 객체에 HTTP 응답 정보를 편리하게 입력
+ - WAS는 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성
+
+
+#### 서블릿 컨테이너
+
+
+- 톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 함
+- 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기 관리
+- 서블릿 객체는 싱글톤으로 관리
+ - 고객의 요청이 올 때 마다 계속 객체를 생성하는 것은 비효율
+ - 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용
+ - 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근
+ - 공유 변수 사용 주의
+ - 서블릿 컨테이너 종료시 함께 종료
+- JSP도 서블릿으로 변환 되어서 사용
+- 동시 요청을 위한 멀티 스레드 처리 지원
+
+
+## 동시 요청 - 멀티 스레드
+
+### 스레드
+- 애플리케이션 코드를 하나하나 순차적으로 실행하는 것 = 스레드
+- 자바 메인 메서드 첫 실행 시 main이라는 이름의 스레드 실행
+- 스레드가 없다면 자바 애플리케이션 실행 불가
+- 스레드는 한번에 하나의 코드 라인 실행
+- 동시 처리가 필요하면 스레드를 추가로 생성
+
+### 요청 마다 스레드 생성
+
+- 장점
+ - 동시 요청 처리 가능
+ - 리소스(CPU, 메모리)가 허용할 때 까지 처리 가능
+ - 하나의 스레드 지연 돼도, 나머지 스레드 정상 작동
+- 단점
+ - 스레드는 생성 비용이 매우 비쌈
+ - 고객의 요청이 올 때마다 스레드 생성 시, 응답 속도가 느림
+ - 스레드는 컨텍스트 스위칭 비용 발생
+ - 스레드 생성에 제한이 없음.
+ - 고객 요청이 너무 많이 오면, CPU, 메모리 임계점을 넘어 서버가 죽을 수 있다.
+
+### 스레드 풀
+#### 요청 마다 스레드 생성의 단점 보완
+- 특징
+ - 필요한 스레드를 스레드 풀에 보관하고 관리.
+ - 스레드 풀에 생성 가능한 스레드의 최대치를 관리. 톰캣은 최대 200개 기본 설정
+- 사용
+ - 스레드가 필요하면, 이미 생성되어 있는 스레드를 스레드 풀에서 꺼내 사용.
+ - 사용을 종료하면 스레드 풀에 해당 스레드 반납.
+ - 최대 스레드가 모두 사용중이어서 스레드 풀에 스레드가 없다면
+ - 기다리는 요청은 거절하거나 특정 숫자만틈만 대기하도록 설정 가능
+- 장점
+ - 스레드가 미리 생성돼 있어, 스레드를 생성하고 종료하는 비용(CPU)가 절약, 응답 시간이 빠름
+ - 생성 가능한 스레드의 최대치가 있어 너무 많은 요청이 들어와도 기존 요청을 안전하게 처리 가능
+
+### WAS의 멀티 스레드 지원
+- 멀티 스레드에 대한 부분은 WAS가 처리
+- 개발자가 멀티 스레드 관련 코드를 신경쓰지 않아도 됨
+- 개발자는 마치 싱글 스레드 프로그래밍 하듯 편리하게 코드 개발 가능
+- 멀티 스레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈)는 주의해서 사용
+
+
+
+## HTML, HTTP API, CSR, SSR
+
+### 정적 리소스
+- 고정된 HTML 파일, CSS, JS, 이미지, 영상 등을 제공
+- 주로 웹 브라우저
+
+### HTML 페이지
+- 동적으로 필요한 HTML 파일을 생성해 전달
+- 웹 브라우저 : HTML 해석
+
+### HTML API
+- HTML이 아니라 데이터 전달
+- 주로 JSON 형식 사용
+- 다양한 시스템에서 호출
+- 데이터만 주고 받음, UI 화면이 필요하면, 클라이언트가 별도 처리
+- 앱, 웹 클라이언트, 서버 to 서버
+
+
+#### 다양한 시스템 연동
+- 주로 JSON 형태로 데이터 통신
+- UI클라이언트 접점
+ - 앱 클라이언트(아이폰, 안드로이드, PC앱)
+ - 웹 브라우저에서 JS를 통한 HTTP API 호출
+ - React, Vue.js 같은 웹 클라이언트
+- 서버 to 서버
+ - 주문 서버 -> 결제 서버
+ - 기업간 데이터 통신
+
+### 서버 사이드 렌더링, 클라이언트 사이드 렌더링
+#### - SSR - 서버 사이드 렌더링
+ - HTML 최종 결과를 서버에서 만들어 웹 브라우저에 전달
+ - 주로 정적인 화면에 사용
+ - 관련기술 : JSP, 타임리프 -> BE 개발자
+
+
+#### - CSR - 클라이언트 사이드 렌더링
+ - HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해 적용
+ - 주로 동적인 화면에 사용, 웹 환경을 마치 앱 처럼 필요한 부분만 변경할 수 있음
+ - 예) 구글지도, Gmail, 구글 캘린더
+ - 관련기술 : React, Vue.js -> FE 개발자
+
+
+
+- 참고
+ - React, Vue.js를 CSR + SSR 동시에 진원하는 웹 프레임워크 존재
+ - SSR을 사용하더라도 자바스크립트를 사용해 화면 일부를 동적으로 변경 가능
diff --git a/9 WEEK/servlet/SECTION2.md b/9 WEEK/servlet/SECTION2.md
new file mode 100644
index 00000000..17c3e7e9
--- /dev/null
+++ b/9 WEEK/servlet/SECTION2.md
@@ -0,0 +1,826 @@
+# 서블릿
+
+
+## Hello 서블릿
+
+> **참고**
+> 서블릿은 톰캣 같은 WAS 서버를 직접 설치, 그 위에 서블리 ㅅ코드를 클래스 파일로 빌드해서
+> 올린 다음, 톰캣 서버를 실행한다. 이 과정은 매우 번거롭다.
+> 스프링 부트는 톰캣 서버를 내장하고 있으므로, 톰캣 서버 설치 없이 편리하게 서블릿 코드를 실행할 수 있다.
+
+
+### 스프링 부트 서블릿 환경 구성
+
+`@ServletComponentScan`
+스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 `@ServletComponentScan`을 지원한다.
+다음과 같이 추가한다.
+
+*ServletApplication*
+```java
+package hello.servlet;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.ServletComponentScan;
+
+@ServletComponentScan //서블릿 자동 등록
+@SpringBootApplication
+public class ServletApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ServletApplication.class, args);
+ }
+
+}
+```
+
+
+**서블릿 등록하기**
+
+*HelloServlet*
+```java
+package hello.servlet.basic;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "helloServlet", urlPatterns = "/hello")
+public class HelloServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("HelloServlet.service");
+ System.out.println("requset = " + requset);
+ System.out.println("response = " + response);
+
+ String username = requset.getParameter("username");
+ System.out.println("username = " + username);
+
+ response.setContentType("text/plain");
+ response.setCharacterEncoding("utf-8");
+ response.getWriter().write("hello " + username);
+ }
+}
+```
+
+- `@WebServlet` 서블릿 애노테이션
+ - name : 서블릿 이름
+ - urlPatterns : URL매핑
+
+HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행
+`protected void service(HttpServletRequest requset, HttpServletResponse response)`
+
+- 웹 브라우저 실행
+ - http://localhost8080/hello?username=world
+ - 결과 : hello world
+
+- 콘솔 실행결과
+```
+HelloServlet.service
+requset = org.apache.catalina.connector.RequestFacade@42a1a9b6
+response = org.apache.catalina.connector.ResponseFacade@e7a3bb
+username = world
+```
+
+
+### 서블릿 컨테이너 동작 방식
+**내장 톰캣 서버 생성**
+
+
+**HTTP 요청, HTTP응답 메시지**
+
+
+**웹 애플리케이션 서버의 요청 응답 구조**
+
+
+> **참고**
+> HTTP응답에서 Content-Length는 웹 애플리케이션 서버가 자동으로 생성해준다.
+
+
+### welcome 페이지 추가
+
+```html
+
+
+
+
+ Title
+
+
+
+
+
+```
+
+
+## HttpServletRequest - 개요
+
+**HttpServletRequest 역할**
+HTTP 요청 메시지를 개발자가 직접 파싱해도 사용해도 되지만, 매우 불편하다. 서블릿은 개발자가 HTTP 요청 메시지를
+편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다. 그리고 그 결과를 `HttpServletRequest`객체에 담아서 제공한다.
+
+HttpServletRequest를 사용하면 다음과 같은 HTTP 요청 메시지를 편리하게 조회할 수 있다.
+
+*HTTP 요청 메시지*
+```
+POST /save HTTP/1.1
+Host: localhost:8080
+Content-Type: application/x-www-form-urlencoded
+username=kim&age=20
+```
+
+- START LINE
+ - HTTP 메서드
+ - URL
+ - 쿼리 스트링
+ - 스키마, 프로토콜
+- 헤더
+ - 헤더 조회
+- 바디
+ - form 파라미터 형식 조회
+ - message body 데이터 직접 조회
+
+HttpServletRequest 객체는 추가로 여러가지 부가기능도 함께 제공한다.
+
+**임시 저장소 기능**
+- 해당 HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능
+ - 저장 : `request.setAttribute(name, value)`
+ - 조회 : `request.getAttribute(name)`
+
+**세션 관리 기능**
+- `request.getSession(create: true)`
+
+> **중요**
+> HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지,
+> HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 점이다. 따라서 이 기능에 대해 깊이 이해하려면
+> **HTTP 스팩이 제공하는 요청, 응답 메시지 자체를 이해**해야 한다.
+
+
+
+## HttpServletRequest - 기본 사용법
+HttpServletRequest가 제공하는 기본 기능
+
+*RequestHeaderServlet*
+```java
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
+public class RequestHeaderServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ printStartLine(request);
+ printHeaders(request);
+ printHeaderUtils(request);
+ printEtc(request);
+
+ }
+
+ //start line 정보
+ private void printStartLine(HttpServletRequest request) {
+ System.out.println("--- REQUEST-LINE - start ---");
+ System.out.println("request.getMethod() = " + request.getMethod()); //GET
+ System.out.println("request.getProtocol() = " + request.getProtocol()); //HTTP/1.1
+ System.out.println("request.getScheme() = " + request.getScheme()); //http
+ // http://localhost:8080/request-header
+ System.out.println("request.getRequestURL() = " + request.getRequestURL());
+ // /request-header
+ System.out.println("request.getRequestURI() = " + request.getRequestURI());
+ //username=hi
+ System.out.println("request.getQueryString() = " +
+ request.getQueryString());
+ System.out.println("request.isSecure() = " + request.isSecure()); //https 사용유무
+ System.out.println("--- REQUEST-LINE - end ---");
+ System.out.println();
+ }
+
+ //Header 모든 정보
+ private void printHeaders(HttpServletRequest request) {
+ System.out.println("--- Headers - start ---");
+ /*
+ Enumeration headerNames = request.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ System.out.println(headerName + ": " + request.getHeader(headerName));
+ }
+ */
+ request.getHeaderNames().asIterator()
+ .forEachRemaining(headerName -> System.out.println(headerName + ": "
+ + request.getHeader(headerName)));
+ System.out.println("--- Headers - end ---");
+ System.out.println();
+ }
+
+
+ //Header 편리한 조회
+ private void printHeaderUtils(HttpServletRequest request) {
+ System.out.println("--- Header 편의 조회 start ---");
+ System.out.println("[Host 편의 조회]");
+ System.out.println("request.getServerName() = " +
+ request.getServerName()); //Host 헤더
+ System.out.println("request.getServerPort() = " +
+ request.getServerPort()); //Host 헤더
+ System.out.println();
+ System.out.println("[Accept-Language 편의 조회]");
+ request.getLocales().asIterator()
+ .forEachRemaining(locale -> System.out.println("locale = " +
+ locale));
+ System.out.println("request.getLocale() = " + request.getLocale());
+ System.out.println();
+ System.out.println("[cookie 편의 조회]");
+ if (request.getCookies() != null) {
+ for (Cookie cookie : request.getCookies()) {
+ System.out.println(cookie.getName() + ": " + cookie.getValue());
+ }
+ }
+ System.out.println();
+ System.out.println("[Content 편의 조회]");
+ System.out.println("request.getContentType() = " +
+ request.getContentType());
+ System.out.println("request.getContentLength() = " +
+ request.getContentLength());
+ System.out.println("request.getCharacterEncoding() = " +
+ request.getCharacterEncoding());
+ }
+
+ //기타 정보
+ private void printEtc(HttpServletRequest request) {
+ System.out.println("--- 기타 조회 start ---");
+ System.out.println("[Remote 정보]");
+ System.out.println("request.getRemoteHost() = " +
+ request.getRemoteHost()); //
+ System.out.println("request.getRemoteAddr() = " +
+ request.getRemoteAddr()); //
+ System.out.println("request.getRemotePort() = " +
+ request.getRemotePort()); //
+ System.out.println();
+ System.out.println("[Local 정보]");
+ System.out.println("request.getLocalName() = " + request.getLocalName()); //
+ System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); //
+ System.out.println("request.getLocalPort() = " + request.getLocalPort()); //
+ System.out.println("--- 기타 조회 end ---");
+ System.out.println();
+ }
+}
+```
+실행결과
+```
+--- REQUEST-LINE - start ---
+request.getMethod() = GET
+request.getProtocol() = HTTP/1.1
+request.getScheme() = http
+request.getRequestURL() = http://localhost:8080/request-header
+request.getRequestURI() = /request-header
+request.getQueryString() = null
+request.isSecure() = false
+--- REQUEST-LINE - end ---
+
+--- Headers - start ---
+host: localhost:8080
+connection: keep-alive
+cache-control: max-age=0
+sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
+sec-ch-ua-mobile: ?0
+sec-ch-ua-platform: "Windows"
+upgrade-insecure-requests: 1
+user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
+accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
+sec-fetch-site: same-origin
+sec-fetch-mode: navigate
+sec-fetch-user: ?1
+sec-fetch-dest: document
+referer: http://localhost:8080/basic.html
+accept-encoding: gzip, deflate, br
+accept-language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
+cookie: Idea-1afd2dea=715e6892-4a57-4307-afc0-f323498e55fb
+--- Headers - end ---
+
+--- Header 편의 조회 start ---
+[Host 편의 조회]
+request.getServerName() = localhost
+request.getServerPort() = 8080
+
+[Accept-Language 편의 조회]
+locale = ko_KR
+locale = ko
+locale = en_US
+locale = en
+request.getLocale() = ko_KR
+
+[cookie 편의 조회]
+Idea-1afd2dea: 715e6892-4a57-4307-afc0-f323498e55fb
+
+[Content 편의 조회]
+request.getContentType() = null
+request.getContentLength() = -1
+request.getCharacterEncoding() = UTF-8
+--- 기타 조회 start ---
+[Remote 정보]
+request.getRemoteHost() = 0:0:0:0:0:0:0:1
+request.getRemoteAddr() = 0:0:0:0:0:0:0:1
+request.getRemotePort() = 57354
+
+[Local 정보]
+request.getLocalName() = 0:0:0:0:0:0:0:1
+request.getLocalAddr() = 0:0:0:0:0:0:0:1
+request.getLocalPort() = 8080
+--- 기타 조회 end ---
+```
+
+> **참고**
+> 로컬에서 테스트하면 IPv6정보가 나오는데, IPv4 정보를 보고싶다면 다음 옵션을 VM options에 넣어주면 된다.
+> `-Djava.net.preferIPv4Stack=true`
+
+
+## HTTP 요청 데이터 - 개요
+HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법이 있다.
+
+**주로 다음 3가지 방법을 사용**
+- **GET - 쿼리 파라미터**
+ - /url?username=seob&age=24
+ - 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
+ - 예)검색, 필터, 페이징등등에서 많이 사용하는 방식
+- **POST - HTML Form**
+ - content-type : application/x-www-form-urlencoded
+ - 메시지 바디에 쿼리 파라미터 형식으로 전달 username=seob&age=24
+ - 예)회원 가입, 상품 주문, HTML Form 사용
+- **HTTP message body**에 데이터를 직접 담아서 요청
+ - HTTP API에서 주로 사용, JSON, XML, TEXT
+ - 데이터 형식은 주로 JSON에 사용
+ - POST, PUT, PATCH
+
+
+### HTTP 요청 데이터 - GET 쿼리 파라미터
+
+전달 데이터
+- username=hello
+- age=20
+
+메시지 바디 없이, URL의 **쿼리 파라미터**를 사용해서 데이터 전달
+- 예)검색, 필터, 페이징등등에서 많이 사용하는 방식
+
+쿼리 파라미터는 URL에 다음과 같이 `?`를 시작으로 보낼 수 있다. 추가 파라미터는 `&`로 구분
+- http://localhost:8080/request-param?username=hello&age=20
+
+*RequestParamServlet*
+```java
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Enumeration;
+
+@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
+public class RequestParamServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("[전체 파라미터 조회] - start");
+
+// Enumeration parameterNames = requset.getParameterNames();
+ requset.getParameterNames().asIterator().forEachRemaining(
+ paramName -> System.out.println(paramName + "=" + requset.getParameter(paramName))
+ );
+
+ System.out.println("[전체 파라미터 조회] - end");
+ System.out.println();
+
+ System.out.println("[단일 파라미터 조회]");
+ String username= requset.getParameter("username");
+ String age = requset.getParameter("age");
+
+ System.out.println("username = " + username);
+ System.out.println("age = " + age);
+
+ System.out.println("[이름이 같은 복수 파라미터 조회]");
+ String[] usernames = requset.getParameterValues("username");
+ for(String name: usernames) {
+ System.out.println("username = " + name);
+ }
+ }
+}
+```
+실행결과 - 파라미터 전송
+http://localhost:8080/request-param?username=hello&age=20
+```
+[전체 파라미터 조회] - start
+username=hello
+age=20
+[전체 파라미터 조회] - end
+
+[단일 파라미터 조회]
+username = hello
+age = 20
+[이름이 같은 복수 파라미터 조회]
+username = hello
+```
+
+
+실행결과 - 동일 파라미터 전송
+```
+[전체 파라미터 조회] - start
+username=hello
+age=20
+[전체 파라미터 조회] - end
+[단일 파라미터 조회]
+request.getParameter(username) = hello
+request.getParameter(age) = 20
+[이름이 같은 복수 파라미터 조회]
+request.getParameterValues(username)
+username=hello
+username=Lee
+```
+
+파라미터의 값은 하나인데, 값이 중복이면 `request.getParameterValue()` 를 사용해야한다.
+중복일 때 `request.getParameter()`를 사용하면 `request.getParameterValue()`의 첫 번째 값을 반환한다.
+
+
+## HTTP 요청 데이터 - POST HTML FORM
+주로 회원가입, 상품주문 등에서 사용
+
+**특징**
+- content-type : `application/x-www-form-urlencode`
+- 메시지 바디에 쿼리 파라미터 형식으로 데이터를 전달. `username=hello&age=20`
+
+*hello-form.html*
+```html
+
+
+
+
+ Title
+
+
+
+
+
+```
+
+실행결과
+
+
+
+전송버튼 클릭
+
+
+
+POST의 HTML FORM을 전송하면 웹 브라우저는 다음 형식으로 HTTP 메시지를 만든다.
+- **요청 URL** : http://localhost:8080/request-param
+- **content-type** : `application/x-www-form-urlencoded`
+- **message body** : `username=hello&age=20`
+
+`application/x-www-form-urlencoded`형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 같다.
+따라서 **쿼리 파라미터 조회 메서드를 그대로 사용**하면 된다.
+
+`request.getParameter()`는 GET URL 쿼리 파라미터 형식도 지원, POST HTML FORM 형식도 지원한다.
+
+> **참고**
+> content-type은 HTTP메시지 바디에 데이터 형식을 지정.
+> **GET URL 쿼리 파라미터 형식**으로 클라이언트에서 서버로 데이터를 전달할 때는 HTTP 메시지 바디를 사용하지 않기 때문에 content-type이 없다.
+> **POST FHTML Form 형식**으로 데이터를 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 바디에 포함된
+> 데이터가 어떤 형식인지 content-type을 반드시 지정해야 한다. 이렇게 폼으로 데이터를 전송하는 형식을 `application/x-www-form-urlencoded`라 한다.
+
+
+### Postman을 사용한 테스트
+Postman을 사용하면 굳이 HTML form 을 생성하지 않고 테스트 가능하다.
+
+
+
+## HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트
+- **HTTP message body**에 데이터를 직접 담아서 요청
+ - HTTP API에서 주로 사용, JSON, XML, TEXT
+ - 데이터 형식은 주로 JSON사용
+ - POST, PUT, PATCH
+
+
+- 먼저 가장 단순한 텍스트 메시지를 HTTP 메시지에 담아 전송하고 읽기.
+- HTTP 메시지 바디의 데이터를 InputStream을 사용해 직접 읽을 수 있다.
+
+*RequestBodyStringServlet*
+```java
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@WebServlet(name="RequestBodyStringServlet", urlPatterns = "/request-body-string")
+public class RequestBodyStringServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ ServletInputStream inputStream = request.getInputStream();
+ String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
+
+ System.out.println("messageBody = " + messageBody);
+
+ response.getWriter().write("OK");
+ }
+}
+```
+
+Postman 테스트
+
+
+
+문자 전송
+- POST http://localhost:8080/request-body-string
+- content-type: text/plain
+- message body: hello
+
+실행결과
+```
+messageBody = helloooo!
+```
+
+> **참고**
+> inputStream은 byte코드로 반환. byte코드를 우리가 읽을 수 있는 문자(String)로 보려면
+> 문자표(Charset)를 지정해야함. 여기서 UTF_8을 지정
+
+
+
+## HTTP 요청 데이터 - API 메시지 바디 - JSON
+HTTP API에서 주로 사용하는 JSON형식으로 데이터 전달해보기
+
+**JSON 형식 전송**
+- content-type : **application/json**
+- message body : `{"username": "hello","age": 20}`
+- 결과 : `messageBody = {"username":"hello","age":20}`
+
+**JSON 형식 바싱 추가**
+
+*HelloData*
+```java
+package hello.servlet.basic;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class HelloData {
+ private String username;
+ private int age;
+}
+```
+
+*RequestBodyJsonServlet*
+```java
+package hello.servlet.basic.request;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import hello.servlet.basic.HelloData;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
+public class RequestBodyJsonServlet extends HttpServlet {
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ ServletInputStream inputStream = request.getInputStream();
+ String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
+
+ System.out.println("messageBody = " + messageBody);
+
+ HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
+
+ System.out.println("helloData.username = " + helloData.getUsername());
+ System.out.println("helloData.age = " + helloData.getAge());
+
+ response.getWriter().write("ok");
+ }
+}
+
+```
+Postman으로 실행
+
+
+
+실행결과
+```
+messageBody = {"username": "hello","age": 20}
+helloData.username = hello
+helloData.age = 20
+```
+
+> **참고**
+> JSON결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 Jackson, Gson 같은 JSON 변환 라이브러리를 추가해서 사용해야한다.
+> Spring MVC를 선택하면 기본으로 Jackson 라이브러리(ObjectMapper)를 함께 제공한다.
+
+
+> **참고**
+> HTML form 데이터도 메시지 바디를 통해 전송되므로 직접 읽을 수 있다. 하지만 편리한 파라미터 조회기능
+> (request.getParameter(...))을 이미 제공하기 때문에 파라미터 조회 기능을 사용하면 된다.
+
+
+
+## HttpServletResponse - 기본 사용법
+### HttpServletResponse 역할
+
+**HTTP 응답 메시지 생성**
+- HTTP 응답 코드 지정
+- 헤더 생성
+- 바디 생성
+
+**편의 기능 제공**
+- Content-Type, 쿠키, Redirect
+
+
+### HttpServletResponse - 기본 사용법
+
+*ResponseHeaderServlet*
+```java
+package hello.servlet.basic.response;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header")
+public class ResponseHeaderServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ //[status-line]
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ //[response-headers]
+ response.setHeader("Content-Type", "text/plain;charset=utf-8");
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("my-header", "hello");
+
+ //[Header 편의 메서드]
+// content(response);
+// cookie(response);
+// redirect(response);
+
+ //[message body]
+ PrintWriter writer = response.getWriter();
+ writer.println("ok");
+ }
+}
+```
+
+*Content 편의 메서드*
+```java
+ private void content(HttpServletResponse response) {
+ //Content-Type: text/plain;charset=utf-8
+ //Content-Length: 2
+ //response.setHeader("Content-Type", "text/plain;charset=utf-8");
+ response.setContentType("text/plain");
+ response.setCharacterEncoding("utf-8");
+ //response.setContentLength(2); //(생략시 자동 생성)
+ }
+```
+
+*Cookie 편의 메서드*
+```java
+ private void cookie(HttpServletResponse response) {
+ //Set-Cookie: myCookie=good; Max-Age=600;
+ //response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
+ Cookie cookie = new Cookie("myCookie", "good");
+ cookie.setMaxAge(600); //600초
+ response.addCookie(cookie);
+ }
+```
+
+*Redirect 편의 메서드*
+```java
+ private void redirect(HttpServletResponse response) throws IOException {
+ //Status Code 302
+ //Location: /basic/hello-form.html
+ //response.setStatus(HttpServletResponse.SC_FOUND); //302
+ //response.setHeader("Location", "/basic/hello-form.html");
+ response.sendRedirect("/basic/hello-form.html");
+ }
+```
+
+
+## HTTP 응답 데이터 - 단순 텍스트, HTML
+HTTP 응답 메시지는 주로 아래와 같은 내용을 담아 전달
+- 단순 텍스트 응답
+ - `writer.println("ok");`
+- HTML 응답
+- HTML API - MessageBody JSON 응답
+
+### HttpServletResponse - HTML 응답
+
+*ResponseHtmlServlet*
+```java
+package hello.servlet.basic.response;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html")
+public class ResponseHtmlServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ //Content-Type: text/html;charset=utf-8
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+ PrintWriter writer = response.getWriter();
+ writer.println("");
+ writer.println("");
+ writer.println(" HI?
");
+ writer.println("");
+ writer.println("");
+ }
+}
+```
+
+HTTP 응답으로 HTML을 반환할 때 content-type을 `text/html`로 지정해야 한다.
+
+
+## HTTP 응답 데이터 - API JSON
+
+
+*ResponseJsonServlet*
+```java
+package hello.servlet.basic.response;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import hello.servlet.basic.HelloData;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
+public class ResponseJsonServlet extends HttpServlet {
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ response.setContentType("application/json");
+ response.setCharacterEncoding("utf-8");
+
+ HelloData helloData = new HelloData();
+ helloData.setUsername("lee");
+ helloData.setAge(20);
+
+ String result = objectMapper.writeValueAsString(helloData);
+ response.getWriter().write(result);
+ }
+}
+```
+
+HTTP 응답으로 JSON을 반환할 때 content-type을 `application/json`로 지정해야 한다.
+Jackson 라이브러리가 제공하는 `objectMapper.writeValueAsString()`를 사용하면 객체를 JSON 문자로 변경할 수 있다.
+
+
diff --git a/9 WEEK/servlet/SECTION3.md b/9 WEEK/servlet/SECTION3.md
new file mode 100644
index 00000000..5f56ad9a
--- /dev/null
+++ b/9 WEEK/servlet/SECTION3.md
@@ -0,0 +1,803 @@
+# 3. 서블릿, JSP, MVC 패턴
+
+
+## 회원 관리 웹 애플리케이션 요구사항
+
+회원정보 - 이름, 나이
+기능 요구사항 - 회원 저장, 회원 목록 조회
+
+*Member*
+```java
+package hello.servlet.domain.member;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class Member {
+
+ private Long id;
+ private String username;
+ private int age;
+
+ public Member(String username, int age) {
+ this.username = username;
+ this.age = age;
+ }
+}
+```
+
+
+*MemberRepository*
+```java
+package hello.servlet.domain.member;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MemberRepository {
+
+ private static Map store = new HashMap<>();
+ private static long sequence = 0L;
+
+ //싱글톤
+ private static final MemberRepository instance = new MemberRepository();
+
+ public static MemberRepository getInstance() {
+ return instance;
+ }
+ private MemberRepository(){}
+
+ public Member save(Member member) {
+ member.setId(++sequence);
+ store.put(member.getId(), member);
+ return member;
+ }
+
+ public Member findById(Long id) {
+ return store.get(id);
+ }
+
+ public List findAll() {
+ return new ArrayList<>(store.values());
+ }
+
+ public void clearStore() {
+ store.clear();
+ }
+}
+```
+
+싱글톤 패턴 적용. 싱글톤 패턴은 객체를 단 하나만 생성해서 공유해야 하므로 생성자를 private 접근자로 막아둔다.
+
+
+*MemberRepositoryTest*
+```java
+package hello.servlet.domain.member;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class MemberRepositoryTest {
+
+ MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @AfterEach
+ void afterEach() {
+ memberRepository.clearStore();
+ }
+
+ @Test
+ void save() {
+ Member member = new Member("Hello", 20);
+
+ Member savedMember = memberRepository.save(member);
+
+ Member findMember = memberRepository.findById(savedMember.getId());
+ Assertions.assertThat(findMember).isEqualTo(savedMember);
+
+ }
+
+ @Test
+ void findAll() {
+ Member member1 = new Member("Hello1", 20);
+ Member member2 = new Member("Hello2", 20);
+
+ memberRepository.save(member1);
+ memberRepository.save(member2);
+
+ List memberList = memberRepository.findAll();
+
+ Assertions.assertThat(memberList.size()).isEqualTo(2);
+ Assertions.assertThat(memberList).contains(member1, member2);
+ }
+
+
+}
+```
+
+
+## 서블릿으로 회원 관리 웹 애플리케이션 만들기
+
+*MemberFormServlet - 회원 등록 폼*
+```java
+package hello.servlet.web.servlet;
+
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
+public class MemberFormServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+
+ PrintWriter w = response.getWriter();
+
+ w.write("\n" +
+ "\n" +
+ "\n" +
+ " \n" +
+ " Title \n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n"); }
+}
+```
+
+
+*MemberSaveServlet - 회원 저장*
+```java
+package hello.servlet.web.servlet;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
+public class MemberSaveServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("MemberSaveServlet.service");
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+ PrintWriter w = response.getWriter();
+
+ w.write("\n" +
+ "\n" +
+ " \n" +
+ "\n" +
+ "\n" +
+ "성공\n" +
+ "\n" +
+ " id="+member.getId()+" \n" +
+ " username="+member.getUsername()+" \n" +
+ " age="+member.getAge()+" \n" +
+ " \n" +
+ "메인 \n" +
+ "\n" +
+ "");
+ }
+}
+```
+
+- `MemberSaveServlet` 동작 순서
+ 1. 파라미터를 조회해 Member 객체 생성
+ 2. Member 객체를 MemberRepository를 통해 저장.
+ 3. Member 객체를 사용해 결과 화면용 HTML을 동적으로 만들어 응답.
+
+
+
+*MemberListServlet - 회원 목록*
+```java
+package hello.servlet.web.servlet;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
+public class MemberListServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+
+ List members = memberRepository.findAll();
+
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+
+ PrintWriter w = response.getWriter();
+ w.write("");
+ w.write("");
+ w.write(" ");
+ w.write(" Title ");
+ w.write("");
+ w.write("");
+ w.write("메인 ");
+ w.write("");
+ w.write(" ");
+ w.write(" id ");
+ w.write(" username ");
+ w.write(" age ");
+ w.write(" ");
+ w.write(" ");
+/*
+ w.write(" ");
+ w.write(" 1 ");
+ w.write(" userA ");
+ w.write(" 10 ");
+ w.write(" ");
+*/
+ for (Member member : members) {
+ w.write(" ");
+ w.write(" " + member.getId() + " ");
+ w.write(" " + member.getUsername() + " ");
+ w.write(" " + member.getAge() + " ");
+ w.write(" ");
+ }
+ w.write(" ");
+ w.write("
");
+ w.write("");
+ w.write("");
+ }
+}
+```
+
+- `MemberListServlet` 동작 순서
+ 1. `memberRepository.findAll()`을 통해 모든 회원을 조회.
+ 2. 회원 목록 HTML을 for 루프를 통해 회원 수 만큼 동적으로 생성하고 응답.
+
+
+
+### Welcome 페이지 변경
+
+*index.html*
+```html
+
+
+
+
+ Title
+
+
+
+ 서블릿 basic
+ 서블릿
+
+
+ JSP
+
+
+ 서블릿 MVC
+
+
+ FrontController - v1
+
+
+ FrontController - v2
+
+
+ FrontController - v3
+
+
+ FrontController - v4
+
+
+ FrontController - v5 - v3
+
+
+ FrontController - v5 - v4
+
+
+ SpringMVC - v1
+
+
+ SpringMVC - v2
+
+
+ SpringMVC - v3
+
+
+
+
+
+```
+
+
+
+
+## JSP로 회원 관리 웹 애플리케이션 만들기
+
+### JSP 라이브러리 추가
+
+*build.gradle*
+```groovy
+ //JSP 추가 시작
+implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
+implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
+implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트3.0 이상
+implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
+//JSP 추가 끝
+```
+
+*new-form.jsp - 회원 등록 폼*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+ Title
+
+
+
+
+
+```
+
+- `<%@ page contentType="text/html;charset=UTF-8" language="java" %>`
+ - 첫 줄은 JSP 문서라는 뜻.
+
+*save.jsp - 회원 저장*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="hello.servlet.domain.member.Member" %>
+<%@ page import="hello.servlet.domain.member.MemberRepository" %>
+<%
+ MemberRepository memberRepository = MemberRepository.getInstance();
+ //request, response는 지원함
+ System.out.println("MemberSaveServlet.service");
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+%>
+
+
+ Title
+
+
+ 성공
+
+ id=<%=member.getId()%>
+ username=<%=member.getUsername()%>
+ age=<%=member.getAge()%>
+
+ 메인
+
+
+```
+
+- '<%@ page import= %>'
+ - 자바의 import 문과 동일.
+- '<% %>'
+ - 이 부분에 자바 코드 입력 가능.
+- `<%= %>`
+ - 이 부분에 자바 코드 출력 가능.
+
+회원 저장 JSP는 회원 저장 servlet 코드와 같다. 다른 점은, HTML을 중심으로 하고,
+자바 코드를 부분부분 입력해 주었다는 점이다. `<% %>`를 사용해 HTML 중간에 자바 코드를 출력하고 있다.
+
+*members.jsp - 회원 목록*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="hello.servlet.domain.member.Member" %>
+<%@ page import="java.util.List" %>
+<%@ page import="hello.servlet.domain.member.MemberRepository" %>
+<%
+ MemberRepository memberRepository = MemberRepository.getInstance();
+
+ List members = memberRepository.findAll();
+%>
+
+
+
+ Title
+
+
+메인
+
+
+ id
+ username
+ age
+
+
+ <%
+ for (Member member : members) {
+ out.write(" ");
+ out.write(" " + member.getId() + " ");
+ out.write(" " + member.getUsername() + " ");
+ out.write(" " + member.getAge() + " ");
+ out.write(" ");
+ }
+ %>
+
+
+
+
+```
+
+회원 repository를 먼저 조회, 결과 List를 사용해 중간에 ``HTML 태그를 반복해서 출력하고 있다.
+
+
+### 서블릿과 JSP의 한계
+서블릿으로 개발할 때는 뷰(view)화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여 복잡했다.
+JSP를 사용한 덕분에 뷰를 생성하는 HTML 작업을 깔끔하게 정리하고, 중간중간 동적으로 변경이 필요한 부분에만
+자바 코드를 적용했다. 하지만 몇 가지 문제점이 존재한다.
+
+회원 저장 폼에서 코드의 상위 절반은 회원을 저장하는 비지니스 로직, 나머지 절반은 결과를 보여주는 HTML 뷰 영역이다. 회원 목록도 마찬가지이다.
+JAVA코드, 데이터를 조회하는 repository 등등 다양한 코드가 모두 JSP에 노출돼 있다. 만약 수백, 수천줄이 넘어간다면 유지보수에 큰 어려움이 발생한다.
+
+
+**MVC 패턴 등장**
+ 비지니스 로직은 서블릿 처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(VIEW)을 보여주는 일에 집중하도록 하게 해준다.
+
+
+
+
+## MVC 패턴 -적용
+서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해 MVC 패턴 적용
+ Model은 HttpServletRequest 객체 사용. request는 내부에 데이터 저장소를 가지고 있는데,
+'request.setAttribute()', `request.getAttribute`를 사용하면 데이터를 보관하고, 조회할 수 있다.
+
+
+### 회원 등록
+
+*MvcMemberFormServlet - 회원 등록 폼 컨트롤러*
+```java
+package hello.servlet.web.servletmvc;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
+public class MvcMemberFormServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String viewPath = "/WEB-INF/views/new-form.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+
+ }
+}
+```
+
+`dispatcher.forward()` : 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 다시 호출 발생
+
+> `/WEB-INF`
+> 이 경로 안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 즉 항상 컨트롤러를 통해서 JSP를 호출하는 것이다.
+
+> **redirect vs forward**
+> 리다이렉트는 실제 클라이언트(web)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다.
+> 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서
+> 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.
+
+
+*new-form.jsp - 회원 등록 폼 뷰*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+
+ Title
+
+
+
+
+
+
+```
+
+여기서 form의 action이 상대경로로 지정돼 있다. 이렇게 상대경로로 지정하면 폼 전송시
+현재 URL이 속한 계층 경로 + save가 호출된다.
+현재 계층 경로 : `/servlet-mvc/members/`
+결과 : `/servlet-mvc/members/save`
+
+
+### 회원 저장
+
+*MvcMemberSaveServlet - 회원 저장 컨트롤러*
+```java
+package hello.servlet.web.servletmvc;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
+public class MvcMemberSaveServlet extends HttpServlet {
+
+ MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+
+ //Model에 데이터 보관.
+ request.setAttribute("member", member);
+
+ String viewPath = "/WEB-INF/views/save-result.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+HttpServletRequest를 Model로 사용.
+request가 제공하는 setAttribute()를 사용하면 request 객체에 데이터를 보관해 뷰에 전달할 수 있다.
+뷰는 request.getAttribute()를 사용해 데이터를 꺼내면 된다.
+
+
+*save-result - 회원저장 뷰*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+
+
+
+성공
+
+ id=${member.id}
+ username=${member.username}
+ age=${member.age}
+
+메인
+
+
+```
+
+JSP는 `${}` 문법을 제공. 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다.
+
+
+### 회원 목록 조회
+
+*MvcMemberListServlet - 회원 목록 조회 컨트롤러*
+```java
+package hello.servlet.web.servletmvc;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
+public class MvcMemberListServlet extends HttpServlet {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+
+ request.setAttribute("members", members);
+
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+
+request 객체를 사용해 `List members`를 모델에 보관한다.
+
+
+*members - 회원 목록 조회 뷰*
+```html
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+
+
+
+ Title
+
+
+메인
+
+
+ id
+ username
+ age
+
+
+
+
+ ${item.id}
+ ${item.username}
+ ${item.age}
+
+
+
+
+
+
+```
+모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해 반복해서 출력한다.
+`memebers`리스트에서 `members`를 순서대로 꺼내 `item`변수에 담고, 출력하는 과정을 반복한다.
+
+``이 기능을 사용하려면 다음과 같이 선언해야 한다.
+`<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>`
+
+
+
+## MVC 패턴 - 한계
+MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다.
+뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다. 단순하게 모델에서 필요한 데이터를 수집하고, 화면을 만들면 된다.
+
+하지만 컨트롤러는 중복코드가 많고, 필요하지 않은 코드가 많이 존재한다.
+
+
+**MVC 컨트롤러의 단점**
+
+*foward 중복*
+view로 이동하는 코드가 항상 중복 호출된다.
+```
+RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+dispatcher.forward(request, response);
+```
+
+
+**ViewPath 중복**
+```java
+String viewPath = "/WEB-INF/views/save-result.jsp";
+```
+
+- prefix : `/WEB-INF/views/`
+- suffix : `.jsp`
+그리고 만약 jsp가 아닌 thymeleaf같은 뷰로 변경한다면 전체 코드를 다 변경 작업을 해야 한다.
+
+
+**사용하지 않는 코드**
+다음 코드를 사용할 때도 있고, 사용하지 않을 때도 있다.
+```
+HttpServletRequest request, HttpServletResponse response
+```
+그리고 이런 `HttpServletRequest`, `HttpServletResponse`를 사용하는 코드는 테스트 케이스를 작성하기 어렵다.
+
+
+**공통 처리 어려움**
+기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가한다. 단순히 공통 기능을 메서드로
+생성하면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 될 수도 있다. 그리고 호출하는 것 자체도 중복이다.
+
+
+**정리!**
+이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다. 소위 **수문장 역할**을 하는 기능이 필요하다.
+**Front Controller**패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.(입구를 하나로)
+스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/9 WEEK/servlet/SECTION4.md b/9 WEEK/servlet/SECTION4.md
new file mode 100644
index 00000000..f27d814e
--- /dev/null
+++ b/9 WEEK/servlet/SECTION4.md
@@ -0,0 +1,1251 @@
+# 4. MVC 프레임워크 만들기
+
+---
+
+## 프론트 컨트롤러 패턴
+
+**프론트 컨트롤러 도입 전**
+
+
+**프론트 컨트롤러 도입 후**
+
+
+
+**FrontController 패턴 특징**
+- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
+- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아 호출
+- 입구를 하나로
+- 공통 처리 가능
+- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
+
+
+**스프링 웹MVC와 프론트 컨트롤러**
+- 스프링 웹 MVC의 핵심도 바로 **FrontController**
+- 스프링 웹 MVC의 **DispatcherServlet**이 FrontController 패턴으로 구현되어 있다.
+
+
+
+## 프론트 컨트롤러 도입 - v1
+
+V1 구조
+
+
+
+*ControllerV1*
+```java
+package hello.servlet.web.frontcontroller.v1;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface ControllerV1 {
+ void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
+}
+```
+
+서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입.
+각 컨트롤러들은 이 인터페이스를 구현
+프론트 컨트롤러는 이 인터페이스를 호출해 구현과 관계 없이 로직의 일관성을 가질 수 있음.
+
+
+*MemberFormControllerV1 - 회원 등록 컨트롤러*
+```java
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberFormControllerV1 implements ControllerV1 {
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String viewPath = "/WEB-INF/views/new-form.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+
+*MemberListControllerV1 - 회원 목록 컨트롤러*
+```java
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MemberListControllerV1 implements ControllerV1 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+
+ request.setAttribute("members", members);
+
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+내부 로직은 기존 서블릿과 비슷
+
+
+*FrontControllerServletV1 - 프론트 컨트롤러*
+```java
+package hello.servlet.web.frontcontroller.v1;
+
+import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
+import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
+import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
+public class FrontControllerServiceV1 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV1() {
+ controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
+ controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
+ controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV1 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ controller.process(request, response);
+ }
+}
+```
+
+**프론트 컨트롤러 분석**
+
+**urlPatterns**
+- `urlPatterns = "/front-controller/v1/*"` : `/front-controller/v1/*`를 포함한 하위 모든 요청은
+이 서블릿에서 받아들인다.
+- Ex) `/front-controller/v1`, `/front-cotroller/v1/a`, `/front-controller/v1/a/b`
+
+
+**controllerMap**
+- key : 매핑 URL
+- value : 호출된 컨트롤러
+
+
+**service()**
+먼저 `requestURI`를 조회해 실제 호출할 컨트롤러를 controllerMap에서 찾는다. 없다면 404 상태 코드 반환한다.
+ 컨트롤러를 찾고 `controller.process(request, response);`을 호출해서 해당 컨트롤러를 실행한다.
+
+
+**JSP**
+JSP는 이전 MVC에서 사용했던 파일을 그대로 사용
+
+
+
+**기존 서블릿, JSP로 만든 MVC와 동일하게 실행된다.**
+
+
+
+
+
+## View 분리 - v2
+모든 컨트롤러에서 뷰로 이동한느 부분에 중복 발생, 깔끔하지 않음
+```
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+```
+
+이를 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만든다.
+
+*V2 구조*
+
+
+이전 V1 구조에서는 controller에서 jsp를 호출했다면 V2는 view를 반환해 FrontController에서 실행하게 한다.
+
+
+*MyView*
+```java
+package hello.servlet.web.frontcontroller;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MyView {
+ private String viewPath;
+
+ public MyView(String viewPath) {
+ this.viewPath = viewPath;
+ }
+
+ public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
+```
+
+*ControllerV2*
+```java
+package hello.servlet.web.frontcontroller.v2;
+
+import hello.servlet.web.frontcontroller.MyView;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface ControllerV2 {
+ MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
+}
+```
+
+
+*MemberFormControllerV2 - 회원 등록 폼*
+```java
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberFormControllerV2 implements ControllerV2 {
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ return new MyView("/WEB-INF/views/new-form.jsp");
+ }
+}
+
+```
+
+각 컨트롤러는 복잡한 dispatcher.forward()를 직접 생성해 호출하지 않아도 됨.
+단순히 MyView 객체를 생성, 거기에 뷰 이름만 넣고 반환하면 됨.
+
+ControllerV1을 구현한 클래스와 ControllerV2를 구현한 클래스를 비교하면, 중복이 확실하게 제거됨을 확인 가능함
+
+*MemberSaveControllerV2 - 회원 저장*
+```java
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberSaveControllerV2 implements ControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ request.setAttribute("member", member);
+ return new MyView("/WEB-INF/views/save-result.jsp");
+ }
+}
+```
+
+
+*MemberListControllerV2 - 회원 목록*
+```java
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MemberListControllerV2 implements ControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+ request.setAttribute("members", members);
+ return new MyView("/WEB-INF/views/members.jsp");
+ }
+}
+```
+
+*FrontControllerV2*
+```java
+package hello.servlet.web.frontcontroller.v2;
+
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
+public class FrontControllerServiceV2 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV2() {
+ controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
+ controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
+ controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV2 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ MyView view = controller.process(request, response);
+ view.render(request, response);
+ }
+}
+```
+
+ControllerV2의 반환 타입이 `MyView`이므로 프론트 컨트롤러는 컨트롤러의 호출 결과를 `MyView`를 반환 받는다.
+그리고 `view.render()`를 호출하면 `forward` 로직을 수행해 JSP가 실행된다.
+
+`MyView.render()`
+```java
+ public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+```
+
+
+
+
+## Model 추가 - v3
+**서블릿 종속 제거**
+컨트롤러 입장에서 HttpServletRequest, HttpServletResponse가 꼭 필요할까?
+요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서 컨트롤러가 서블릿 기술을 몰라도 동작 할 수 있다.
+또한 request 객체를 Model로 사용하는 대신 별도의 Model 객체를 만들어 반환하면 된다.
+구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않고 변경 ->>
+
+**뷰 이름 종속 제거**
+컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다.
+컨트롤러의 *뷰의 논리 이름*을 반환, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화.
+이렇게 하면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다.
+
+- `/WEB-INF/views/new-form.jsp` -> **new-form**
+- `/WEB-INF/views/save-result.jsp` -> **save-result**
+- `/WEB-INF/views/members.jsp` -> **members**
+
+
+**V3 구조**
+
+
+
+**Model View**
+지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다. 그리고 Model도 `request.setAttribute()`를 통해 데이터를 저장, 뷰에 전달했다.
+ 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View이름가지 전달하는 객체를 만들어본다.
+ 참고로 `ModelView`객체는 다른 버전에서도 사용하므로 패키지를 `frontController`에 둔다.
+
+
+*ModelView*
+```java
+package hello.servlet.web.frontcontroller;
+
+import java.util.HashMap;
+import java.util.Map;
+public class ModelView {
+ private String viewName;
+ private Map model = new HashMap<>();
+ public ModelView(String viewName) {
+ this.viewName = viewName;
+ }
+ public String getViewName() {
+ return viewName;
+ }
+ public void setViewName(String viewName) {
+ this.viewName = viewName;
+ }
+ public Map getModel() {
+ return model;
+ }
+ public void setModel(Map model) {
+ this.model = model;
+ }
+}
+```
+뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있음. model은 단순히 map으로 되어 있어 컨트롤러에서 뷰에 필요한 데이터를
+key, value로 넣어주면 됨.
+
+
+*ControllerV3*
+```java
+package hello.servlet.web.frontcontroller.v3;
+
+import hello.servlet.web.frontcontroller.ModelView;
+
+import java.util.Map;
+
+public interface ControllerV3 {
+ ModelView process(Map paramMap);
+}
+```
+이 컨트롤러는 서블릿 기술을 사용하지 않음. 따라서 구현이 단순해지고, 테스트 코드를 작성하기 쉬움
+HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아 호출하면 됨
+응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 됨.
+
+
+*MemberFormControllerV3 - 회원 등록 폼*
+```java
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.Map;
+
+public class MemberFormControllerV3 implements ControllerV3 {
+ @Override
+ public ModelView process(Map paramMap) {
+ return new ModelView("new-form");
+ }
+}
+```
+`ModelView`를 생성할 때 `new-form`이라는 view의 논리적인 이름을 지정. 실제 물리적은 이름은 프론트 컨트롤러에서 처리한다.
+
+
+*MemberSaveControllerV3 - 회원 저장*
+```java
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.Map;
+
+public class MemberSaveControllerV3 implements ControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public ModelView process(Map paramMap) {
+ String username = paramMap.get("username");
+ int age = Integer.parseInt(paramMap.get("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ ModelView mv = new ModelView("save-result");
+ mv.getModel().put("member", member);
+ return mv;
+ }
+}
+```
+
+- `paramMap.get("username");`
+ - 파라미터 정보는 map에 담겨있음. map에서 필요한 요청 파라미터를 조회하면 됨.
+- `mv.getModel().put("member", member);`
+ - 모델은 단순한 map이므로 모델에 뷰에서 필요한 `member`객체를 담고 반환
+
+
+*MemberListControllerV3 - 회원 목록*
+```java
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.List;
+import java.util.Map;
+
+public class MemberListControllerV3 implements ControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public ModelView process(Map paramMap) {
+ List members = memberRepository.findAll();
+ ModelView mv = new ModelView("members");
+ mv.getModel().put("members", members);
+
+ return mv;
+
+ }
+
+}
+```
+
+*FrontControllerServletV3*
+```java
+package hello.servlet.web.frontcontroller.v3;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
+public class FrontControllerServiceV3 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV3() {
+ controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
+ controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
+ controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV3 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ String viewName = mv.getViewName();
+ MyView view = viewResolver(viewName);
+ view.render(mv.getModel(), request, response);
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
+```
+
+
+`view.render(mv.getModel(), request, response)`코드에서 컴파일 오류가 발생함. 다음 코드를 참고해 MyView 객체에 필요한 메서드 추가.
+
+`createParamMap()`
+HttpServletRequest에서 파라미터 정보를 꺼내 Map으로 변환. 그리고 해당 Map(`paramMap`)을 컨트롤러에 전달하면서 호출
+
+
+**View Resolver**
+`MyView view = viewResolver(viewName)`
+컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리 경로가 있는 MyView 객체를 반환
+한다.
+- 논리 뷰 이름 : `members`
+- 물리 뷰 경로 : `/WEB-INF/views/members.jsp`
+
+
+
+`view.render(mv.getModel(), request, response)`
+- 뷰 객체를 통해서 HTML 화면을 렌더링 한다.
+- 뷰 객체의 render() 는 모델 정보도 함께 받는다.
+- JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둔다.
+- JSP로 포워드 해서 JSP를 렌더링 한다.
+
+
+*MyView*
+```java
+package hello.servlet.web.frontcontroller;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class MyView {
+ private String viewPath;
+
+ public MyView(String viewPath) {
+ this.viewPath = viewPath;
+ }
+ public void render(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+ public void render(Map model, HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ modelToRequestAttribute(model, request);
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+ private void modelToRequestAttribute(Map model,
+ HttpServletRequest request) {
+ model.forEach((key, value) -> request.setAttribute(key, value));
+ }
+}
+```
+
+
+
+## 단순하고 실용적인 컨트롤러 - v4
+앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다.
+하지만 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 항상 ModelView 객체를 생성하고 반환해야하는 번거러움이 존재한다.
+ 좋은 프레임 워크는 아키텍쳐도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 즉 실용성이 있어야 한다.
+
+
+v4는 v3를 약간 변경해 실제 구현하는 개발자들이 매우 편리하게 개발할 수 있도록 한다.
+
+
+**V4 구조**
+
+- 기본적인 구조는 V3와 같다. 대신에 컨트롤러가 ModelView를 반환하지 않고, ViewName만 반환한다.
+
+
+*ControllerV4*
+```java
+package hello.servlet.web.frontcontroller.v4;
+
+import java.util.Map;
+
+public interface ControllerV4 {
+ String process(Map paramMap, Map model);
+}
+```
+이번 V4는 interface에 ModelView가 없다. model 객체는 파라미터로 전달되기 때문에 그냥 사용하면 되고, 결과로 뷰의 이름만 반환하면 된다.
+
+
+*MemberFormControllerV4*
+```java
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.Map;
+
+public class MemberFormControllerV4 implements ControllerV4 {
+ @Override
+ public String process(Map paramMap, Map model) {
+ return "new-form";
+ }
+}
+```
+
+
+*MemberSaveControllerV4*
+```java
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.Map;
+
+public class MemberSaveController implements ControllerV4 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public String process(Map paramMap, Map model) {
+ String username = paramMap.get("username");
+ int age = Integer.parseInt(paramMap.get("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ model.put("member", member);
+
+ return "save-result";
+ }
+}
+```
+`model.put("member", member)` : 모델이 파라미터로 전달되기 때문에 모델을 직접 생성하지 않아도 된다.
+
+
+
+*MemberListControllerV4*
+```java
+package hello.servlet.web.frontcontroller.v4.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+
+import java.util.List;
+import java.util.Map;
+
+public class MemberListControllerV4 implements ControllerV4 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public String process(Map paramMap, Map model) {
+ List members = memberRepository.findAll();
+
+ model.put("members", members);
+ return "members";
+ }
+}
+```
+
+
+*FrontControllerServletV4*
+```java
+package hello.servlet.web.frontcontroller.v4;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
+public class FrontControllerServiceV4 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV4() {
+ controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
+ controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
+ controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV4 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+
+ String viewName = controller.process(paramMap, model);
+
+ MyView view = viewResolver(viewName);
+ view.render(model, request, response);
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
+```
+`FrontControllerServletV4`는 이전 버전과 거의 동일
+
+
+**모델 객체 전달**
+`Map model = new HashMap<>();`
+모델 객체를 프론트 컨트롤러에서 생성해 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.
+
+
+**뷰의 논리 이름을 직접 반환**
+```java
+String viewName = controller.process(paramMap, model);
+MyView view = viewResolver(viewName);
+```
+컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해 실제 물리 뷰를 찾을 수 있다.
+
+**정리**
+V4는 매우 단순하고 실용적이다. 기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어를 적용했을 뿐인데, 컨트롤러를 구현하는
+개발자 입장에서 군더더기 없는 코드를 작성할 수 있다.
+
+
+
+
+
+## 유연한 컨트롤러1 - V5
+만약 `ControllerV3` or `ControllerV4` 방식으로 다양한 컨트롤러를 사용해 개발하고 싶다면 **어댑터 패턴을**
+을 사용해야 한다.
+
+**어댑터 패턴**
+지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있었다.
+`ControllerV3`, `ControllerV4`는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다. 마치 v3는 110v, v4는 220v 전기 콘셉트 같은 것이다.
+ 어댑터 패턴을 사용해 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경한다.
+
+**V5 구조**
+
+- **핸들러 어댑터** : 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름은 어댑터 핸들러이다.
+여기서 어댑터 역할을 해주기 때문에 다양한 종료의 컨트롤러를 호출할 수 있다.
+- **핸들러** : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어뎁터가 있기 때문이다.
+꼭 컨트롤러의 개념 뿐 아니라 어떠한 것이든 해당하는 종료의 어댑터만 있으면 다 처리할 수 있기 때문이다.
+
+
+*MyHandlerAdapter* 인터페이스
+```java
+package hello.servlet.web.frontcontroller.v5;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface MyHandlerAdapter {
+ boolean supports(Object handler);
+
+ ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
+}
+```
+- `boolean supports(Object handler)`
+ - handler는 컨트롤러를 말함
+ - 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 Method.
+- `ModelView handle(HttpServletRequest request, HttpServletResponse response, Object Handler)`
+ - 어댑터는 실제 컨트롤러를 호출, 그 결과로 ModelView를 반환해야 함.
+ - 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 함.
+ - 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해 실제 컨트롤러가 호출됨.
+
+
+실제 ControllerV3를 지원하는 어댑터를 구현
+*ControllerV3HandlerAdapter*
+```java
+package hello.servlet.web.frontcontroller.v5.adapter;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
+ @Override
+ public boolean supports(Object handler) {
+ return (handler instanceof ControllerV3);
+ }
+ @Override
+ public ModelView handle(HttpServletRequest request, HttpServletResponse
+ response, Object handler) {
+ ControllerV3 controller = (ControllerV3) handler;
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ return mv;
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+}
+```
+
+
+
+```java
+public boolean supports(Object handler) {
+ return (handler instanceof ControllerV3);
+}
+```
+`ControllerV3`를 처리할 수 있는 어댑터.
+
+
+```java
+public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+ ControllerV3 controller = (ControllerV3) handler;
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ return mv;
+}
+```
+handler를 컨트롤러 V3로 변환한 다음 V3 형식에 맞추도록 호출.
+`support()`를 통해 `ControllerV3`만 지원하기 때문에 타입 변환은 걱정없이 실행해도 됨.
+ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 됨.
+
+
+
+*FrontControllerServletV5*
+```java
+package hello.servlet.web.frontcontroller.v5;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/ v5/*")
+public class FrontControllerServletV5 extends HttpServlet {
+ private final Map handlerMappingMap = new HashMap<>();
+ private final List handlerAdapters = new ArrayList<>();
+ public FrontControllerServletV5() {
+ initHandlerMappingMap();
+ initHandlerAdapters();
+ }
+ private void initHandlerMappingMap() {
+ handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
+ }
+ private void initHandlerAdapters() {
+ handlerAdapters.add(new ControllerV3HandlerAdapter());
+ }
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ Object handler = getHandler(request);
+ if (handler == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ MyHandlerAdapter adapter = getHandlerAdapter(handler);
+ ModelView mv = adapter.handle(request, response, handler);
+ MyView view = viewResolver(mv.getViewName());
+ view.render(mv.getModel(), request, response);
+ }
+ private Object getHandler(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ return handlerMappingMap.get(requestURI);
+ }
+ private MyHandlerAdapter getHandlerAdapter(Object handler) {
+ for (MyHandlerAdapter adapter : handlerAdapters) {
+ if (adapter.supports(handler)) {
+ return adapter;
+ }
+ }
+ throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
+```
+
+**컨트롤러(Controller) -> 핸들러(Handler)**
+이전에는 컨트롤러를 직접 매핑해서 사용했다. 이젠느 어댑터를 사용하기 때문에, 컨트롤러 뿐 아니라 어댑터가 지원하기만 하면,
+어떤 것이라도 URL에 매핑해 사용할 수 있다. 그래서 이름을 컨트롤러에서 더 넓은 범위의 핸들러로 변경했다.
+
+
+**생성자**
+```java
+ public FrontControllerServletV5() {
+ initHandlerMappingMap();
+ initHandlerAdapters();
+ }
+```
+생성자는 핸들러 매핑과 어댑터를 초기화(등록)함
+
+
+**매핑 정보**
+`private final Map handlerMappingMap = new HashMap<>();`
+
+매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변
+경되었다
+
+
+**핸들러 매핑**
+`Object handler = getHandler(request);`
+```java
+ private Object getHandler(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ return handlerMappingMap.get(requestURI);
+ }
+```
+핸들러 매핑 정보인 `handlerMappingMap`에서 URL에 매핑된 핸들러(컨트롤러)객체를 찾아 반환
+
+
+
+**핸들러를 처리할 수 있는 어댑터 조회**
+`MyHandlerAdapter adapter = getHandlerAdapter(handler)`
+```
+for (MyHandlerAdapter adapter : handlerAdapters) {
+ if (adapter.supports(handler)) {
+ return adapter;
+ }
+}
+```
+
+`handler`를 처리활 수 있는 어댑터를 `adapter.supports(handler)`를 통해 찾음.
+handler가 `ControllerV3` 인터페이스를 구현했다면, `ControllerV3HandlerAdapter()`객체가 반환됨.
+
+
+**어댑터 호출**
+`ModelView mv = adapter.handler(request, response, handler);`
+
+어댑터의 `handler(request, response, handler)`메서드를 통해 실제 어댑터가 호출됨.
+어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환.
+`ControllerV3HandlerAdapter`의 경우 어댑터의 모양과 컨트롤러의 모양이 유사해 변환 로직이 단순함
+
+
+
+## 유연한 컨트롤러2 - v5
+
+*FrontControllerServletV5* - `Controller4` 기능 추가
+```java
+package hello.servlet.web.frontcontroller.v5;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
+import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
+import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
+public class FrontControllerServletV5 extends HttpServlet {
+ private final Map handlerMappingMap = new HashMap<>();
+ private final List handlerAdapters = new ArrayList<>();
+ public FrontControllerServletV5() {
+ initHandlerMappingMap();
+ initHandlerAdapters();
+ }
+ private void initHandlerMappingMap() {
+ handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
+ handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
+
+ handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
+ handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
+ handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
+ }
+
+ private void initHandlerAdapters() {
+ handlerAdapters.add(new ControllerV3HandlerAdapter());
+ handlerAdapters.add(new ControllerV4HandlerAdapter());
+ }
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ Object handler = getHandler(request);
+ if (handler == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ MyHandlerAdapter adapter = getHandlerAdapter(handler);
+ ModelView mv = adapter.handle(request, response, handler);
+ MyView view = viewResolver(mv.getViewName());
+ view.render(mv.getModel(), request, response);
+ }
+ private Object getHandler(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ return handlerMappingMap.get(requestURI);
+ }
+ private MyHandlerAdapter getHandlerAdapter(Object handler) {
+ for (MyHandlerAdapter adapter : handlerAdapters) {
+ if (adapter.supports(handler)) {
+ return adapter;
+ }
+ }
+ throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
+```
+
+`handlerMappingMap`에 `ControllerV4`를 사용하는 컨트롤러를 추가, 해당 컨트롤러를 처리할 수 있는 어댑터인 `ControllerV4HandlerAdapter`도 추가
+
+
+*ControllerV4HandlerAdapter*
+```java
+package hello.servlet.web.frontcontroller.v5.adapter;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v4.ControllerV4;
+import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
+ @Override
+ public boolean supports(Object handler) {
+ return (handler instanceof ControllerV4);
+ }
+
+ @Override
+ public ModelView handle(HttpServletRequest request, HttpServletResponse
+ response, Object handler) {
+ ControllerV4 controller = (ControllerV4) handler;
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+ String viewName = controller.process(paramMap, model);
+
+ ModelView mv = new ModelView(viewName);
+ mv.setModel(model);
+
+ return mv;
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+}
+```
+
+분석
+```java
+ public boolean supports(Object handler) {
+ return (handler instanceof ControllerV4);
+ }
+```
+`handler`가 `ControllerV4`인 경우에만 처리하는 어댑터.
+
+실행로직
+```java
+ ControllerV4 controller = (ControllerV4) handler;
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+ String viewName = controller.process(paramMap, model);
+```
+handler를 ControllerV4로 케스팅, paramMap, model을 만들어 해당 컨트롤러를 호출한다.
+그리고 viewName을 반환 받는다.
+
+
+**어댑터 반환**
+```
+ ModelView mv = new ModelView(viewName);
+ mv.setModel(model);
+
+ return mv;
+```
+어댑터에서 이 부분이 중요한 부분임
+
+어댑터가 호출하는 `ControllerV4`는 뷰의 이름알 반환. 그런데 어댑터는 뷰의 이름이 아니라 `ModelView`를 만들어 반환해야함
+여기서 어댑터가 꼭 필요한 이유가 나옴.
+`ControllerV4`는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞춰 반환함.
+마치 110v 콘센트를 220v 콘센트로 변경하듯.
+
+
+**ControllerV4 & Adapter**
+```java
+public interface ControllerV4 {
+ String process(Map paramMap, Map model);
+}
+public interface MyHandlerAdapter {
+ ModelView handle(HttpServletRequest request, HttpServletResponse response,
+Object handler) throws ServletException, IOException;
+}
+```
+
+
+
+
+---
\ No newline at end of file
diff --git a/9 WEEK/servlet/SECTION5.md b/9 WEEK/servlet/SECTION5.md
new file mode 100644
index 00000000..3d909129
--- /dev/null
+++ b/9 WEEK/servlet/SECTION5.md
@@ -0,0 +1,634 @@
+# 5. 스프링 MVC - 구조 이해
+
+---
+
+## 스프링 MVC 전체 구조
+
+**직접 만든 MVC 프레임워크 구조**
+
+
+**Spring MVC 구조**
+
+
+
+**직접 만든 프레임워크 -> 스프링 MVC 비교**
+- FrontController -> DispatcherServlet
+- handlerMappingMap -> HandlerMapping
+- MyHandlerAdapter -> HandlerAdapter
+- viewResolver -> ViewResolver
+- MyView -> View
+
+
+### DispatcherServlet 구조 살펴보기
+
+`org.springframewokr.web.servlet.DispatcherServlet`
+
+스프링 MVC도 프론트 컨트롤러 패턴으로 구현됨.
+스프링 MVC의 프론트 컨트롤러가 바로 DispatcherServlet.
+그리고 이 디스패처 서빌릇이 바로 스프링 MVC의 핵심.
+
+
+**DispatcherServlet 서블릿 등록**
+- `DispatcherServlet`도 부모 클래스에서 `HttpServlet`을 상속 받아 사용하고, 서블릿으로 동작한다.
+ - DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
+- 스프링 부트는 `DispatcherServlet`을 서블릿으로 자동으로 등록하면서 *모든 경로*(`urlPatterns="/"`)에 대해 매핑한다.
+ - 참고 : 더 자세한 경로가 우선순위가 높음. 그래서 기존에 등록한 서블릿도 함께 동작
+
+**요청 흐름**
+- 서블릿이 호출되면 `HttpServlet`이 제공하는 `service()`가 호출된다.
+- 스프링 MVC는 `DispatcherServlet`의 부모인 `FrameworkServlet`에서 `service()`를 오버라이드 해두었다.
+- `FrameworkServlet.service()`를 시작으로 여러 메서드가 호출되면서 `DispatcherSerlvet.doDispatch()`가 호출된다.
+
+
+
+
+
+**Spring MVC 구조**
+
+
+*동작 순서*
+1. **핸들러 조회** : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
+2. **핸들러 어댑터 조회** : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
+3. **핸들러 어댑터 실행** : 핸들러 어댑터를 실행한다.
+4. **핸들러 실행** : 핸들러 어댑터가 실제 핸들러를 실행한다.
+5. **ModelAndView 반환** : 핸들러 어댑터는 핸들러가 반환하는 정보를 `ModelAndView`로 **변환**
+해서 반환한다.
+6. **viewResolver 호출** : 뷰 리졸버를 찾고 실행한다.
+ - JSP의 경우 : `interfaceResourceViewResolver`가 자동 등록되고 사용된다.
+7. **View반환** : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 런데링 역할을 담당하는 뷰 객체를 반환한다.
+ - JSP의 경우 : `interfaceResourceView(JstView)`를 반환하는데, 내부에 `forward()` 로직이 있다.
+8. *viewRendering* : 뷰를 통해 뷰를 렌더링 한다.
+
+
+**인터페이스 살펴보기**
+- 스프링 MVC의 큰 강점은 `DispatcherServlet` 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다는 점이다.
+지금까지 설명한 대부분을 확장 가능할 수 있게 인터페이스로 제공한다.
+- 이 인터페이스들만 구현해 `DispatcherServlet`에 등록하면 자신만의 컨트롤러를 만들 수 있다.
+
+
+
+
+
+**주요 인터페이스 목록**
+- 핸들러 매핑: `org.springframework.web.servlet.HandlerMapping`
+- 핸들러 어댑터: `org.springframework.web.servlet.HandlerAdapter`
+- 뷰 리졸버: `org.springframework.web.servlet.ViewResolver`
+- 뷰: `org.springframework.web.servlet.View`
+
+
+
+**정리**
+스프링 MVC는 코드 분량도 매우 많고, 복잡해서 내부 구조를 다 파악하는 것은 쉽지 않음.
+사실 해당 기능을 직접 확장 하거나 나만의 컨트롤러를 만드는 일은 없으므로 걱정하지 않아도 됨.
+왜냐하면 스프링 MVC는 전세계 수 많은 개발자들의 요구사항에 맞추어 기능을 계속 확장해왔고,
+그래서 웹 애플리케이션을 만들 때 필요로 하는 대부분의 기능이 이미 다 구현되어 있음
+그래도 이렇게 핵심 동작방식을 알아두어야 향후 문제가 발생했을 때 어떤 부분에서 문제가 발생했는지 쉽게 파악하고,
+문제를 해결할 수 있음.
+그리고 확장 포인트가 필요할 때, 어떤 부분을 확장해야 할지 감을 잡을 수 있음.
+
+
+
+
+
+## 핸들러 매핑과 핸들러 어댑터
+
+### Controller 인터페이스
+
+**과거 버전 스프링 컨트롤러**
+`org.springframework.web.servlet.mvc.Controller`
+```java
+public interface Controller {
+ ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse
+ response) throws Exception;
+}
+```
+
+> **참고**
+> `Controller`인터페이스는 `@Controller`애노테이션과 전혀 다르다.
+
+
+
+*OldController*
+```java
+package hello.servlet.web.springmvc.old;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+
+@Component("/springmvc/old-controller")
+public class OldController implements Controller {
+ @Override
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ System.out.println("OldController.handleRequest");
+ return null;
+ }
+}
+```
+- `@Component` : 이 컨트롤러는 `/springmvc/old-controller`라는 이름의 스프링 빈으로 등록되었다.
+- **빈의 이름으로 URL을 매핑**할 것
+
+
+이 컨트롤러가 호출되려면 2가지가 필요하다.
+- **HandlerMapping(핸들러 매핑)**
+ - 핸들러 매핑에사 이 컨트롤을 찾을 수 있어야 한다.
+ - 예) **스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑**이 필요하다.
+- **HandlerAdapter(핸들러 어댑터)**
+ - 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
+ - 예) `Controller` 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
+
+스프링은 이미 필요한 핸들러 매핑과 어댑터를 대부분 구현해 두었다.개발자가 직접 핸들러 매핑과 핸들러 어댑터를 만드는 일은 거의 없다.
+
+
+
+**스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터**
+
+**HandlerMapping**
+```
+0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
+1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
+```
+
+**HandlerAdapter**
+```
+0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
+1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
+2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
+```
+핸들러 매핑, 핸들러 어댑터도 모두 순서대로 찾고, 없으면 다음 순서로 넘어감.
+
+
+**1. 핸들러 매핑으로 핸들러 조회**
+1. `HandlerMapping`을 순서대로 실행해 핸들러를 찾음
+2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는
+`BeanNameUrlHandlerMapping`가 실행에 성공하고 핸들러인 `OldController`를 반환.
+
+
+**2. 핸들러 어댑터 조회**
+1. `HandlerAdapter`의 `supports()`를 순서대로 호출.
+2. `SimpleControllerHandlerAdapter`가 `Controller` 인터페이스를 지원하는 대상이 됨.
+
+
+**3. 핸들러 어댑터 실행**
+1. 디스패처 서블릿이 조회한 `SimpleControllerHandlerAdapter`를 실행하면서 핸들러 정보도 함께 넘겨줌
+2. `SimpleControllerHandlerAdapter` 는 핸들러인 `OldController` 를 내부에서 실행하고,
+그 결과를 반환.
+
+
+> 정리 - OldController 핸들러매핑, 어댑터
+> `OldController` 를 실행하면서 사용된 객체는 다음과 같다.
+> `HandlerMapping = BeanNameUrlHandlerMapping`
+> `HandlerAdapter = SimpleControllerHandlerAdapte`
+
+
+
+### HttpRequestHandler
+`HttpRequestHandler`핸들러(컨트롤러)는 **서블릿과 가장 유사한 형태**의 핸들러.
+
+*MyHttpRequestHandler*
+```java
+package hello.servlet.web.springmvc.old;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.HttpRequestHandler;
+
+import java.io.IOException;
+
+@Component("/springmvc/request-handler")
+public class MyHttpRequestHandler implements HttpRequestHandler {
+ @Override
+ public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("MyHttpRequestHandler.handleRequest");
+ }
+}
+```
+
+**1. 핸들러 매핑으로 핸들러 조회**
+1. `HandlerMapping` 을 순서대로 실행해서, 핸들러를 찾는다.
+2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는
+`BeanNameUrlHandlerMapping` 가 실행에 성공하고 핸들러인 `MyHttpRequestHandler` 를 반환한다.
+
+**2. 핸들러 어댑터 조회**
+1. `HandlerAdapter` 의 `supports()` 를 순서대로 호출한다.
+2. `HttpRequestHandlerAdapter` 가 `HttpRequestHandler` 인터페이스를 지원하므로 대상이 된다.
+
+**3. 핸들러 어댑터 실행**
+1. 디스패처 서블릿이 조회한 HttpRequestHandlerAdapter 를 실행하면서 핸들러 정보도 함께 넘겨준다.
+2. HttpRequestHandlerAdapter 는 핸들러인 MyHttpRequestHandler 를 내부에서 실행하고,
+그 결과를 반환한다.
+
+
+> **정리 - MyHttpRequestHandler 핸들러매핑, 어댑터**
+> `MyHttpRequestHandler` 를 실행하면서 사용된 객체는 다음과 같다.
+> `HandlerMapping = BeanNameUrlHandlerMapping`
+> `HandlerAdapter = HttpRequestHandlerAdapter`
+
+
+**@RequestMapping**
+가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는
+`RequestMappingHandlerMapping`,
+`RequestMappingHandlerAdapter` 이다.
+@RequestMapping 의 앞글자를 따서 만든 이름인데, 이것이 바로 지금 스프링에서 주로 사용하는 애노테이션 기반의
+컨트롤러를 지원하는 매핑과 어댑터이다.
+
+
+## 뷰 리졸버
+
+**OldController - View 조회할 수 있도록 변경**
+```java
+package hello.servlet.web.springmvc.old;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+
+@Component("/springmvc/old-controller")
+public class OldController implements Controller {
+ @Override
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ System.out.println("OldController.handleRequest");
+ return new ModelAndView("new-form");
+ }
+}
+```
+View를 사용할 수 있도록 `return new ModelAndView("new-form");` 를 추가함.
+또한 `application.properties`에 다음 코드를 추가한다.
+```
+spring.mvc.view.prefix=/WEB-INF/views/
+spring.mvc.view.suffix=.jsp
+```
+
+**뷰 리졸버 - InternalResourceViewResolver**
+스프링부트는 `InternalResourceViewResolver`라느 뷰 리졸버를 자동으로 등록, 이때
+`application.properties`에 등록한 `spring.mvc.view.prefix=/WEB-INF/views/`,
+`spring.mvc.view.suffix=.jsp` 정보를 사용해 등록함.
+
+ 아래의 방법도 가능하다. 다만 권장하지는 않는다.
+```
+return new ModelAndView("/WEB-INF/views/new-form.jsp");
+```
+
+
+### 뷰 리졸버 동작 방식
+
+**Spring MVC 구조**
+
+
+**스프링 부트가 자동 등록하는 뷰 리졸버**
+```
+1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능
+에 사용)
+2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
+```
+
+
+**1. 핸들러 어댑터 호출**
+- 핸들러 어댑터를 통해 `new-form`이라는 논리 뷰 이름을 획득.
+
+**2. ViewResolver 호출**
+- `new-form`이라는 뷰 이름으로 viewResolver를 순서대로 호출
+- `BeanNameViewResolver`는 `new-form`이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없음.
+- `InternalResourceViewResolver`가 호출됨.
+
+**3. InternalResourceViewResolver**
+- 이 뷰 리졸버는 `InternalResourceView`를 반환
+
+**4. 뷰 - InternalResourceView**
+- `InternalResourceView`는 JSP처럼 포워드`forward()`를 호출해 처리할 수 있는 경우에 사용
+
+**5. view.render()**
+- `view.render()`가 호출, `InternalResourceView`는 `forward()`를 사용해 JPS를 실행
+
+
+> **참고**
+> `InternalResourceViewResolver`는 만약 JSTL 라이브러리가 있으면 `InternalResourceView`를 상속받은
+> `JstlView`를 반환. `JstlView`는 JSTL태그 사용시 야간의 부가 기능이 추가됨.
+
+> **참고**
+> 다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 `forward()` 통해서 해당 JSP로 이동(실행)해야 렌더링이 됨.
+> JSP를 제외한 나머지 뷰 템플릿들은 `forward()`과정 없이 바로 렌더링 됨.
+
+> **참고**
+> Thymeleaf 뷰 템플릿을 사용하면 `ThymeleafViewResolver` 를 등록해야 함.
+> 최근에는 라이브러리만 추가하면 스프링 부트가 이런 작업을 모두 자동화해줌.
+
+
+## 스프링 MVC - 시작하기
+
+**@RequestMapping**
+스프링은 애노테이션을 활용한 매우 유연하고, 실용적인 컨트롤러를 만들었는데 이것이 바로
+`@RequestMapping`애노테이션을 사용하는 컨트롤러임.
+
+`@RequestMapping`
+- `RequestMappingHandlerMapping`
+- `RequestMappingHandlerAdapter`
+
+가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 위와 같다.
+`@RequestMapping`의 앞글자를 따서 만든 이름인데, 이것이 바로 지금 스프링에서 주로 사용하는
+애노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터이다. **실무에서는 99.9% 이 방식의 컨트롤러르 사용**한다.
+
+
+지금까지 만들었던 프레임워크에서 사용했던 컨트롤러를 `@RequestMapping`기반의 스프링 MVC컨트롤러로 변경한다.
+
+
+
+*SpringMemberFormControllerV1 - 회원 등록 폼*
+```java
+package hello.servlet.web.springmvc.v1;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+@Controller
+public class SpringMemverFormControllerV1 {
+
+ @RequestMapping("/springmvc/v1/members/new-form")
+ public ModelAndView process() {
+ return new ModelAndView("new-form");
+ }
+}
+```
+- `@Controller`
+ - 스프링이 자동으로 스프링 빈으로 등록. (내부에 `@Component` 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨.)
+ - 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식.
+- `@RequestMapping` : 요청 정보를 매핑. 해당 URL이 호출되면 이 메서드가 호출됨.
+애노테이션을 기반으로 동작하기 때문에 메서드의 이름은 임의로 지으면 된다.
+- `ModelAndView` : 모델과 뷰의 정보를 담아 반환하면 됨.
+
+
+
+`RequestMappingHandlerMapping`은 스프링 빈 중에서 `@RequestMapping` or `@Controller`가 클래스
+레벨에 붙어 있는 경우 매핑 정보로 인식.
+따라서 아래의 코드도 같은 동작을 수행함
+```java
+@Component //컴포넌트 스캔을 통해 스프링 빈으로 등록
+@RequestMapping
+public class SpringMemberFormControllerV1 {
+ @RequestMapping("/springmvc/v1/members/new-form")
+ public ModelAndView process() {
+ return new ModelAndView("new-form");
+ }
+}
+```
+물론 `@Component`이 없어도 동작한다.
+
+
+
+
+> **주의! - 스프링 3.0 이상 **
+> 스프링 부트 3.0(스프링 프레임워크 6.0)부터는 클래스 레벨에 @RequestMapping 이 있어도 스프링 컨트롤러로 인
+> 식하지 않는다. 오직 @Controller 가 있어야 스프링 컨트롤러로 인식한다. 참고로 @RestController 는 해당 애
+> 노테이션 내부에 @Controller 를 포함하고 있으므로 인식 된다. 따라서 @Controller 가 없는 위의 두 코드는 스
+> 프링 컨트롤러로 인식되지 않는다. ( RequestMappingHandlerMapping 에서 @RequestMapping 는
+> 이제 인식하지 않고, Controller 만 인식한다.)
+
+
+*SpringMemberSaveControllerV1 - 회원 저장*
+```java
+package hello.servlet.web.springmvc.v1;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.util.Map;
+
+@Controller
+public class SpringMemberSaveControllerV1 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @RequestMapping("/springmvc/v1/members/save")
+ public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ ModelAndView mv = new ModelAndView("save-result");
+ mv.addObject("member", member);
+ return mv;
+ }
+}
+```
+- `mv.addObject("member", member)`
+ - 스프링이 제공하는 `ModelAndView`를 통해 Model 데이터를 추가할 때 `addObject()`를 사용하면 됨.
+ 이 데이터는 이후 뷰를 렌더링할 때 사용된다.
+
+
+*SpringMemberListControllerV1 - 회원 목록*
+```java
+package hello.servlet.web.springmvc.v1;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.util.List;
+
+@Controller
+public class SpringMemberListControllerV1 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @RequestMapping("/springmvc/v1/members")
+ public ModelAndView process() {
+ List members = memberRepository.findAll();
+ ModelAndView mv = new ModelAndView("members");
+ mv.addObject("members", members);
+
+ return mv;
+
+ }
+}
+```
+
+
+
+## 스프링 MVC - 컨트롤러 통합
+`@RequestMapping`을 잘 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다.
+따라서 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.
+
+
+*SpringMemberControllerV2*
+```java
+package hello.servlet.web.springmvc.v2;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.util.List;
+
+@Controller
+@RequestMapping("/springmvc/v2/members")
+public class SpringMemberControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @RequestMapping("/new-form")
+ public ModelAndView newForm() {
+ return new ModelAndView("new-form");
+ }
+
+
+ @RequestMapping()
+ public ModelAndView save() {
+ List members = memberRepository.findAll();
+ ModelAndView mv = new ModelAndView("members");
+ mv.addObject("members", members);
+
+ return mv;
+
+ }
+
+
+ @RequestMapping("/save")
+ public ModelAndView members(HttpServletRequest request, HttpServletResponse response) {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ ModelAndView mv = new ModelAndView("save-result");
+ mv.addObject("member", member);
+ return mv;
+ }
+}
+```
+
+**조합**
+컨트롤러 클래스를 통합하는 것을 넘어 조합도 가능.
+`/springmvc/v2/members`가 중복이 있었다.
+- `@RequestMapping("/springmvc/v2/members/new-form")`
+- `@RequestMapping("/springmvc/v2/members")`
+- `@RequestMapping("/springmvc/v2/members/save")`
+
+클래스 레벨에서 `@RequestMapping`을 두면 메서드 레벨과 조합된다.
+```java
+@Controller
+@RequestMapping("/springmvc/v2/members")
+public class SpringMemberControllerV2{}
+```
+**조합 결과**
+- `@RequestMapping("/springmvc/v2/members/new-form")` ->
+- `@RequestMapping("/springmvc/v2/members")` ->
+- `@RequestMapping("/springmvc/v2/members/save")` ->
+
+| 조합 전 | 조합 후 |
+|------|--------------------------------|
+| `@RequestMapping("/springmvc/v2/members/new-form")` | `@RequestMapping("/new-form")` |
+| `@RequestMapping("/springmvc/v2/members")` | `@RequestMapping("")` |
+| `@RequestMapping("/springmvc/v2/members/save")` | `@RequestMapping("/save")` |
+
+
+
+
+## 스프링 MVC - 실용적인 방법
+MVC 프레임워크 만들기에서 V3는 ModelView를 개발자가 직접 생성해 반환했기 떄문에 불편했다.
+
+**다음은 실무에서 사용하는 방식이다.**
+
+*SpringMemberControllerV3*
+```java
+package hello.servlet.web.springmvc.v3;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@Controller
+@RequestMapping("/springmvc/v3/members")
+public class SpringMemberControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @GetMapping("/new-form")
+ public String newForm() {
+ return "new-form";
+ }
+
+
+ @PostMapping("/save")
+ public String save(@RequestParam("username") String username, @RequestParam("age") int age, Model model) {
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+ model.addAttribute("member", member);
+ return "save-result";
+ }
+
+
+ @GetMapping("")
+ public String members(Model model) {
+ List members = memberRepository.findAll();
+ model.addAttribute("members", members);
+ return "members";
+ }
+
+
+}
+```
+
+**Model 파라미터**
+`save()`, `members()`를 보면 Model을 파라미터로 받는 것을 확인할 수 있다. 스프링 MVC도 이런 편의 기능을 제공한다.
+
+
+**ViewName 직접 반환**
+뷰의 논리 이름을 반환한다.
+
+
+**@RequestParam**
+스프링은 HTTP 요청 파라미터를 `@RequsetParam`으로 받을 수 있다.
+`@RequestParam("username")`은 `request.getParameter("username")`과 거의 같은 코드이다.
+ GET쿼리 파라미터, POST Form 방식을 모두 지원한다.
+
+
+**@RequestMapping @GetMapping, @PostMapping**
+`@RequestMapping` 은 URL만 매칭하는 것이 아니라, HTTP Method도 함께 구분할 수 있다.
+예를 들어서 URL이 `/new-form` 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면 다음과 같이
+처리하면 된다.
+```
+@RequestMapping(value = "/new-form", method = RequestMethod.GET)
+```
+이것을 @GetMapping , @PostMapping 으로 더 편리하게 사용할 수 있다.
+참고로 Get, Post, Put, Delete, Patch 모두 애노테이션이 준비되어 있다.
+
+
+
+
+
+
+
+
+
+
+
+
+---
\ No newline at end of file
diff --git a/9 WEEK/servlet/build.gradle b/9 WEEK/servlet/build.gradle
new file mode 100644
index 00000000..de959ed2
--- /dev/null
+++ b/9 WEEK/servlet/build.gradle
@@ -0,0 +1,43 @@
+plugins {
+ id 'java'
+ id 'war'
+ id 'org.springframework.boot' version '3.2.1'
+ id 'io.spring.dependency-management' version '1.1.4'
+}
+
+group = 'hello'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ sourceCompatibility = '17'
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+
+ //JSP 추가 시작
+ implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
+ implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
+ implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트3.0 이상
+ implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
+ //JSP 추가 끝
+
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar b/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..d64cd491
Binary files /dev/null and b/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties b/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..1af9e093
--- /dev/null
+++ b/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/9 WEEK/servlet/gradlew b/9 WEEK/servlet/gradlew
new file mode 100644
index 00000000..1aa94a42
--- /dev/null
+++ b/9 WEEK/servlet/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/9 WEEK/servlet/gradlew.bat b/9 WEEK/servlet/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/9 WEEK/servlet/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/9 WEEK/servlet/img/S1-1.png b/9 WEEK/servlet/img/S1-1.png
new file mode 100644
index 00000000..b4382b9b
Binary files /dev/null and b/9 WEEK/servlet/img/S1-1.png differ
diff --git a/9 WEEK/servlet/img/S1-2.png b/9 WEEK/servlet/img/S1-2.png
new file mode 100644
index 00000000..ff31b6a6
Binary files /dev/null and b/9 WEEK/servlet/img/S1-2.png differ
diff --git a/9 WEEK/servlet/img/S1-3.png b/9 WEEK/servlet/img/S1-3.png
new file mode 100644
index 00000000..d0a945c4
Binary files /dev/null and b/9 WEEK/servlet/img/S1-3.png differ
diff --git a/9 WEEK/servlet/img/S1-4.png b/9 WEEK/servlet/img/S1-4.png
new file mode 100644
index 00000000..b2538fe4
Binary files /dev/null and b/9 WEEK/servlet/img/S1-4.png differ
diff --git a/9 WEEK/servlet/img/S1-5.png b/9 WEEK/servlet/img/S1-5.png
new file mode 100644
index 00000000..496dd994
Binary files /dev/null and b/9 WEEK/servlet/img/S1-5.png differ
diff --git a/9 WEEK/servlet/img/S1-6.png b/9 WEEK/servlet/img/S1-6.png
new file mode 100644
index 00000000..0f147271
Binary files /dev/null and b/9 WEEK/servlet/img/S1-6.png differ
diff --git a/9 WEEK/servlet/img/S1-7.png b/9 WEEK/servlet/img/S1-7.png
new file mode 100644
index 00000000..cc6ffda9
Binary files /dev/null and b/9 WEEK/servlet/img/S1-7.png differ
diff --git a/9 WEEK/servlet/img/S1-8.png b/9 WEEK/servlet/img/S1-8.png
new file mode 100644
index 00000000..dafb1dc7
Binary files /dev/null and b/9 WEEK/servlet/img/S1-8.png differ
diff --git a/9 WEEK/servlet/img/S2-1.png b/9 WEEK/servlet/img/S2-1.png
new file mode 100644
index 00000000..2f468be2
Binary files /dev/null and b/9 WEEK/servlet/img/S2-1.png differ
diff --git a/9 WEEK/servlet/img/S2-2.png b/9 WEEK/servlet/img/S2-2.png
new file mode 100644
index 00000000..c2d94f77
Binary files /dev/null and b/9 WEEK/servlet/img/S2-2.png differ
diff --git a/9 WEEK/servlet/img/S2-3.png b/9 WEEK/servlet/img/S2-3.png
new file mode 100644
index 00000000..15d29abf
Binary files /dev/null and b/9 WEEK/servlet/img/S2-3.png differ
diff --git a/9 WEEK/servlet/img/S2-4.png b/9 WEEK/servlet/img/S2-4.png
new file mode 100644
index 00000000..a6ce3e89
Binary files /dev/null and b/9 WEEK/servlet/img/S2-4.png differ
diff --git a/9 WEEK/servlet/img/S2-5.png b/9 WEEK/servlet/img/S2-5.png
new file mode 100644
index 00000000..5c762ffa
Binary files /dev/null and b/9 WEEK/servlet/img/S2-5.png differ
diff --git a/9 WEEK/servlet/img/S2-6.png b/9 WEEK/servlet/img/S2-6.png
new file mode 100644
index 00000000..c5924ca5
Binary files /dev/null and b/9 WEEK/servlet/img/S2-6.png differ
diff --git a/9 WEEK/servlet/img/S2-7.png b/9 WEEK/servlet/img/S2-7.png
new file mode 100644
index 00000000..3948838e
Binary files /dev/null and b/9 WEEK/servlet/img/S2-7.png differ
diff --git a/9 WEEK/servlet/img/S2-8.png b/9 WEEK/servlet/img/S2-8.png
new file mode 100644
index 00000000..76f40bbd
Binary files /dev/null and b/9 WEEK/servlet/img/S2-8.png differ
diff --git a/9 WEEK/servlet/img/S4-1.png b/9 WEEK/servlet/img/S4-1.png
new file mode 100644
index 00000000..3944b4aa
Binary files /dev/null and b/9 WEEK/servlet/img/S4-1.png differ
diff --git a/9 WEEK/servlet/img/S4-2.png b/9 WEEK/servlet/img/S4-2.png
new file mode 100644
index 00000000..6f55a519
Binary files /dev/null and b/9 WEEK/servlet/img/S4-2.png differ
diff --git a/9 WEEK/servlet/img/S4-3.png b/9 WEEK/servlet/img/S4-3.png
new file mode 100644
index 00000000..d907f0d0
Binary files /dev/null and b/9 WEEK/servlet/img/S4-3.png differ
diff --git a/9 WEEK/servlet/img/S4-4.png b/9 WEEK/servlet/img/S4-4.png
new file mode 100644
index 00000000..bd12948f
Binary files /dev/null and b/9 WEEK/servlet/img/S4-4.png differ
diff --git a/9 WEEK/servlet/img/S4-5.png b/9 WEEK/servlet/img/S4-5.png
new file mode 100644
index 00000000..beb9c402
Binary files /dev/null and b/9 WEEK/servlet/img/S4-5.png differ
diff --git a/9 WEEK/servlet/img/S4-6.png b/9 WEEK/servlet/img/S4-6.png
new file mode 100644
index 00000000..f2539420
Binary files /dev/null and b/9 WEEK/servlet/img/S4-6.png differ
diff --git a/9 WEEK/servlet/img/S4-7.png b/9 WEEK/servlet/img/S4-7.png
new file mode 100644
index 00000000..c805b5b0
Binary files /dev/null and b/9 WEEK/servlet/img/S4-7.png differ
diff --git a/9 WEEK/servlet/img/S5-1.png b/9 WEEK/servlet/img/S5-1.png
new file mode 100644
index 00000000..b560216c
Binary files /dev/null and b/9 WEEK/servlet/img/S5-1.png differ
diff --git a/9 WEEK/servlet/img/S5-2.png b/9 WEEK/servlet/img/S5-2.png
new file mode 100644
index 00000000..5f9beda2
Binary files /dev/null and b/9 WEEK/servlet/img/S5-2.png differ
diff --git a/9 WEEK/servlet/settings.gradle b/9 WEEK/servlet/settings.gradle
new file mode 100644
index 00000000..9c5e4d1a
--- /dev/null
+++ b/9 WEEK/servlet/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'servlet'
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java b/9 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java
new file mode 100644
index 00000000..a1c772de
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java
@@ -0,0 +1,15 @@
+package hello.servlet;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.ServletComponentScan;
+
+@ServletComponentScan //서블릿 자동 등록
+@SpringBootApplication
+public class ServletApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ServletApplication.class, args);
+ }
+
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java b/9 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java
new file mode 100644
index 00000000..057e157f
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java
@@ -0,0 +1,13 @@
+package hello.servlet;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+public class ServletInitializer extends SpringBootServletInitializer {
+
+ @Override
+ protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+ return application.sources(ServletApplication.class);
+ }
+
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java
new file mode 100644
index 00000000..c514bc33
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java
@@ -0,0 +1,11 @@
+package hello.servlet.basic;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class HelloData {
+ private String username;
+ private int age;
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java
new file mode 100644
index 00000000..4a128f82
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java
@@ -0,0 +1,26 @@
+package hello.servlet.basic;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "helloServlet", urlPatterns = "/hello")
+public class HelloServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("HelloServlet.service");
+ System.out.println("requset = " + requset);
+ System.out.println("response = " + response);
+
+ String username = requset.getParameter("username");
+ System.out.println("username = " + username);
+
+ response.setContentType("text/plain");
+ response.setCharacterEncoding("utf-8");
+ response.getWriter().write("hello " + username);
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java
new file mode 100644
index 00000000..3bb95632
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java
@@ -0,0 +1,35 @@
+package hello.servlet.basic.request;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import hello.servlet.basic.HelloData;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
+public class RequestBodyJsonServlet extends HttpServlet {
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ ServletInputStream inputStream = request.getInputStream();
+ String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
+
+ System.out.println("messageBody = " + messageBody);
+
+ HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
+
+ System.out.println("helloData.username = " + helloData.getUsername());
+ System.out.println("helloData.age = " + helloData.getAge());
+
+ response.getWriter().write("ok");
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java
new file mode 100644
index 00000000..660342ba
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java
@@ -0,0 +1,25 @@
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@WebServlet(name="requestBodyStringServlet", urlPatterns = "/request-body-string")
+public class RequestBodyStringServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ ServletInputStream inputStream = request.getInputStream();
+ String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
+
+ System.out.println("messageBody = " + messageBody);
+
+ response.getWriter().write("OK");
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java
new file mode 100644
index 00000000..3d19efca
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java
@@ -0,0 +1,108 @@
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
+public class RequestHeaderServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ printStartLine(request);
+ printHeaders(request);
+ printHeaderUtils(request);
+ printEtc(request);
+
+ }
+
+ //start line 정보
+ private void printStartLine(HttpServletRequest request) {
+ System.out.println("--- REQUEST-LINE - start ---");
+ System.out.println("request.getMethod() = " + request.getMethod()); //GET
+ System.out.println("request.getProtocol() = " + request.getProtocol()); //HTTP/1.1
+ System.out.println("request.getScheme() = " + request.getScheme()); //http
+ // http://localhost:8080/request-header
+ System.out.println("request.getRequestURL() = " + request.getRequestURL());
+ // /request-header
+ System.out.println("request.getRequestURI() = " + request.getRequestURI());
+ //username=hi
+ System.out.println("request.getQueryString() = " +
+ request.getQueryString());
+ System.out.println("request.isSecure() = " + request.isSecure()); //https 사용유무
+ System.out.println("--- REQUEST-LINE - end ---");
+ System.out.println();
+ }
+
+ //Header 모든 정보
+ private void printHeaders(HttpServletRequest request) {
+ System.out.println("--- Headers - start ---");
+ /*
+ Enumeration headerNames = request.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ System.out.println(headerName + ": " + request.getHeader(headerName));
+ }
+ */
+ request.getHeaderNames().asIterator()
+ .forEachRemaining(headerName -> System.out.println(headerName + ": "
+ + request.getHeader(headerName)));
+ System.out.println("--- Headers - end ---");
+ System.out.println();
+ }
+
+
+ //Header 편리한 조회
+ private void printHeaderUtils(HttpServletRequest request) {
+ System.out.println("--- Header 편의 조회 start ---");
+ System.out.println("[Host 편의 조회]");
+ System.out.println("request.getServerName() = " +
+ request.getServerName()); //Host 헤더
+ System.out.println("request.getServerPort() = " +
+ request.getServerPort()); //Host 헤더
+ System.out.println();
+ System.out.println("[Accept-Language 편의 조회]");
+ request.getLocales().asIterator()
+ .forEachRemaining(locale -> System.out.println("locale = " +
+ locale));
+ System.out.println("request.getLocale() = " + request.getLocale());
+ System.out.println();
+ System.out.println("[cookie 편의 조회]");
+ if (request.getCookies() != null) {
+ for (Cookie cookie : request.getCookies()) {
+ System.out.println(cookie.getName() + ": " + cookie.getValue());
+ }
+ }
+ System.out.println();
+ System.out.println("[Content 편의 조회]");
+ System.out.println("request.getContentType() = " +
+ request.getContentType());
+ System.out.println("request.getContentLength() = " +
+ request.getContentLength());
+ System.out.println("request.getCharacterEncoding() = " +
+ request.getCharacterEncoding());
+ }
+
+ //기타 정보
+ private void printEtc(HttpServletRequest request) {
+ System.out.println("--- 기타 조회 start ---");
+ System.out.println("[Remote 정보]");
+ System.out.println("request.getRemoteHost() = " +
+ request.getRemoteHost()); //
+ System.out.println("request.getRemoteAddr() = " +
+ request.getRemoteAddr()); //
+ System.out.println("request.getRemotePort() = " +
+ request.getRemotePort()); //
+ System.out.println();
+ System.out.println("[Local 정보]");
+ System.out.println("request.getLocalName() = " + request.getLocalName()); //
+ System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); //
+ System.out.println("request.getLocalPort() = " + request.getLocalPort()); //
+ System.out.println("--- 기타 조회 end ---");
+ System.out.println();
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java
new file mode 100644
index 00000000..d90e08d1
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java
@@ -0,0 +1,39 @@
+package hello.servlet.basic.request;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Enumeration;
+
+@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
+public class RequestParamServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("[전체 파라미터 조회] - start");
+
+// Enumeration parameterNames = requset.getParameterNames();
+ requset.getParameterNames().asIterator().forEachRemaining(
+ paramName -> System.out.println(paramName + "=" + requset.getParameter(paramName))
+ );
+
+ System.out.println("[전체 파라미터 조회] - end");
+ System.out.println();
+
+ System.out.println("[단일 파라미터 조회]");
+ String username= requset.getParameter("username");
+ String age = requset.getParameter("age");
+
+ System.out.println("username = " + username);
+ System.out.println("age = " + age);
+
+ System.out.println("[이름이 같은 복수 파라미터 조회]");
+ String[] usernames = requset.getParameterValues("username");
+ for(String name: usernames) {
+ System.out.println("username = " + name);
+ }
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java
new file mode 100644
index 00000000..12923c0a
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java
@@ -0,0 +1,62 @@
+package hello.servlet.basic.response;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header")
+public class ResponseHeaderServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ //[status-line]
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ //[response-headers]
+ response.setHeader("Content-Type", "text/plain;charset=utf-8");
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("my-header","hello");
+
+ //[Header 편의 메서드]
+// content(response);
+// cookie(response);
+// redirect(response);
+
+ //[message body]
+ PrintWriter writer = response.getWriter();
+ writer.println("ok");
+ }
+
+ private void content(HttpServletResponse response) {
+ //Content-Type: text/plain;charset=utf-8
+ //Content-Length: 2
+ //response.setHeader("Content-Type", "text/plain;charset=utf-8");
+ response.setContentType("text/plain");
+ response.setCharacterEncoding("utf-8");
+ //response.setContentLength(2); //(생략시 자동 생성)
+ }
+
+ private void cookie(HttpServletResponse response) {
+ //Set-Cookie: myCookie=good; Max-Age=600;
+ //response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
+ Cookie cookie = new Cookie("myCookie", "good");
+ cookie.setMaxAge(600); //600초
+ response.addCookie(cookie);
+ }
+
+ private void redirect(HttpServletResponse response) throws IOException {
+ //Status Code 302
+ //Location: /basic/hello-form.html
+ //response.setStatus(HttpServletResponse.SC_FOUND); //302
+ //response.setHeader("Location", "/basic/hello-form.html");
+ response.sendRedirect("/basic/hello-form.html");
+ }
+
+}
+
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java
new file mode 100644
index 00000000..a64c8450
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java
@@ -0,0 +1,27 @@
+package hello.servlet.basic.response;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html")
+public class ResponseHtmlServlet extends HttpServlet {
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ //Content-Type: text/html;charset=utf-8
+ response.setContentType("text/html");
+ response.setCharacterEncoding("utf-8");
+ PrintWriter writer = response.getWriter();
+ writer.println("");
+ writer.println("");
+ writer.println(" HI?
");
+ writer.println("");
+ writer.println("");
+ }
+}
+
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java
new file mode 100644
index 00000000..5260635b
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java
@@ -0,0 +1,30 @@
+package hello.servlet.basic.response;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import hello.servlet.basic.HelloData;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
+public class ResponseJsonServlet extends HttpServlet {
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ response.setContentType("application/json");
+ response.setCharacterEncoding("utf-8");
+
+ HelloData helloData = new HelloData();
+ helloData.setUsername("lee");
+ helloData.setAge(20);
+
+ String result = objectMapper.writeValueAsString(helloData);
+ response.getWriter().write(result);
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java b/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java
new file mode 100644
index 00000000..6fe75506
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java
@@ -0,0 +1,18 @@
+package hello.servlet.domain.member;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class Member {
+
+ private Long id;
+ private String username;
+ private int age;
+
+ public Member(String username, int age) {
+ this.username = username;
+ this.age = age;
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java b/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java
new file mode 100644
index 00000000..15eaaf81
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java
@@ -0,0 +1,38 @@
+package hello.servlet.domain.member;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MemberRepository {
+
+ private static Map store = new HashMap<>();
+ private static long sequence = 0L;
+
+ //싱글톤
+ private static final MemberRepository instance = new MemberRepository();
+
+ public static MemberRepository getInstance() {
+ return instance;
+ }
+ private MemberRepository(){}
+
+ public Member save(Member member) {
+ member.setId(++sequence);
+ store.put(member.getId(), member);
+ return member;
+ }
+
+ public Member findById(Long id) {
+ return store.get(id);
+ }
+
+ public List findAll() {
+ return new ArrayList<>(store.values());
+ }
+
+ public void clearStore() {
+ store.clear();
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java
new file mode 100644
index 00000000..a6a33309
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java
@@ -0,0 +1,23 @@
+package hello.servlet.web.frontcontroller;
+
+import java.util.HashMap;
+import java.util.Map;
+public class ModelView {
+ private String viewName;
+ private Map model = new HashMap<>();
+ public ModelView(String viewName) {
+ this.viewName = viewName;
+ }
+ public String getViewName() {
+ return viewName;
+ }
+ public void setViewName(String viewName) {
+ this.viewName = viewName;
+ }
+ public Map getModel() {
+ return model;
+ }
+ public void setModel(Map model) {
+ this.model = model;
+ }
+}
\ No newline at end of file
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java
new file mode 100644
index 00000000..4da0c980
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java
@@ -0,0 +1,32 @@
+package hello.servlet.web.frontcontroller;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class MyView {
+ private String viewPath;
+
+ public MyView(String viewPath) {
+ this.viewPath = viewPath;
+ }
+ public void render(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+ public void render(Map model, HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ modelToRequestAttribute(model, request);
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+ private void modelToRequestAttribute(Map model,
+ HttpServletRequest request) {
+ model.forEach((key, value) -> request.setAttribute(key, value));
+ }
+}
\ No newline at end of file
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java
new file mode 100644
index 00000000..0e2f00a3
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java
@@ -0,0 +1,11 @@
+package hello.servlet.web.frontcontroller.v1;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface ControllerV1 {
+ void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java
new file mode 100644
index 00000000..6dc0cab1
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java
@@ -0,0 +1,40 @@
+package hello.servlet.web.frontcontroller.v1;
+
+import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
+import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
+import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
+public class FrontControllerServiceV1 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV1() {
+ controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
+ controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
+ controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV1 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ controller.process(request, response);
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java
new file mode 100644
index 00000000..e4927c67
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java
@@ -0,0 +1,18 @@
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberFormControllerV1 implements ControllerV1 {
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String viewPath = "/WEB-INF/views/new-form.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java
new file mode 100644
index 00000000..da67d7b2
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java
@@ -0,0 +1,28 @@
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MemberListControllerV1 implements ControllerV1 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+
+ request.setAttribute("members", members);
+
+ String viewPath = "/WEB-INF/views/members.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java
new file mode 100644
index 00000000..c089740b
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java
@@ -0,0 +1,30 @@
+package hello.servlet.web.frontcontroller.v1.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.v1.ControllerV1;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberSaveControllerV1 implements ControllerV1 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ request.setAttribute("member", member);
+
+ String viewPath = "/WEB-INF/views/save-result.jsp";
+ RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java
new file mode 100644
index 00000000..6e1de8a2
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java
@@ -0,0 +1,12 @@
+package hello.servlet.web.frontcontroller.v2;
+
+import hello.servlet.web.frontcontroller.MyView;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public interface ControllerV2 {
+ MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java
new file mode 100644
index 00000000..8a46b0c9
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java
@@ -0,0 +1,42 @@
+package hello.servlet.web.frontcontroller.v2;
+
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
+import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
+public class FrontControllerServiceV2 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV2() {
+ controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
+ controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
+ controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV2 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ MyView view = controller.process(request, response);
+ view.render(request, response);
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java
new file mode 100644
index 00000000..ffdece7a
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java
@@ -0,0 +1,16 @@
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberFormControllerV2 implements ControllerV2 {
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ return new MyView("/WEB-INF/views/new-form.jsp");
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java
new file mode 100644
index 00000000..1a69f49e
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java
@@ -0,0 +1,24 @@
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MemberListControllerV2 implements ControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ List members = memberRepository.findAll();
+ request.setAttribute("members", members);
+ return new MyView("/WEB-INF/views/members.jsp");
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java
new file mode 100644
index 00000000..e56b5984
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java
@@ -0,0 +1,28 @@
+package hello.servlet.web.frontcontroller.v2.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v2.ControllerV2;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class MemberSaveControllerV2 implements ControllerV2 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+
+ @Override
+ public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String username = request.getParameter("username");
+ int age = Integer.parseInt(request.getParameter("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ request.setAttribute("member", member);
+ return new MyView("/WEB-INF/views/save-result.jsp");
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java
new file mode 100644
index 00000000..ba2edc59
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java
@@ -0,0 +1,9 @@
+package hello.servlet.web.frontcontroller.v3;
+
+import hello.servlet.web.frontcontroller.ModelView;
+
+import java.util.Map;
+
+public interface ControllerV3 {
+ ModelView process(Map paramMap);
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java
new file mode 100644
index 00000000..46e92e6d
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java
@@ -0,0 +1,56 @@
+package hello.servlet.web.frontcontroller.v3;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
+import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
+public class FrontControllerServiceV3 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV3() {
+ controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
+ controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
+ controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV3 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ Map paramMap = createParamMap(request);
+ ModelView mv = controller.process(paramMap);
+ String viewName = mv.getViewName();
+ MyView view = viewResolver(viewName);
+ view.render(mv.getModel(), request, response);
+ }
+ private Map createParamMap(HttpServletRequest request) {
+ Map paramMap = new HashMap<>();
+ request.getParameterNames().asIterator()
+ .forEachRemaining(paramName -> paramMap.put(paramName,
+ request.getParameter(paramName)));
+ return paramMap;
+ }
+ private MyView viewResolver(String viewName) {
+ return new MyView("/WEB-INF/views/" + viewName + ".jsp");
+ }
+}
\ No newline at end of file
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java
new file mode 100644
index 00000000..4b886d83
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java
@@ -0,0 +1,13 @@
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.Map;
+
+public class MemberFormControllerV3 implements ControllerV3 {
+ @Override
+ public ModelView process(Map paramMap) {
+ return new ModelView("new-form");
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java
new file mode 100644
index 00000000..897cb7fd
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java
@@ -0,0 +1,24 @@
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.List;
+import java.util.Map;
+
+public class MemberListControllerV3 implements ControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public ModelView process(Map paramMap) {
+ List members = memberRepository.findAll();
+ ModelView mv = new ModelView("members");
+ mv.getModel().put("members", members);
+
+ return mv;
+
+ }
+
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java
new file mode 100644
index 00000000..cae1ba01
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java
@@ -0,0 +1,25 @@
+package hello.servlet.web.frontcontroller.v3.controller;
+
+import hello.servlet.domain.member.Member;
+import hello.servlet.domain.member.MemberRepository;
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.v3.ControllerV3;
+
+import java.util.Map;
+
+public class MemberSaveControllerV3 implements ControllerV3 {
+
+ private MemberRepository memberRepository = MemberRepository.getInstance();
+ @Override
+ public ModelView process(Map paramMap) {
+ String username = paramMap.get("username");
+ int age = Integer.parseInt(paramMap.get("age"));
+
+ Member member = new Member(username, age);
+ memberRepository.save(member);
+
+ ModelView mv = new ModelView("save-result");
+ mv.getModel().put("member", member);
+ return mv;
+ }
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java
new file mode 100644
index 00000000..6a4a5803
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java
@@ -0,0 +1,7 @@
+package hello.servlet.web.frontcontroller.v4;
+
+import java.util.Map;
+
+public interface ControllerV4 {
+ String process(Map paramMap, Map model);
+}
diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java
new file mode 100644
index 00000000..065a4e8f
--- /dev/null
+++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java
@@ -0,0 +1,58 @@
+package hello.servlet.web.frontcontroller.v4;
+
+import hello.servlet.web.frontcontroller.ModelView;
+import hello.servlet.web.frontcontroller.MyView;
+import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
+import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
+public class FrontControllerServiceV4 extends HttpServlet {
+ private Map controllerMap = new HashMap<>();
+
+ public FrontControllerServiceV4() {
+ controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
+ controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
+ controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ System.out.println("FrontControllerServiceV1.service");
+
+ String requestURI = request.getRequestURI();
+
+ ControllerV4 controller = controllerMap.get(requestURI);
+ if(controller == null) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ Map paramMap = createParamMap(request);
+ Map model = new HashMap<>();
+
+ String viewName = controller.process(paramMap, model);
+
+ MyView view = viewResolver(viewName);
+ view.render(model, request, response);
+ }
+ private Map