Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[비트캠프] 82일차(17주차4일) - Spring Framework(Spring WebMVC 아키텍처, Tiles), myapp-60-3, 61, 프로젝트 절차(전체 과정) 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Spring Framework, Spring Boot

[비트캠프] 82일차(17주차4일) - Spring Framework(Spring WebMVC 아키텍처, Tiles), myapp-60-3, 61, 프로젝트 절차(전체 과정)

끈기JK 2023. 3. 3. 11:50

 

60-2 Spring WebMVC 아키텍처

 

"/app/*" 로 《Front Controller》DispatcherServlet 에게 요청한다. 여기서 Page Controller 를 call 하면 viewName (JSP URL, view 컴포넌트 이름) 예) "board/list" 을 리턴한다. 주고 받는 중간에 인터셉터 (스프링 필터)가 들어간다.

DispatcherServlet 이 viewName 예) "board/list" 을 가지고 ViewResolver 에게 요청한다. DispatcherServlet 은 요청 핸들러로부 view 이름을 리턴받지 못하면 페이지 컨트롤러의 요청 URL 을 view 이름으로 전달한다.

여기서 view 위치정보 "/WEB-INF/jsp/board/list.jsp" 를 리턴한다.

이걸 받아서 DispatcherServlet 이 list.jsp 를 실행한다. 결과를 리턴 하는데 인터셉터가 중간에 들어간다.

 

 

"spring viewresolver architecture" 검색 이미지

 

 

 

60-3 공용 객체와 웹관련 객체

 

공용 객체 : Service, DAO 등  /  웹관련 객체 : Controller, Intercepter 등

각각의 프론트 컨트롤러당 IoC 컨테이너를 보유하고 있다.

하나의 앱이 여러 프론트 컨트롤러 보유할 수 있다.

 

