package kr.or.ddit.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
//공지사항 게시판
@RequestMapping("/notice")
@Slf4j
@Controller
public class NoticeController {
//공지사항 목록 - 누구나 접근 가능
@GetMapping("/list")
public String list() {
log.info("공지사항 목록 - 누구나 접근 가능");
//forwarding : jsp
return "notice/list";
}
//공지사항 등록 - 로그인 한 관리자만 접근 가능
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
@GetMapping("/register")
public String register() {
log.info("공지사항 등록 - 로그인 한 관리자만 접근 가능");
//forwarding : jsp
return "notice/register";
}
}
CREATE TABLE PERSISTENT_LOGINS(
USERNAME VARCHAR2(150),
SERIES VARCHAR2(150),
TOKEN VARCHAR2(150),
LAST_USED DATE,
CONSTRAINT PK_PL PRIMARY KEY(SERIES)
);
자동로그인을 위한 테이블 생성 - 스프링에서 제공하는 기본 양식
<!-- dataSource를 통해 지정한 Database의 약속된 테이블(PERSISTENT_LOGINS)을
이용하여 기존 로그인 정보를 기록함 -->
<!-- token-validity-seconds : 쿠키의 유효시간(초) 604800초는 7일 -->
<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
<!-- ref : bean 객체(root-context) -->
<!-- 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화 함 -->
<!-- 로그아웃을 하면 자동 로그인에 사용된 쿠키도 함께 삭제해 줌 invalidate-session="true"-->
<security:logout logout-url="/logout" invalidate-session="true" delete-cookies="remember-me,JSESSION_ID" />
dataSource -> root-context.xml 에 있는 dataSource에 해당하는 정보를 가져옴
세션,쿠키 둘다 생성은 서버에서 생성 , 쿠키는 사용자(클라이언트)에 저장
쿠키 : 세션의 아이디(JSESSIONID)를 쿠키에 넣어서 클라이언트에 저장
로그아웃(버튼클릭시)시 jsessionid를 제거(쿠키도 같이 삭제)
로그아웃버튼생성하기
https://adminlte.io/themes/v3/pages/UI/buttons.html
<div class="info">
<a href="#" class="d-block">${studVO.studNm}(${studVO.studId})</a>
<form action="/logout" method="post">
<button type="submit" class="btn btn-block btn-outline-primary btn-xs">로그아웃</button>
<sec:csrfInput/>
</form>
</div>
버튼/폼생성(csrf 공격방지)
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">로그인</h3>
</div>
<form class="form-horizontal" action="/login" method="post">
<div class="card-body">
<div class="form-group row">
<label for="inputEmail3" class="col-sm-2 col-form-label">아이디</label>
<div class="col-sm-10">
<input type="text" name="username" class="form-control" id="username"
placeholder="아이디" />
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-2 col-form-label">비밀번호</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control" id="password"
placeholder="비밀번호">
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<!-- 로그인 상태를 유지시켜주는 체크박스
체크시 : PERSISTENT_LOGINS에 정보가 INSERT된다.
-->
<input type="checkbox" name="remember-me" class="form-check-input" id="remember-me">
<label class="form-check-label" for="remember-me">자동 로그인</label>
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-info">로그인</button>
<button type="reset" class="btn btn-default float-right">Cancel</button>
</div>
<!-- csrf : Cross Site(크로스 사이트) Request(요청) Forgery(위조) -->
<sec:csrfInput />
</form>
</div>
form에서 div => name값 중요
로그인시 DB PERSISTENT_LOGINS테이블에 데이터 삽입되는것 확인
@PreAuthorize
@PostAuthorize(@PreAuthorize의 반대 개념이 아님 after x)
xmlns:security="http://www.springframework.org/schema/security"
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.2.xsd">
servlet-context.xml에 추가
<!-- 스프링 시큐리티 애너테이션을 활성화
- pre-post-annotations="enabled" -> 골뱅이PreAuthorize, 골뱅이PostAuthorize 활성화
*** PreAuthorize : 특정 메소드를 실행하기 전에 role 체킹
PostAuthorize : 특정 메소드를 실행한 후에 role 체킹
- secured-annotations="enabled" -> 골뱅이Secured를 활성화
Secured : 스프링 시큐리티 모듈을 지원하기 위한 애너테이션
-->
<security:global-method-security pre-post-annotations="enabled" secured-annotations="enabled" />
서버 재기동후 에러 없으면 됨
<!-- URI 패턴으로 접근 제한을 설정, Role(권한,역할) -->
<!-- <security:intercept-url pattern="/brd/list" access="permitAll" /> -->
<security:intercept-url pattern="/brd/register" access="hasRole('ROLE_MEMBER')" />
<!-- <security:intercept-url pattern="/notice/list" access="permitAll" /> -->
<security:intercept-url pattern="/notice/register" access="hasRole('ROLE_ADMIN')" />
<!-- 도서 등록 : /create -->
<security:intercept-url pattern="/create" access="hasRole('ROLE_ADMIN')" />
permitAll -> 생략해도 가능(애초에 모든 접근에 열려있을때)
package kr.or.ddit.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
//회원게시판
@RequestMapping("/brd")
@Slf4j
@Controller
public class BrdController {
//회원게시판-목록(누구나 접근 가능)
@GetMapping("/list")
public String list() {
log.info("brd->list->누구나 접근 가능");
//forwarding : jsp
return "brd/list";
}
//회원게시판-등록(로그인 한 회원만 접근 가능)
// ROLE_MEMBER의 권한을 갖고있을때
@PreAuthorize("hasRole('ROLE_MEMBER')")
@GetMapping("/register")
public String register() {
log.info("brd->register->로그인 한 회원만 접근 가능");
//forwarding : jsp
return "brd/register";
}
}
@PreAuthorize -> 메소드 실행전 권한을 우선적으로 체크한 후 해당메소드 실행
package kr.or.ddit.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
//공지사항 게시판
@RequestMapping("/notice")
@Slf4j
@Controller
public class NoticeController {
//공지사항 목록 - 누구나 접근 가능
@GetMapping("/list")
public String list() {
log.info("공지사항 목록 - 누구나 접근 가능");
//forwarding : jsp
return "notice/list";
}
//공지사항 등록 - 로그인 한 관리자만 접근 가능
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/register")
public String register() {
log.info("공지사항 등록 - 로그인 한 관리자만 접근 가능");
//forwarding : jsp
return "notice/register";
}
}
NoticeController에도 동일한 방식으로 관리자에 접근 권한 부여
//요청URI : /create
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping(value="/create",method=RequestMethod.GET)
public ModelAndView create() {
/*
ModelAndView
1) Model : Controller가 반환할 데이터(String, int, List, Map, VO..)를 담당
2) View : 화면을 담당(뷰(View : JSP)의 경로)
*/
ModelAndView mav = new ModelAndView();
// <beans:property name="prefix" value="/WEB-INF/views/" />
// <beans:property name="suffix" value=".jsp" />
// prefix(접두어) : /WEB-INF/views/
// suffix(접미어) : .jsp
// /WEB-INF/views/ + book/create + .jsp
//forwarding
mav.setViewName("book/create");
return mav;
}
BookController create메소드에도 동일작업
이후 servlet-context.xml에 intercept-url 모두 주석처리 후 서버 재기동 - 권한체크에 따른 로그인 여부 확인
package kr.or.ddit.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
//공지사항 게시판
@RequestMapping("/notice")
@Slf4j
@Controller
public class NoticeController {
//공지사항 목록 - 누구나 접근 가능
@GetMapping("/list")
public String list() {
log.info("공지사항 목록 - 누구나 접근 가능");
//forwarding : jsp
return "notice/list";
}
// 1. 공지사항 등록 - 로그인 한 관리자만 접근 가능
// 2. 공지사항 등록 - 로그인(인증) 한 관리자 또는 회원(인가)만 접근 가능
@PreAuthorize("hasAnyRole('ROLE_ADMIN' , 'ROLE_MEMBER')")
@GetMapping("/register")
public String register() {
log.info("공지사항 등록 - 로그인 한 관리자만 접근 가능");
//forwarding : jsp
return "notice/register";
}
}
hasAnyRole추가 -> any : or연산
--다중행 서브쿼리
--교집합
SELECT *
FROM LPROD
WHERE LPROD_GU IN('P101','P102');
SELECT *
FROM LPROD A
WHERE A.LPROD_GU IN(SELECT B.LPROD_GU FROM LPROD B WHERE B.LPROD_GU LIKE 'P1%');
--OR
--메인쿼리 ID : 1,2,3,4,5,6,7.....
--서브쿼리 결과 : 1,2
--최종 결과 : 2,3,4,5,6,7...
SELECT *
FROM LPROD A
WHERE A.LPROD_ID > ANY(SELECT B.LPROD_ID FROM LPROD B WHERE B.LPROD_GU LIKE 'P1%');
--AND
--메인쿼리 ID : 1,2,3,4,5,6,7.....
--서브쿼리 결과 : 1,2
--최종 결과 : 3,4,5,6,7...
SELECT *
FROM LPROD A
WHERE A.LPROD_ID > ALL(SELECT B.LPROD_ID FROM LPROD B WHERE B.LPROD_GU LIKE 'P1%');
-- 교집합(공통)
-- 연결고리(공통부분) : 자료형 동일 , 동일 데이터가 존재해야함
-- 아래쿼리문에서 AND부분(AND B.LPROD_GU = A.LPROD_GU--*******)
-- A집합 GU : P101 , P102, P201, P202 , P301 ,...
-- B집합 GU : P101 , P102
SELECT *
FROM LPROD A
WHERE EXISTS(
SELECT B.LPROD_GU
FROM LPROD B
WHERE B.LPROD_GU LIKE 'P1%'
AND B.LPROD_GU = A.LPROD_GU--*******
);
서브쿼리의 결과가 여러개 -> 다중행 서브쿼리
package kr.or.ddit.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
//공지사항 게시판
@RequestMapping("/notice")
@Slf4j
@Controller
public class NoticeController {
//공지사항 목록 - 누구나 접근 가능
@GetMapping("/list")
public String list() {
log.info("공지사항 목록 - 누구나 접근 가능");
//forwarding : jsp
return "notice/list";
}
// 1. 공지사항 등록 - 로그인 한 관리자만 접근 가능
// @PreAuthorize("hasRole('ROLE_ADMIN')")
// @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_MEMBER')")
// 2. 공지사항 등록 - 로그인(인증) 한 관리자 또는 회원(인가)만 접근 가능
// @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_MEMBER')")
// @Secured({"ROLE_MEMBER","ROLE_ADMIN"})
// 3. 공지사항 등록 - 로그인(인증) 한 관리자 이면서 회원(인가)만 접근 가능
// @PreAuthorize("hasRole('ROLE_ADMIN') and hasRole('ROLE_MEMBER')")
// 4. 로그인한 사용자만 접근 가능(권한과 상관 없음)
// @PreAuthorize("isAuthenticated()")
// 5. 로그인 안 한 사용자가 접근 가능 -> 로그인 한 사용자는 접근 불가
// @PreAuthorize("isAnonymous()")
// 6. 누구나 접근 가능(PreAuthorize 생략)
@GetMapping("/register")
public String register() {
log.info("공지사항 등록 - 로그인 한 관리자만 접근 가능");
//forwarding : jsp
return "notice/register";
}
}
파일 업로드
<!-- 파일업로드 시작 -->
<!-- common-fileupload 라이브러리는 tomcat7.0버전 이후로는 서블릿3.0이상에서 지원함 -->
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!-- 파일을 처리하기 위한 의존 라이브러리 -->
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- 썸네일 -->
<!-- https://mvnrepository.com/artifact/org.imgscalr/imgscalr-lib -->
<dependency>
<groupId>org.imgscalr</groupId>
<artifactId>imgscalr-lib</artifactId>
<version>4.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.coobird/thumbnailator -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
<!-- 파일업로드 끝 -->
메이븐 의존 라이브러리 추가
commons-fileupload , commons-io는 같이 필요,
썸네일 사용안할시 제외해도 무방
<!-- 파일업로드 설정
CommonsMultipartResolver multipartResolver = new multipartResolver();
multipartResolver.setMaxUploadSize(10485760);
multipartResolver.setDefaultEncoding("UTF-8");
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 파일업로드 용량 (10MB)-->
<property name="maxUploadSize" value="10485760"/>
<property name="defaultEncoding" value="UTF-8" />
</bean>
<!-- 파일업로드 디렉토리 설정 -->
<bean id="uploadPath" class="java.lang.String">
<constructor-arg value="c:\\upload"/>
</bean>
root-context.xml에 bean 객체 추가
파일용량은 자유롭게 설정 가능
web.xml로 이동
<!-- multipart filter 추가하기(한글 처리 다음에 넣기!!!) -->
<filter>
<display-name>springMultipartFilter</display-name>
<filter-name>springMultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>springMultipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
web.xml에 필터추가(순서주의 , 한글처리 다음에 추가)
<!-- web.xml의 설정은 WAS(Tomcat) 자체 설정일 뿐임. -->
<!-- multipart-config : 메모리사이즈, 업로드 파일 저장 위치, 최대 크기 설정 -->
<multipart-config>
<location>c:\\upload</location><!-- 업로드 되는 파일을 저장할 공간 -->
<max-file-size>20971520</max-file-size><!-- 업로드 파일의 최대 크기 1MB * 20 -->
<max-request-size>41943040</max-request-size><!-- 한 번에 올릴 수 있는 최대 크기 40MB -->
<file-size-threshold>20971520</file-size-threshold><!-- 메모리 사용 크기 20MB -->
</multipart-config>
web.xml에 톰캣설정 추가(같은 파일임, 순서 주의)
SERVERS
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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
http://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.
--><!-- The contents of this file will be loaded for each web application -->
<!-- 만약 WAS에 아래와 같이 설정하지 않으면
"Could not parse multipart servlet request" 에러발생, ? WAS에서 Multipart를 찾지 못하게 된다. -->
<Context allowCasualMultipartParsing="true" >
<!-- 캐시문제 해결 -->
<Resources cachingAllowed="true" cacheMaxSize="100000"></Resources>
<!-- Default set of monitored resources. If one of these changes, the -->
<!-- web application will be reloaded. -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
</Context>
톰캣 서버 설정 추가(프로젝트내 파일 X , 톰캣 서버파일)
이후 톰캣 서버 재기동
파일 업로드 테이블 생성(3개)
-- 하나의 이미지를 업로드
CREATE TABLE ITEM(
ITEM_ID NUMBER,
ITEM_NAME VARCHAR2(60),
PRICE NUMBER,
DESCRIPTION VARCHAR2(150),
PICTURE_URL VARCHAR2(600),
CONSTRAINT PK_ITEM PRIMARY KEY (ITEM_ID) );
-- 여러개의 이미지 업로드 테이블
-- 제 1정규형 위반
CREATE TABLE ITEM2(
ITEM_ID NUMBER,
ITEM_NAME VARCHAR2(60),
PRICE NUMBER,
DESCRIPTION VARCHAR2(150),
PICTURE_URL VARCHAR2(600),
PICTURE_URL2 VARCHAR2(600),
CONSTRAINT PK_ITEM2 PRIMARY KEY(ITEM_ID) );
-- 제1정규형 준수
CREATE TABLE ITEM3(
ITEM_ID NUMBER,
ITEM_NAME VARCHAR2(60),
PRICE NUMBER,
DESCRIPTION VARCHAR2(150),
CONSTRAINT PK_ITEM3 PRIMARY KEY(ITEM_ID) );
CREATE TABLE ATTACH(
ITEM_ID NUMBER,
SEQ NUMBER,
PICTURE_URL VARCHAR2(600),
PICTURE_SIZE NUMBER,
PITCTURE_TYPE VARCHAR2(100),
REG_DATE DATE,
CONSTRAINT PK_ATTACH PRIMARY KEY(ITEM_ID , SEQ),
CONSTRAINT FK_ATTACH FOREIGN KEY (ITEM_ID) REFERENCES ITEM3(ITEM_ID)
);
ITEM , ITEM2 테이블은 서로 제1정규형에 위배된다 -> 원자성
제 1정규형 : 릴레이션의 속성값이 모두 원자값으로 구성되어야 하며 중복되어서는 안된다.
옵션창에서 종속삭제 제약조건 추가
ItemVO, Item2VO , Item3VO , AttachVO 클래 생성
view -> register.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<h2>REGISTER</h2>
<!--
요청 URI : /item/registerPost
요청 파라미터 : {itemName = 삼성태블릿 , price = 120000 , description=쓸만한 , uploadFile= 파일객체}
요청 방식 : post
-->
<form action="/item/registerPost" method="post">
<table>
<tr>
<th>상품명</th>
<td><input type="text" name="itemName" required="required" placeholder="상품명"></td>
</tr>
<tr>
<th>가격</th>
<td><input type="text" name="price" required="required" placeholder="가격"></td>
</tr>
<tr>
<th>상품이미지</th>
<td><input type="file" name="uploadFile" placeholder="상품이미지"></td>
</tr>
<tr>
<th>개요</th>
<td><input type="text" name="description" placeholder="개요"></td>
</tr>
</table>
<button type="submit">상품 등록</button>
<button type="reset">초기화</button>
</form>
ItemVO클래스에
멤버필드 private MultipartFile uploadFile추가
private MultipartFile uploadFile;
ItemController 클래스 생성
package kr.or.ddit.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
//자바빈 선언 및 관리
@Slf4j
@RequestMapping("/item")
@Controller
public class ItemController {
/*
요청URI : /item/register
요청파라미터 :
요청방식 : get
*/
@GetMapping("register")
public String register() {
// forwarding
return "item/register";
}
}
http://localhost/item/register 페이지 확인
package kr.or.ddit.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import kr.or.ddit.vo.ItemVO;
import lombok.extern.slf4j.Slf4j;
//자바빈 선언 및 관리
@Slf4j
@RequestMapping("/item")
@Controller
public class ItemController {
/*
요청URI : /item/register
요청파라미터 :
요청방식 : get
*/
@GetMapping("/register")
public String register() {
// forwarding
return "item/register";
}
/*
요청URI : /item/registerPost
요청파라미터 : {itemName=삼성태블릿,price=120000,description=쓸만함,uploadFile=파일객체}
요청방식 : post
*/
@PostMapping("/registerPost")
public String regiterPost(ItemVO itemVO) {
log.info("itemVO : " + itemVO);
// 아이템 등록되고 난 뒤 ,
// forwarding : 새로운 URL 요청
return "redirect:/item/detail?itemId="+itemVO.getItemId();
}
}
registerPost 메소드 작성
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<h2>REGISTER</h2>
<!--
요청 URI : /item/registerPost
요청 파라미터 : {itemName = 삼성태블릿 , price = 120000 , description=쓸만한 , uploadFile= 파일객체}
요청 방식 : post
파일업로드
1) method는 꼭 post!
2) enctype="multipart/form-data"
3) <input type="file" name="uploadFile".. name속성이 꼭 있어야 함!
4) <sec:csrfInput />
*5) action 속성의 uri 뒤에 token 추가
-->
<form action="/item/registerPost?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
<table>
<tr>
<th>상품명</th>
<td><input type="text" name="itemName" required="required" placeholder="상품명"></td>
</tr>
<tr>
<th>가격</th>
<td><input type="text" name="price" required="required" placeholder="가격"></td>
</tr>
<tr>
<th>상품이미지</th>
<td><input type="file" name="uploadFile" placeholder="상품이미지"></td>
</tr>
<tr>
<th>개요</th>
<td><input type="text" name="description" placeholder="개요"></td>
</tr>
</table>
<sec:csrfInput/>
<button type="submit">상품 등록</button>
<button type="reset">초기화</button>
</form>
detail 페이지를 만들지 않았기 때문에 404에러가 정상 , 주소창에서 http://localhost/item/detail?itemId=0 으로
itemId받아오는지만 체!
package kr.or.ddit.controller;
import java.io.File;
import java.io.IOException;
import org.springframework.stereotype.Controller;
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 kr.or.ddit.vo.ItemVO;
import lombok.extern.slf4j.Slf4j;
//자바빈 선언 및 관리
@Slf4j
@RequestMapping("/item")
@Controller
public class ItemController {
/*
요청URI : /item/register
요청파라미터 :
요청방식 : get
*/
@GetMapping("/register")
public String register() {
// forwarding
return "item/register";
}
/*
요청URI : /item/registerPost
요청파라미터 : {itemName=삼성태블릿,price=120000,description=쓸만함,uploadFile=파일객체}
요청방식 : post
*/
@PostMapping("/registerPost")
public String regiterPost(ItemVO itemVO) {
// ItemVO[itemId=0,itemName=삼성태블릿,price=120000,description=쓸만함,
// pictureUrl=null,uploadFile=파일객체]
log.info("itemVO : " + itemVO);
//스프링 파일 객체
MultipartFile multipartFile = itemVO.getUploadFile();
log.info("이미지 파일명 : " + multipartFile.getOriginalFilename());
log.info("이미지 크기 : " + multipartFile.getSize());
// MIME(Multipurpose Internet Mail Extensions , 미디어타입) : 문서, 파일 또는 바이트 집합의 성격과 형식(확장자)
// .jpg / .jpeg 의 MIME타입 : image/jpeg
log.info("MIME 타입 : " + multipartFile.getContentType());
String uploadFolder = "c:\\upload";
// 설계
// ,의 역할 : \\
// uploadFolder : c:\\upload + \\ + 개똥이.jpg
// File saveFile = new File(uploadFolder + "\\" + multipartFile.getOriginalFilename());
// ↕↕↕↕↕↕↕ 동일
File saveFile = new File(uploadFolder , multipartFile.getOriginalFilename());
//파일 복사 실행(설계대로)
//스프링파일객체.transferTo(설계)
// 실제 파일을 복사하기 때문에 try-catch로 예외처리 해야한다
try {
multipartFile.transferTo(saveFile);
} catch (IllegalStateException | IOException e) {
log.error(e.getMessage());
}
// redirect : 새로운 URL 요청
return "redirect:/item/detail?itemId="+itemVO.getItemId();
}
}
http://localhost/item/register 접속 -> 책등록 -> 콘솔창 확인
INFO : kr.or.ddit.controller.ItemController - itemVO : ItemVO(itemId=0, itemName=갤럭시, price=12000, description=쓸만해, pictureUrl=null, uploadFile=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@4c3bd9e4)
INFO : kr.or.ddit.controller.ItemController - 이미지 파일명 : 20240120_161244.png
INFO : kr.or.ddit.controller.ItemController - 이미지 크기 : 19658
INFO : kr.or.ddit.controller.ItemController - MIME 타입 : image/png
http://localhost/item/register 접속 -> 책등록
미리 지정한 업로드 경로(c:\\upload)에 파일이 복사되었는지 확인
package kr.or.ddit.controller;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.stereotype.Controller;
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 kr.or.ddit.vo.ItemVO;
import lombok.extern.slf4j.Slf4j;
//자바빈 선언 및 관리
@Slf4j
@RequestMapping("/item")
@Controller
public class ItemController {
/*
요청URI : /item/register
요청파라미터 :
요청방식 : get
*/
@GetMapping("/register")
public String register() {
// forwarding
return "item/register";
}
/*
요청URI : /item/registerPost
요청파라미터 : {itemName=삼성태블릿,price=120000,description=쓸만함,uploadFile=파일객체}
요청방식 : post
*/
@PostMapping("/registerPost")
public String regiterPost(ItemVO itemVO) {
// ItemVO[itemId=0,itemName=삼성태블릿,price=120000,description=쓸만함,
// pictureUrl=null,uploadFile=파일객체]
log.info("itemVO : " + itemVO);
//스프링 파일 객체
MultipartFile multipartFile = itemVO.getUploadFile();
log.info("이미지 파일명 : " + multipartFile.getOriginalFilename());
log.info("이미지 크기 : " + multipartFile.getSize());
// MIME(Multipurpose Internet Mail Extensions , 미디어타입) : 문서, 파일 또는 바이트 집합의 성격과 형식(확장자)
// .jpg / .jpeg 의 MIME타입 : image/jpeg
log.info("MIME 타입 : " + multipartFile.getContentType());
String uploadFolder = "C:\\Users\\PC-12\\git\\repository\\springProj\\src\\main\\webapp\\resources\\upload";
//연월일 폴더 생성 설계
// ... \\upload \\ 2024 \\ 01 \\ 30
File uploadPath = new File(uploadFolder , getFolder());
//연월일 폴더 생성 실행
if(uploadPath.exists() == false) {
uploadPath.mkdirs();
}
String uploadFileName = multipartFile.getOriginalFilename();
// 파일명 중복 방지*
//같은 날 같은 이미지 업로드 시 파일명 중복 방지 시작----------------
//java.util.UUID => 랜덤값 생성
UUID uuid = UUID.randomUUID();
//원래의 파일 이름과 구분하기 위해 _를 붙임(sdafjasdlfksadj_개똥이.jpg)
uploadFileName = uuid.toString() + "_" + uploadFileName;
//같은 날 같은 이미지 업로드 시 파일 중복 방지 끝----------------
// 설계
// , 의 역할 : \\
// uploadFolder : ...upload\\2024\\01\\30 + \\ + 개똥이.jpg
// File saveFile = new File(uploadFolder + "\\" + multipartFile.getOriginalFilename());
// ↕↕↕↕↕↕↕ 동일
File saveFile = new File(uploadPath , uploadFileName);
//파일 복사 실행(설계대로)
//스프링파일객체.transferTo(설계)
// 실제 파일을 복사하기 때문에 try-catch로 예외처리 해야한다
try {
multipartFile.transferTo(saveFile);
} catch (IllegalStateException | IOException e) {
log.error(e.getMessage());
}
// redirect : 새로운 URL 요청
return "redirect:/item/detail?itemId="+itemVO.getItemId();
}
public String getFolder() {
// 2024-01-30 형식(format) 지정
// 간단한 날짜 형식
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 날짜 객체 생성(java.util 패키지)
Date date = new Date();
// 2024-01-30 -> 2024\\01\\30
String str = sdf.format(date);
return str.replace("-", File.separator);
// file.separator -> 파일 이름 구분자 (2024\\01\\30)
}
}
http://localhost/item/register 접속하여 등록후 uploadFolder에 해당하는 경로 가서 파일복사 확인
uuid까지 확인
uploadFolder는 자주 사용되는 경로이기때문에 빈객체로 등록하여 관
<bean id="uploadFolder" class="java.lang.String">
<constructor-arg value="C:\\Users\\PC-12\\git\\repository\\springProj\\src\\main\\webapp\\resources\\upload"></constructor-arg>
</bean>
root-context.xml 에 bean객체 등록 후 ItemController에서 @Autowired 선언
썸네일 처리
썸네일은 이미지만 가능하기때문에 이미지인지 먼저 체크
ItemController에 아래 메소드(checkImageType) 추가
//이미지인지 판단. 썸네일은 이미지만 가능하므로..
public boolean checkImageType(File file) {
//MIME(Multipurpose Internet Mail Extensions) : 문서, 파일 또는 바이트 집합의 성격과 형식. 표준화
//MIME 타입 알아냄. .jpeg / .jpg의 MIME타입 : image/jpeg
String contentType;
try {
contentType = Files.probeContentType(file.toPath());
log.info("contentType : " + contentType);
//image/jpeg는 image로 시작함->true
return contentType.startsWith("image");
} catch (IOException e) {
e.printStackTrace();
}
//이 파일이 이미지가 아닐 경우
return false;
}
package kr.or.ddit.controller;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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 kr.or.ddit.vo.ItemVO;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnailator;
//자바빈 선언 및 관리
@Slf4j
@RequestMapping("/item")
@Controller
public class ItemController {
//DI 의존성 주입 / IoC(제어역전)
@Autowired
String uploadFolder;
/*
요청URI : /item/register
요청파라미터 :
요청방식 : get
*/
@GetMapping("/register")
public String register() {
// forwarding
return "item/register";
}
/*
요청URI : /item/registerPost
요청파라미터 : {itemName=삼성태블릿,price=120000,description=쓸만함,uploadFile=파일객체}
요청방식 : post
*/
@PostMapping("/registerPost")
public String regiterPost(ItemVO itemVO) {
// ItemVO[itemId=0,itemName=삼성태블릿,price=120000,description=쓸만함,
// pictureUrl=null,uploadFile=파일객체]
log.info("itemVO : " + itemVO);
//스프링 파일 객체
MultipartFile multipartFile = itemVO.getUploadFile();
log.info("이미지 파일명 : " + multipartFile.getOriginalFilename());
log.info("이미지 크기 : " + multipartFile.getSize());
// MIME(Multipurpose Internet Mail Extensions , 미디어타입) : 문서, 파일 또는 바이트 집합의 성격과 형식(확장자)
// .jpg / .jpeg 의 MIME타입 : image/jpeg
log.info("MIME 타입 : " + multipartFile.getContentType());
String uploadFolder = "C:\\Users\\PC-12\\git\\repository\\springProj\\src\\main\\webapp\\resources\\upload";
//연월일 폴더 생성 설계
// ... \\upload \\ 2024 \\ 01 \\ 30
File uploadPath = new File(uploadFolder , getFolder());
//연월일 폴더 생성 실행
if(uploadPath.exists() == false) {
uploadPath.mkdirs();
}
String uploadFileName = multipartFile.getOriginalFilename();
// 파일명 중복 방지*
//같은 날 같은 이미지 업로드 시 파일명 중복 방지 시작----------------
//java.util.UUID => 랜덤값 생성
UUID uuid = UUID.randomUUID();
//원래의 파일 이름과 구분하기 위해 _를 붙임(sdafjasdlfksadj_개똥이.jpg)
uploadFileName = uuid.toString() + "_" + uploadFileName;
//같은 날 같은 이미지 업로드 시 파일 중복 방지 끝----------------
// 설계
// , 의 역할 : \\
// uploadFolder : ...upload\\2024\\01\\30 + \\ + 개똥이.jpg
// File saveFile = new File(uploadFolder + "\\" + multipartFile.getOriginalFilename());
// ↕↕↕↕↕↕↕ 동일
File saveFile = new File(uploadPath , uploadFileName);
//파일 복사 실행(설계대로)
//스프링파일객체.transferTo(설계)
// 실제 파일을 복사하기 때문에 try-catch로 예외처리 해야한다
try {
multipartFile.transferTo(saveFile);
// 썸네일 처리
// 이미지만 가능하기때문에 이미지인지 사전체크
if(checkImageType(saveFile)) { // 이미지가 맞다면
// 설계
FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath , "s_"+ uploadFileName) );
//썸네일 생성
Thumbnailator.createThumbnail(multipartFile.getInputStream() , thumbnail , 100 , 100);
thumbnail.close();
};
} catch (IllegalStateException | IOException e) {
log.error(e.getMessage());
}
// redirect : 새로운 URL 요청
return "redirect:/item/detail?itemId="+itemVO.getItemId();
}
public String getFolder() {
// 2024-01-30 형식(format) 지정
// 간단한 날짜 형식
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 날짜 객체 생성(java.util 패키지)
Date date = new Date();
// 2024-01-30 -> 2024\\01\\30
String str = sdf.format(date);
return str.replace("-", File.separator);
// file.separator -> 파일 이름 구분자 (2024\\01\\30)
}
//이미지인지 판단. 썸네일은 이미지만 가능하므로..
public boolean checkImageType(File file) {
//MIME(Multipurpose Internet Mail Extensions) : 문서, 파일 또는 바이트 집합의 성격과 형식. 표준화
//MIME 타입 알아냄. .jpeg / .jpg의 MIME타입 : image/jpeg
String contentType;
try {
contentType = Files.probeContentType(file.toPath());
log.info("contentType : " + contentType);
//image/jpeg는 image로 시작함->true
return contentType.startsWith("image");
} catch (IOException e) {
e.printStackTrace();
}
//이 파일이 이미지가 아닐 경우
return false;
}
}
DB로 테스트
package kr.or.ddit.controller;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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 kr.or.ddit.vo.ItemVO;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnailator;
//자바빈 선언 및 관리
@Slf4j
@RequestMapping("/item")
@Controller
public class ItemController {
//DI 의존성 주입 / IoC(제어역전)
@Autowired
String uploadFolder;
/*
요청URI : /item/register
요청파라미터 :
요청방식 : get
*/
@GetMapping("/register")
public String register() {
// forwarding
return "item/register";
}
/*
요청URI : /item/registerPost
요청파라미터 : {itemName=삼성태블릿,price=120000,description=쓸만함,uploadFile=파일객체}
요청방식 : post
*/
@PostMapping("/registerPost")
public String regiterPost(ItemVO itemVO) {
// ItemVO[itemId=0,itemName=삼성태블릿,price=120000,description=쓸만함,
// pictureUrl=null,uploadFile=파일객체]
log.info("itemVO : " + itemVO);
//스프링 파일 객체
MultipartFile multipartFile = itemVO.getUploadFile();
log.info("이미지 파일명 : " + multipartFile.getOriginalFilename());
log.info("이미지 크기 : " + multipartFile.getSize());
// MIME(Multipurpose Internet Mail Extensions , 미디어타입) : 문서, 파일 또는 바이트 집합의 성격과 형식(확장자)
// .jpg / .jpeg 의 MIME타입 : image/jpeg
log.info("MIME 타입 : " + multipartFile.getContentType());
String uploadFolder = "C:\\Users\\PC-12\\git\\repository\\springProj\\src\\main\\webapp\\resources\\upload";
//연월일 폴더 생성 설계
// ... \\upload \\ 2024 \\ 01 \\ 30
File uploadPath = new File(uploadFolder , getFolder());
//연월일 폴더 생성 실행
if(uploadPath.exists() == false) {
uploadPath.mkdirs();
}
String uploadFileName = multipartFile.getOriginalFilename();
// 파일명 중복 방지*
//같은 날 같은 이미지 업로드 시 파일명 중복 방지 시작----------------
//java.util.UUID => 랜덤값 생성
UUID uuid = UUID.randomUUID();
//원래의 파일 이름과 구분하기 위해 _를 붙임(sdafjasdlfksadj_개똥이.jpg)
uploadFileName = uuid.toString() + "_" + uploadFileName;
//같은 날 같은 이미지 업로드 시 파일 중복 방지 끝----------------
// 설계
// , 의 역할 : \\
// uploadFolder : ...upload\\2024\\01\\30 + \\ + 개똥이.jpg
// File saveFile = new File(uploadFolder + "\\" + multipartFile.getOriginalFilename());
// ↕↕↕↕↕↕↕ 동일
File saveFile = new File(uploadPath , uploadFileName);
//파일 복사 실행(설계대로)
//스프링파일객체.transferTo(설계)
// 실제 파일을 복사하기 때문에 try-catch로 예외처리 해야한다
try {
multipartFile.transferTo(saveFile);
// 썸네일 처리
// 이미지만 가능하기때문에 이미지인지 사전체크
if(checkImageType(saveFile)) { // 이미지가 맞다면
// 설계
FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath , "s_"+ uploadFileName) );
//썸네일 생성 -> 기존 이미지를 100*100 사이즈로 축소시킨다
Thumbnailator.createThumbnail(multipartFile.getInputStream() , thumbnail , 100 , 100);
thumbnail.close();
};
// ITEM 테이블에 반영
//ItemVO[itemId=0,itemName=삼성태블릿,price=120000,description=쓸만함
// pictureUrl=null,uploadFile=파일객체]
itemVO.setItemId(0);//자동 데이터 생성
// 웹경로
// getFolder().replace("\\" , "/") : 2024/01/30
// /2024/01/30 sdafjasdlfksadj_개똥이.jpg
itemVO.setPictureUrl(
"/" + getFolder().replace("\\" , "/") + "/" + uploadFileName
);
// uuid가 적용된 파일명
} catch (IllegalStateException | IOException e) {
log.error(e.getMessage());
}
// redirect : 새로운 URL 요청
return "redirect:/item/detail?itemId="+itemVO.getItemId();
}
public String getFolder() {
// 2024-01-30 형식(format) 지정
// 간단한 날짜 형식
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 날짜 객체 생성(java.util 패키지)
Date date = new Date();
// 2024-01-30 -> 2024\\01\\30
String str = sdf.format(date);
return str.replace("-", File.separator);
// file.separator -> 파일 이름 구분자 (2024\\01\\30)
}
//이미지인지 판단. 썸네일은 이미지만 가능하므로..
public boolean checkImageType(File file) {
//MIME(Multipurpose Internet Mail Extensions) : 문서, 파일 또는 바이트 집합의 성격과 형식. 표준화
//MIME 타입 알아냄. .jpeg / .jpg의 MIME타입 : image/jpeg
String contentType;
try {
contentType = Files.probeContentType(file.toPath());
log.info("contentType : " + contentType);
//image/jpeg는 image로 시작함->true
return contentType.startsWith("image");
} catch (IOException e) {
e.printStackTrace();
}
//이 파일이 이미지가 아닐 경우
return false;
}
}
package kr.or.ddit.service 경로로 ItemService 인터페이스 생성 -> 구현클래스 생성 -> DI주입
ItemMapper 인터페이스 생성 후 ItemMapper SQL파일 생
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.or.ddit.mapper.ItemMapper">
<!-- 매퍼 인터페이스 사용시 네임스페이스는 패키지 풀네임을 적어준다 -->
<!-- public int registerPost(ItemVO itemVO) -->
<!-- public int registerPost(ItemVO itemVO)
들어옴 : ItemVO[itemId=0,itemName=삼성태블릿,price=120000,description=쓸만함
pictureUrl=null,uploadFile=/2024/01/30/dsfak_개똥이.jpg]
나감 : ItemVO[itemId=1,itemName=삼성태블릿,price=120000,description=쓸만함
pictureUrl=null,uploadFile=/2024/01/30/dsfak_개똥이.jpg]
public int registerPost(ItemVO itemVO)
들어옴 : ItemVO[itemId=0,itemName=삼성태블릿,price=120000,description=쓸만함
pictureUrl=/2024/01/30/dsfak_개똥이.jpg,uploadFile=파일객체]
나감 : ItemVO[itemId=1,itemName=삼성태블릿,price=120000,description=쓸만함
pictureUrl=/2024/01/30/dsfak_개똥이.jpg,uploadFile=]
-->
<insert id="registerPost" parameterType="itemVO">
<selectKey resultType="int" order="BEFORE" keyProperty="itemId">
select NVL(max(item_id) , 0) + 1 from item
</selectKey>
insert into item( ITEM_ID, ITEM_NAME, PRICE, DESCRIPTION, PICTURE_URL)
values (#{itemId}, #{itemName}, #{price}, #{description}, #{pictureUrl} )
</insert>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--
[마이바티스] 스프링에서 "_"를 사용한 컬럼명을 사용 시(BOOK 테이블의 BOOK_ID)
카멜케이스로 읽어줌(bookId)
ex) 테이블 컬러명이 member_id인 경우 jsp화면단에서 이 값을 사용 시 memberId로 사용
-->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 자주 사용하는 타입의 별칭을 세팅 -->
<typeAliases>
<typeAlias type="kr.or.ddit.vo.BookVO" alias="bookVO" />
<typeAlias type="kr.or.ddit.vo.StudVO" alias="studVO" />
<typeAlias type="kr.or.ddit.vo.StudAuthVO" alias="studAuthVO" />
<typeAlias type="kr.or.ddit.vo.LprodVO" alias="lprodVO" />
<typeAlias type="kr.or.ddit.vo.HobbyVO" alias="hobbyVO" />
<typeAlias type="kr.or.ddit.vo.MemberVO" alias="memberVO" />
<typeAlias type="kr.or.ddit.vo.MemberAuthVO" alias="memberAuthVO" />
<typeAlias type="kr.or.ddit.vo.ItemVO" alias="itemVO" />
<typeAlias type="kr.or.ddit.vo.Item2VO" alias="item2VO" />
<typeAlias type="kr.or.ddit.vo.Item3VO" alias="item3VO" />
<typeAlias type="kr.or.ddit.vo.AttachVO" alias="attachVO" />
</typeAliases>
</configuration>
mybatisAlias에 별칭 추가(ItemVO~AttachVO)
DB에서 쿼리문 자바로 옮겨올때 세미콜론 확인 필수!
http://localhost/item/register 접속후 등록하여 DB로 데이터 넘어가는지 확인(상세페이지 없어서 404에러 뜨면 정상)
주소페이지 http://localhost/item/detail?itemId=1 인 확인 / 콘솔 로그출력메시지 확인