/app/* 를 처리하는 《Front Controller》DispatcherServlet 은 《IoC 컨테이너》WebApplicationContext 를 보유한다. 웹 관련 객체만 둔다 → AppConfig.class

관리 : AuthController, BoardController + 인터셉터

 

/admin/* 를 처리하는 《Front Controller》DispatcherServlet 은 《IoC 컨테이너》WebApplicationContext 를 보유한다. 웹 관련 객체만 둔다 → AdminConfig.class

관리 : StudentController, TeacherController + 인터셉터

 

《ServletContextListener》ContextLoaderListener 는 《IoC 컨테이너》WebApplicationContext 를 보유한다. 이를 Root IoC 컨테이너라 부른다 → RootConfig.class

Front Controller 들이 공유한다. 그러므로 service 객체를 여기 둬야한다.

관리 : BoardService, StudentService, TeacherService, BoardDao, BoardFileDao, StudentDao, TeacherDao, MemberDao, Mybatis 관련 객체, 트랜잭션 관련 객체(SqlSessionFactory, PlatformTransactionManager)

 

유지보수를 위해 이름을 길게 작성하는게 낫다

 

 

 

60-3 필터링 기술들 - 3가지

 

필터링 기술은 3가지가 있다.

- Servlet Container 에서 서블릿 사이의 서블릿 필터

- 서블릿에서 페이지 컨트롤러 사이의 인터셉터(preHandle, postHandle, afterCompletion)

- IoC 컨테이너 관리 객체 메서드 전/후의 AOP (Aspect Oriented Programming)

 

요청이 Servlet Container 로 오면 서블릿 필터(javax.servlet.Filter 구현체) 를 거쳐서 《서블릿》DispatcherServlet 으로 전달한다. 여기서 인터셉터나 AOP를 거쳐서 Page Controller 를 call 한다. Page Controller 에서 AOP 거쳐서 Service 를 call 한다. 여기서 AOP 거쳐서 DAO 를 call 한다. 리턴시 AOP 거쳐서 한다. JSP 에서 DispatcherServlet 으로 리턴시 인터셉터 거친다.

 

*AOP 동작원리

Caller 가 Proxy 의 m() 실행하면 여기서 선행작업 후 Original 의 m() 호출하고 후행작업 한다. 원래 코드를 손대지 않고 기능을 추가하는 방법이다.

 

 

 

60-3. Interceptor

 

Spring WebMVC 에서 《interface》WebMvcConfigurer 를 구현한 Java Config 를 call 한다. Java Config 설정에 따라 Spring Framework 를 준비한다.

 

 

### 60-3. Spring WebMVC 프론트 컨트롤러 도입하기 
- 공용 객체와 웹 관련 객체를 구분해 관리하는 방법: Root vs Servlet WebApplicationContext 
- 로그인 여부를 필터 대신 인터셉터로 처리하는 방법

 

AppConfig 를 공용 객체, 웹 객체(웹 안에서도 app, admin)으로 나눈다.

공용 : service 객체 및 같이 사용하는 객체, mybatis 관련 객체, 트랜잭션 관련 객체

app : auth, board

admin : student, teacher

 

/app/* 요청 처리하는 《프론트 컨트롤러》Dispatcher Servlet 정의한다.

package bitcamp.myapp.config;

import javax.servlet.Filter;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class AppWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

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

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

  @Override
  protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement(
        System.getProperty("java.io.tmpdir"), // 클라이언트가 보낸 파일을 임시 보관할 폴더
        1024 * 1024 * 20, // 한 파일의 최대 크기
        1024 * 1024 * 20 * 10, // 한 요청 당 최대 총 파일 크기
        1024 * 1024 * 1 // 클라이언트가 보낸 파일을 메모리에 임시 보관하는 최대 크기.
        // 최대 크기를 초과하면 파일에 내보낸다.
        ));
  }

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

 

《IoC 컨테이너》 설정 파일인 AppConfig.java 는 다음과 같이 설정한다.

@ComponentScan 의 value 를 controller 패키지로 설정한다. excludeFilters 에 검색 제외할 클래스 지정한다.

@EnableWebMvc 붙여서 로 WebMVC 설정을 수행하라고 지시한다.

인스턴스 블록으로 객체 생성시 콘솔에 출력한다.

addInterceptors(...) 매서드로 인터셉터 등록한다.

package bitcamp.myapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import bitcamp.myapp.controller.StudentController;
import bitcamp.myapp.controller.TeacherController;
import bitcamp.myapp.web.interceptor.AuthInterceptor;

//@Configuration

@ComponentScan(
    value = "bitcamp.myapp.controller",
    excludeFilters = {
        @Filter(
            type = FilterType.ASSIGNABLE_TYPE,
            classes = {StudentController.class, TeacherController.class})
    })

// WebMVC 관련 설정을 처리하고 싶다면 다음 애노테이션을 추가하라!
// => WebMVC 관련 설정을 수행하는 클래스를 정의했으니,
//    WebMvcConfigurer 구현체를 찾아
//    해당 인터페이스에 정의된대로 메서드를 호출하여
//    설정을 수행하라는 의미다!
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {

  {
    System.out.println("AppConfig 생성됨!");
  }

  @Bean
  public MultipartResolver multipartResolver() {
    return new StandardServletMultipartResolver();
  }

  @Bean
  public ViewResolver viewResolver() {
    // 페이지 컨트롤러가 jsp 경로를 리턴하면
    // viewResolver가 그 경로를 가지고 최종 jsp 경로를 계산한 다음에
    // JstlView를 통해 실행한다.
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    viewResolver.setViewClass(JstlView.class);
    viewResolver.setPrefix("/WEB-INF/jsp/");
    viewResolver.setSuffix(".jsp");
    return viewResolver;
  }

  // WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    System.out.println("AppConfig.addInterceptors() 호출됨!");
    registry.addInterceptor(
        new AuthInterceptor()).addPathPatterns("/**/*insert", "/**/*update", "/**/*delete");
  }
}

 

인터셉터 호출은 preHandle() → BoardController → postHandle() → JSP 실행 → afterCompletion() 순으로 진행된다.

AuthIntercepter.preHandle 호출됨!
BoardController.list() 호출됨!
AuthIntercepter.postHandle() 호출됨!
board/list.jsp 실행!
AuthIntercepter.afterCompletion() 호출됨!

 

myapp > web > interceptor 패키지 생성해서 클래스 파일 생성한다.

preHandle(...) 메서드 등록하여 login 체크하는 코드 넣어준다. return false 하면 요청을 거부하고 처리 흐름을 중단한다.

package bitcamp.myapp.web.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import bitcamp.myapp.vo.Member;

public class AuthInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    Member loginUser = (Member) request.getSession().getAttribute("loginUser");
    if (loginUser == null) {
      response.sendRedirect(request.getContextPath() + "/app/auth/form");
      return false;
    }
    return true;
  }
}

 

/admin/* 요청 처리하는 《프론트 컨트롤러》Dispatcher Servlet 정의한다.

ContextLoaderListener 의 IoC 컨테이너 설정한다. 모든 서블릿이 공유하므로 어느 서블릿이든 한 곳에서만 설정한다.

package bitcamp.myapp.config;

import javax.servlet.Filter;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class AdminWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  // DispatcherServlet 의 서블릿이름을 설정한다.
  @Override
  protected String getServletName() {
    return "admin";
  }

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

  @Override
  protected Class<?>[] getRootConfigClasses() {
    // ContextLoaderListener의 IoC 컨테이너 설정
    // ContextLoaderListener는 모든 서블릿이 공유하기 때문에
    // 여기에서 한 번 설정하면 다른 쪽에서는 설정할 필요가 없다.
    return new Class<?>[] {RootConfig.class};
  }

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

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

 

《IoC 컨테이너》 설정 파일인 AdminConfig.java 설정한다.

AuthInterceptor(), AdminCheckInterceptor() 등록한다.

 

package bitcamp.myapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import bitcamp.myapp.controller.AuthController;
import bitcamp.myapp.controller.BoardController;
import bitcamp.myapp.controller.DownloadController;
import bitcamp.myapp.web.interceptor.AdminCheckInterceptor;
import bitcamp.myapp.web.interceptor.AuthInterceptor;

//@Configuration

@ComponentScan(
    value = "bitcamp.myapp.controller",
    excludeFilters = {
        @Filter(
            type = FilterType.ASSIGNABLE_TYPE,
            classes = {AuthController.class, BoardController.class, DownloadController.class})
    })
@EnableWebMvc // 프론트 컨트롤러 각각에 대해 설정해야 한다.
public class AdminConfig implements WebMvcConfigurer {

  {
    System.out.println("AdminConfig 생성됨!");
  }

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    viewResolver.setViewClass(JstlView.class);
    viewResolver.setPrefix("/WEB-INF/jsp/");
    viewResolver.setSuffix(".jsp");
    return viewResolver;
  }

  // WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    System.out.println("AdminConfig.addInterceptors() 호출됨!");
    registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**");
    registry.addInterceptor(new AdminCheckInterceptor()).addPathPatterns("/**");
  }
}

 

프론트 컨트롤러에 들어온 요청 주소가 /admin/* 일 때 loginUser 가 admin 계정이 아니면 로그인 form 으로 튕겨낸다.

package bitcamp.myapp.web.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import bitcamp.myapp.vo.Member;

public class AdminCheckInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    Member loginUser = (Member) request.getSession().getAttribute("loginUser");
    if (!loginUser.getEmail().equals("admin@test.com")) {
      response.sendRedirect(request.getContextPath() + "/app/auth/form");
      return false;
    }
    return true;
  }
}

 

RootConfig.java 는 DataSource, PlatformTransactionManager, SqlSessionFactory 객체 메서드를 둔다.

package bitcamp.myapp.config;

import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;


@ComponentScan(
    value = "bitcamp.myapp",
    excludeFilters = {
        @Filter(
            type = FilterType.REGEX,
            pattern = {"bitcamp.myapp.controller.*"})
    })
@PropertySource("classpath:/bitcamp/myapp/config/jdbc.properties")
@MapperScan("bitcamp.myapp.dao")
@EnableTransactionManagement
public class RootConfig {

  {
    System.out.println("RootConfig 생성됨!");
  }

  @Bean
  public DataSource dataSource(
      @Value("${jdbc.driver}") String jdbcDriver,
      @Value("${jdbc.url}") String url,
      @Value("${jdbc.username}") String username,
      @Value("${jdbc.password}") String password) {
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName(jdbcDriver);
    ds.setUrl(url);
    ds.setUsername(username);
    ds.setPassword(password);
    return ds;
  }

  @Bean
  public PlatformTransactionManager transactionManager(DataSource dataSource) throws Exception {
    System.out.println("PlatformTransactionManager 객체 생성! ");
    return new DataSourceTransactionManager(dataSource);
  }

  @Bean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext appCtx) throws Exception {
    System.out.println("SqlSessionFactory 객체 생성!");
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource);
    factoryBean.setTypeAliasesPackage("bitcamp.myapp.vo");
    factoryBean.setMapperLocations(appCtx.getResources("classpath*:bitcamp/myapp/mapper/*Mapper.xml"));
    return factoryBean.getObject();
  }
}

 

 

 

 

Tiles 뷰 기술 적용

 

docs.spring.io > Web Servlet > Spring Web MVC > View Technologies

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-view

 

 

central.sonatype.com 에서 "tiles jsp" 검색한다.

Referenced Libraries 에 아래 추가된다.

 

 

 

61. Tiles View 사용하기

 

///////////////////////

/app/board/list 요청이 《Front Controller》Dispatcher Servlet 으로 오면 인터셉터를 거쳐 list() 를 BoardController 에게 지시한다. 리턴값 없다.

리턴 값이 없으므로 요청 들어온 주소를 UrlBasedViewResolver 로 보낸다. prefix 로 "app/" 붙인 후 객체 리턴한다.

이를 TilesView 객체를 이용하여 JSP 를 읽어들인다.

 

 

 

### 61. Tiles 뷰 기술을 적용하기 

 

### 61. Tiles 뷰 기술을 적용하기 
- JSP에 Tiles 뷰 기술을 적용하여 UI 템플릿을 다루는 방법

 

AppConfig.java 에 tilesViewResolver() 등록한다. prefix 설정하고 뷰리졸버의 우선 순위를 1로 한다.

기존 뷰리졸버의 우선 순위는 2로 한다.

package bitcamp.myapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesView;
import bitcamp.myapp.controller.StudentController;
import bitcamp.myapp.controller.TeacherController;
import bitcamp.myapp.web.interceptor.AuthInterceptor;

//@Configuration

@ComponentScan(
    value = "bitcamp.myapp.controller",
    excludeFilters = {
        @Filter(
            type = FilterType.ASSIGNABLE_TYPE,
            classes = {StudentController.class, TeacherController.class})
    })

// WebMVC 관련 설정을 처리하고 싶다면 다음 애노테이션을 추가하라!
// => WebMVC 관련 설정을 수행하는 클래스를 정의했으니,
//    WebMvcConfigurer 구현체를 찾아
//    해당 인터페이스에 정의된대로 메서드를 호출하여
//    설정을 수행하라는 의미다!
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {

  {
    System.out.println("AppConfig 생성됨!");
  }

  @Bean
  public MultipartResolver multipartResolver() {
    return new StandardServletMultipartResolver();
  }

  @Bean
  public ViewResolver viewResolver() {
    // 페이지 컨트롤러가 jsp 경로를 리턴하면
    // viewResolver가 그 경로를 가지고 최종 jsp 경로를 계산한 다음에
    // JstlView를 통해 실행한다.
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    viewResolver.setViewClass(JstlView.class);
    viewResolver.setPrefix("/WEB-INF/jsp/");
    viewResolver.setSuffix(".jsp");
    viewResolver.setOrder(2);
    return viewResolver;
  }

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

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

    // request handler가 리턴한 view name 앞에 일반 페이지임을 표시하기 위해 접두사를 붙인다.
    vr.setPrefix("app/");

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

  // WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    System.out.println("AppConfig.addInterceptors() 호출됨!");
    registry.addInterceptor(
        new AuthInterceptor()).addPathPatterns("/**/*insert", "/**/*update", "/**/*delete");
  }
}

 

AdminConfig.java 에 tilesViewResolver() 등록한다.

package bitcamp.myapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesView;
import bitcamp.myapp.controller.AuthController;
import bitcamp.myapp.controller.BoardController;
import bitcamp.myapp.controller.DownloadController;
import bitcamp.myapp.web.interceptor.AdminCheckInterceptor;
import bitcamp.myapp.web.interceptor.AuthInterceptor;

//@Configuration

@ComponentScan(
    value = "bitcamp.myapp.controller",
    excludeFilters = {
        @Filter(
            type = FilterType.ASSIGNABLE_TYPE,
            classes = {AuthController.class, BoardController.class, DownloadController.class})
    })
@EnableWebMvc // 프론트 컨트롤러 각각에 대해 설정해야 한다.
public class AdminConfig implements WebMvcConfigurer {

  {
    System.out.println("AdminConfig 생성됨!");
  }

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    viewResolver.setViewClass(JstlView.class);
    viewResolver.setPrefix("/WEB-INF/jsp/");
    viewResolver.setSuffix(".jsp");
    viewResolver.setOrder(2);
    return viewResolver;
  }

  @Bean
  public ViewResolver tilesViewResolver() {
    UrlBasedViewResolver vr = new UrlBasedViewResolver();
    vr.setViewClass(TilesView.class);
    vr.setPrefix("admin/");
    vr.setOrder(1);
    return vr;
  }

  // WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    System.out.println("AdminConfig.addInterceptors() 호출됨!");
    registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**");
    registry.addInterceptor(new AdminCheckInterceptor()).addPathPatterns("/**");
  }
}

 

WEB-INF 에 defs 폴더 만들고 app-tiles.xml 및 admin-tiles.xml 생성한다.

공통 레이아웃인 admin-template.jsp 정의하고 안에서 사용할 admin-header.jsp, admin-footer.jsp 정의한다.

요청 핸들러가 리턴한 경로로 TilesView 템플릿 엔진이 사용할 JSP URL 을 정의한다.

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

 

app-template.jsp 에 디렉티브 엘리먼트로 tiles 라이브러리 불러들인다. (admin-template.jsp 도 동일함)

<tiles:*> 사용해서 header, body, footer 에 놓일 jsp 위치 지정한다.

refresh 값이 있는지 검사해서 있으면 <meta *> 태그로 "Refresh" 지정한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<c:if test="${not empty refresh}">
  <meta http-equiv='Refresh' content='1;url=${refresh}'>
</c:if>
<title>비트캠프 - NCP 1기</title>
<style>
  header {
    height: 60px;
    background-color: gray;
    color: black;
  }
  
  footer {
    height: 60px;
    background-color: navy;
    color: white;
  }
</style>
</head>
<body>

<tiles:insertAttribute name="header"/>

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

<tiles:insertAttribute name="footer"/>

</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<c:if test="${not empty refresh}">
  <meta http-equiv='Refresh' content='1;url=${refresh}'>
</c:if>
<title>비트캠프 - NCP 1기</title>
<style>
header {
  height: 80px;
  background: black;
  color: white;
}

footer {
  height: 100px;
  background: darkgray;
  color: white;
}

a:visited {
	color: white;
}
</style>
</head>
<body>

<tiles:insertAttribute name="header"/>

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

<tiles:insertAttribute name="footer"/>

</body>
</html>

 

*-template.jsp 외에는 <body> 이전부터 </body> 이후 태그 삭제한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>  
<header>
강의관리시스템 - 관리자

  <a href="/web/admin/student/list">학생관리</a>
  <a href="/web/admin/teacher/list">강사관리</a>
  
<c:if test="${empty loginUser}">
  <a href="/web/app/auth/form">로그인</a>
</c:if>

<c:if test="${not empty loginUser}">
  <a href="/web/app/auth/logout">로그아웃(${loginUser.name})</a>
</c:if>
</header>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<footer>
비트캠프 | 서울 강남구 강남대로94길 20, 삼오빌딩 6층| 사업자등록번호 : 328-85-02112
</footer>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<h1>게시판(Tiles)</h1>

<div><a href='form'>새 글</a></div>

<table border='1'>
<tr>
  <th>번호</th> <th>제목</th> <th>작성자</th> <th>작성일</th> <th>조회수</th>
</tr>

<c:forEach items="${boards}" var="b">
  <tr>
     <td>${b.no}</td> 
     <td><a href='view?no=${b.no}'>${b.title}</a></td> 
     <td>${b.writer.name}</td>
     <td>${b.createdDate}</td> 
     <td>${b.viewCount} </td>
  </tr>
</c:forEach>

</table>

<form action='list' method='get'>
  <input type='text' name='keyword' value='${param.keyword}'>
  <button>검색</button>
</form>

나머지는 생략한다.

 

 

*Controller 에서 refresh 해야하는 insert, update, delete 메서드에서 해당 서비스 수행 후 model.addAttribute("refresh", "list"); 코드 넣어 값을 세팅해준다.

package bitcamp.myapp.controller;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
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.multipart.MultipartFile;
import bitcamp.myapp.service.BoardService;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;

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

  {
    System.out.println("BoardController 생성됨!");
  }

  // ServletContext 는 요청 핸들러의 파라미터로 주입 받을 수 없다.
  // 객체의 필드로만 주입 받을 수 있다.
  @Autowired private ServletContext servletContext;
  @Autowired private BoardService boardService;

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

  @PostMapping("insert")
  public void insert(
      Board board,
      List<MultipartFile> files,
      Model model,
      HttpSession session) {
    try {
      Member loginUser = (Member) session.getAttribute("loginUser");

      Member writer = new Member();
      writer.setNo(loginUser.getNo());
      board.setWriter(writer);

      List<BoardFile> boardFiles = new ArrayList<>();
      for (MultipartFile file : files) {
        if (file.isEmpty()) {
          continue;
        }

        String filename = UUID.randomUUID().toString();
        file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));

        BoardFile boardFile = new BoardFile();
        boardFile.setOriginalFilename(file.getOriginalFilename());
        boardFile.setFilepath(filename);
        boardFile.setMimeType(file.getContentType());
        boardFiles.add(boardFile);
      }
      board.setAttachedFiles(boardFiles);

      boardService.add(board);
      model.addAttribute("refresh", "list");

    } catch (Exception e) {
      e.printStackTrace();
      model.addAttribute("error", "data");
    }
  }

  @GetMapping("list")
  public void list(String keyword, Model model) {
    System.out.println("BoardController.list() 호출됨!");
    model.addAttribute("boards", boardService.list(keyword));
  }

  @GetMapping("view")
  public void view(int no, Model model) {
    model.addAttribute("board", boardService.get(no));
  }

  @PostMapping("update")
  public String update(
      Board board,
      List<MultipartFile> files,
      Model model,
      HttpSession session) {
    try {
      Member loginUser = (Member) session.getAttribute("loginUser");

      Board old = boardService.get(board.getNo());
      if (old.getWriter().getNo() != loginUser.getNo()) {
        return "redirect:../auth/fail";
      }

      List<BoardFile> boardFiles = new ArrayList<>();
      for (MultipartFile file : files) {
        if (file.isEmpty()) {
          continue;
        }

        String filename = UUID.randomUUID().toString();
        file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));

        BoardFile boardFile = new BoardFile();
        boardFile.setOriginalFilename(file.getOriginalFilename());
        boardFile.setFilepath(filename);
        boardFile.setMimeType(file.getContentType());
        boardFile.setBoardNo(board.getNo());
        boardFiles.add(boardFile);
      }
      board.setAttachedFiles(boardFiles);

      boardService.update(board);
      model.addAttribute("refresh", "list");

    }  catch (Exception e) {
      e.printStackTrace();
      model.addAttribute("error", "data");
    }

    return "board/update";
  }

  @PostMapping("delete")
  public String delete(int no, Model model, HttpSession session) {
    try {
      Member loginUser = (Member) session.getAttribute("loginUser");

      Board old = boardService.get(no);
      if (old.getWriter().getNo() != loginUser.getNo()) {
        return "redirect:../auth/fail";
      }
      boardService.delete(no);

    }  catch (Exception e) {
      e.printStackTrace();
      model.addAttribute("error", "data");
    }
    model.addAttribute("refresh", "list");
    return "board/delete";
  }

  @GetMapping("filedelete")
  public String filedelete(int boardNo, int fileNo, HttpSession session) {
    Member loginUser = (Member) session.getAttribute("loginUser");
    Board old = boardService.get(boardNo);
    if (old.getWriter().getNo() != loginUser.getNo()) {
      return "redirect:../auth/fail";
    } else {
      boardService.deleteFile(fileNo);
      return "redirect:view?no=" + boardNo;
    }
  }
}

 

 

 

 

프로젝트 수행 절차

 

① 프로젝트 주제 선정

- 개요 (현황 → 문제점, 해결방안 → 이점) : 문제점이 없더라도 이야기를 꾸며내라

- UI prototype (주요 기능)

 

② Use Case 모델링

- Actor 식별 (시스템 사용)

- Use Case 식별 (사용자의 기능)

- Use Case Model

- Use Case 명세서

→ UI prototype 상세화

 

③ DB 모델링

- 테이블 식별

- 컬럼 식별

- key 식별

- 제약조건 식별

→ DB Model → ER-Diagram : eXERD (이클립스 기반 DB 모델링 구조)

 

④ 구현

- 기능별 full stack 작업 → 테스트. 반복

 

⑤ 발표

- 프로젝트 설명

- 시연

- 소감

 

 

 


 

 

조언

 

*어떤 객체가 어떤 일을 하는지 이름 파악을 빨리 해야한다. 회사 생활도 마찬가지다.

 

 


 

과제

 

/