boardList.html
<a href="/board/openBoardWrite.do" class="btn">글쓰기</a>
**boardWrite.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<title>게시판</title>
<link rel="stylesheet" th:href="@{/css/board.css}" />
</html>
<body>
<div class="container">
<h2>게시판 등록</h2>
<form id="form" method="post" action="/board/insertBoard.do">
<table class="board_detail">
<tr>
<td>제목</td>
<td><input type="text" id="title" name="title" /></td>
</tr>
<tr>
<td colspan="2"><textarea id="contents" name="contents"></textarea></td>
</tr>
</table>
<input type="submit" id="submit" value="저장" class="btn" />
</form>
</div>
</body>
BoardController
// 글쓰기 화면 요청을 처리하는 메서드
@GetMapping("/board/openBoardWrite.do")
public String openBoardWrite() throws Exception {
return "/board/boardWrite"; // 라우팅
}
// 글 저장 요청을 처리하는 메서드
@PostMapping("/board/insertBoard.do")
public String insertBoard(BoardDto boardDto) throws Exception {
boardService.insertBoard(boardDto);
return "redirect:/board/openBoardList.do"; // 리다이렉션 (요청을 다른 요청으로 바꾸는 것)
}
BoardService 인터페이스
public interface BoardService {
List<BoardDto> selectBoardList();
void insertBoard(BoardDto boardDto);
}
BoardServiceImpl 구현 클래스
@Override
public void insertBoard(BoardDto boardDto) {
// 로그인한 사용자를 글쓴이로 설정
// TODO. 로그인한 사용자의 ID로 변경
boardDto.setCreatedId("hong");
boardMapper.insertBoard(boardDto);
BoardMapper 인터페이스
@Mapper
public interface BoardMapper {
List<BoardDto> selectBoardList();
void insertBoard(BoardDto boardDto);
}
sql-board.xml
<!--
외부 입력값을 쿼리에 반영할 경우, #{ }을 이용해야 SQL 인젝션 공격을 방어할 수 있음
-->
<insert id="insertBoard" parameterType="board.dto.BoardDto">
insert into t_board(title, contents, created_dt, created_id)
values (#{title}, #{contents}, now(), #{createdId})
</insert>
저장 누르면
302 코드 - 클라이언트 안 거치고 서버에서 redirect된다.
cf
목록 화면에서 게시물 제목을 클릭하면 상세 화면으로 이동하고, 상세 내용을 조회하면 조회수를 증가
boardList.html
<!--
<td th:text="${list.title}" class="title">게시판 글 제목</td>
-->
<td class="title">
<a href="/board/openBoardDetail.do?boardIdx="
th:attrappend="href=${list.boardIdx}" th:text="${list.title}"></a>
</td>
BoardController
// 상세 조회 요청을 처리하는 메서드
// /board/openBoardDetail.do?boardIdx=1234
@GetMapping("/board/openBoardDetail.do")
public ModelAndView openBoardDetail(@RequestParam("boardInx") int boardIdx) throws Exception {
BoardDto boardDto = boardService.selectBoardDetail(boardIdx);
ModelAndView mv = new ModelAndView("/board/boardDetail");
mv.addObject("board", boardDto);
return mv;
}
쿼리스트링 뽑아내려면 @RequestParam("")
사용한다
public interface BoardService {
List<BoardDto> selectBoardList();
void insertBoard(BoardDto boardDto);
BoardDto selectBoardDetail(int boardIdx);
}
BoardDto selectBoardDetail(int boardIdx);
추가
@Override
public BoardDto selectBoardDetail(int boardIdx) {
return boardMapper.selectBoardDetail(boardIdx);
}
@Mapper
public interface BoardMapper {
List<BoardDto> selectBoardList();
void insertBoard(BoardDto boardDto);
BoardDto selectBoardDetail(int boardIdx);
}
<select id="selectBoardDetail" parameterType="int" resultType="board.dto.BoardDto">
select board_idx, title, hit_cnt, date_format(created_dt, '%Y.%m.%d %H:%i:%s') as created_dt,
contents, created_id
from t_board
where deleted_yn = 'N' and board_idx = #{boardIdx}
</select>
boardDetail.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<title>게시판</title>
<link rel="stylesheet" th:href="@{/css/board.css}" />
</html>
<body>
<div class="container">
<h2>게시판 상세</h2>
<form id="form" method="post" action="/board/insertBoard.do">
<table class="board_detail">
<colgroup>
<col width="15%" />
<col width="*" />
<col width="15%" />
<col width="35%" />
</colgroup>
<tr>
<th>글번호</th>
<td th:text="${board.boardIdx}"></td>
<th>조회수</th>
<td th:text="${board.hitCnt"></td>
</tr>
<tr>
<th>작성자</th>
<td th:text="${board.createdId}"></td>
<th>작성일</th>
<td th:text="${board.createdDt}"></td>
</tr>
<tr>
<th>제목</th>
<td colspan="3"><input type="text" id="title" name="title" th:value="${board.title}"/></td>
</tr>
<tr>
<td colspan="4"><textarea id="contents" name="contents" th:text="${board.contents}"</td>
</tr>
</table>
<input type="submit" id="submit" value="저장" class="btn" />
</form>
</div>
</body>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<title>게시판</title>
<link rel="stylesheet" th:href="@{/css/board.css}" />
</html>
<body>
<div class="container">
<h2>게시판 상세</h2>
<form id="frm" method="post" action="/board/insertBoard.do">
<table class="board_detail">
<colgroup>
<col width="15%" />
<col width="*" />
<col width="15%" />
<col width="35%" />
</colgroup>
<tr>
<th>글번호</th>
<td th:text="${board.boardIdx}"></td>
<th>조회수</th>
<td th:text="${board.hitCnt}"></td>
</tr>
<tr>
<th>작성자</th>
<td th:text="${board.createdId}"></td>
<th>작성일</th>
<td th:text="${board.createdDt}"></td>
</tr>
<tr>
<th>제목</th>
<td colspan="3"><input type="text" id="title" name="title" th:value="${board.title}"/></td>
</tr>
<tr>
<td colspan="4"><textarea id="contents" name="contents" th:text="${board.contents}"</td>
</tr>
</table>
</form>
<input type="button" id="list" class="btn" value="목록으로" />
<input type="button" id="update" class="btn" value="수정하기" />
<input type="button" id="delete" class="btn" value="삭제하기" />
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$(function() {
$("#list").on("click", function() {
location.href = "openBoardList.do";
});
$("#update").on("click", function() {
let frm = $("#frm")[0];
frm.action = "updateBoard.do";
frm.submit();
});
$("#delete").on("click", function() {
let frm = $("#frm")[0];
frm.action = "deleteBoard.do";
frm.submit();
});
});
</script>
</div>
</body>
// 수정 요청을 처리할 메서드
@PostMapping("/board/updateBoard.do")
public String updateBoard(BoardDto boardDto) throws Exception {
boardService.updateBoard(boardDto);
return "redirect:/board/openBoardList.do";
}
// 삭제 요청을 처리할 메서드
@PostMapping("/board/deleteBoard.do")
public String deleteBoard(@RequestParam("boardIdx") int boardIdx) throws Exception {
boardService.deleteBoard(boardIdx);
return "redirect:/board/openBoardList.do";
}
void updateBoard(BoardDto boardDto);
void deleteBoard(int boardIdx);
@Override
public void updateBoard(BoardDto boardDto) {
// TODO. 로구인한 사용자 아이디로 변경
boardDto.setUpdatorId("go");
boardMapper.updateBoard(boardDto);
}
@Override
public void deleteBoard(int boardIdx) {
BoardDto boardDto = new BoardDto();
// TODO. 로그인한 사용자 아이디로 변경
boardDto.setUpdatorId("go");
boardMapper.deleteBoard(boardDto);
}
void updateBoard(BoardDto boardDto);
void deleteBoard(BoardDto boardDto);
<update id="updateBoard" parameterType="board.dto.BoardDto">
update t_board
set title = #{title}
, contents = #{contents}
, updator_dt = now()
, updator_id = #{updator_id}
where board_idx = #{boardIdx}
</update>
<delete id="deleteBoard" parameterType="board.dto.BoardDto">
update t_board
set delete_yn = 'Y'
, contents = #{contents}
, updator_dt = now()
, updator_id = #{updator_id}
where board_idx = #{boardIdx}
</delete>
@Override
public BoardDto selectBoardDetail(int boardIdx) {
boardMapper.updateHitCnt(boardIdx);
return boardMapper.selectBoardDetail(boardIdx);
}
void updateHitCnt(int boardIdx);
<update id="updateHitCnt" parameterType="int">
update t_board
set hit_cnt = hit_cnt + 1
where board_idx = #{boardIdx}
</update>
여기까지 CRUD 를 Spring MVC 패턴으로 만들어보았다 !!!!!! 이제 부가 기능을 만들자
logging.level.root=OFF
logging.level.board=DEBUG
logging.pattern.console=%d{HH:mm:ss.SSS} %highlight(%-5p) %cyan(%c) %m%n
package board;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BoardApplication {
private static Logger log = LoggerFactory.getLogger(BoardApplication.class);
public static void main(String[] args) {
log.trace("trace");
log.debug("debug");
log.info("info");
log.warn("warn");
log.error("error");
SpringApplication.run(BoardApplication.class, args);
}
}
@Slf4j
@SpringBootApplication
public class BoardApplication {
//private static Logger log = LoggerFactory.getLogger(BoardApplication.class);
public static void main(String[] args) {
log.trace("trace");
log.debug("debug");
log.info("info");
log.warn("warn");
log.error("error");
SpringApplication.run(BoardApplication.class, args);
}
}
private static Logger log = LoggerFactory.getLogger(BoardApplication.class);
대신
@Slf4j
를 사용할 수 있다.
@SpringBootApplication
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}
이렇게 쓰면 된다!
현재 로그 설정 상태에서도 쿼리문의 구조, 적용할 값, 결과가 로그로 출력되는 것을 확인
=> but, 읽기 어렵고 값이 대입된 상태의 쿼리를 확인할 수 없음
JDBC 드라이버의 SQL 활동을 콘솔 또는 파일에 출력하는 용도로 사용
SQL 쿼리와 데이터베이스 상호 작용을 투명하게 추적하는 것이 가능하다 !
SQL 쿼리 실행 시간을 측정하고, 실행된 쿼리를 기록하며, 디버깅 및 성능 튜닝을 용이하게 함
implementation group: 'org.bgee.log4jdbc-log4j2', name: 'log4jdbc-log4j2-jdbc4.1', version: '1.16'
resources/log4jdbc.log4j2.properties
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0
#spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/springbootdb?useUnicode=true&characterEncoding=utf-8&serverTimeZone=Asia/Seoul
spring.datasource.hikari.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.hikari.jdbc-url=jdbc:log4jdbc:mysql://localhost:3306/springbootdb?useUnicode=true&characterEncoding=utf-8&serverTimeZone=Asia/Seoul
logging.level.jdbc.sqlonly=info
logging.level.jdbc.resultsettable=info
이렇게 예쁘게 로그가 나온다 !
jdbc.sqlonly SQL을 출력
Prepared Statement의 경우, 관련된 파라미터는 자동으로 변경되어 출력
jdbc.sqltiming SQL 문과 해당 SQL 문의 실행 시간을 밀리초 단위로 출력
jdbc.audit ResultSet을 제외한 모든 JDBC 호출 정보를 출력 (잘 안 쓴다.)
jdbc.resultset ResultSet을 포함한 모든 JDBC 호출 정보를 출력 (잘 안 쓴다.)
jdbc.resultsettable SQL 문의 조회 결과를 테이블 형식으로 출력 (잘 안 쓴다.)
jdbc.connection Connection의 연결과 종료에 관련된 로그를 출력
int id = 100;
Statement statement = Connection.createStatement();
String sql = "select * from t_board where id = " + id; ⇐ 쿼리 생성을 개발자가 담당
statement.executeQuery(sql);
int id = 100;
String sql = "select * from t_board where id = ?";
PreparedStatement statement = Connection.prepareStatement(sql);
statement.setInt(1, id); ⇐ 쿼리 생성을 PreparedStatement가 담당
statement.executeQuery();
스프링 프레임워크에서 특정 요청을 가로채고 처리하기 위해 사용되는 도구
주로 요청 전후에 공통된 작업을 실행하거나, 요청을 처리하는 컨트롤러로 가기 전에 요청을 변경 또는 검사할 때 사용
스프링 MVC에서 인터셉터는 HandlerInterceptor 인터페이스를 상속받아서 구현
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
afterCompletion()
board/interceptor/LoggerInterceptor
package board.interceptor;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LoggerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
log.debug("************** START **************");
log.debug(" Request URI \t" + request.getRequestURI());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
log.debug("************** END **************");
}
}
스프링 4.0 이상에서는 자바 기반 설정을 지원
WebMvcConfigurer 인터페이스를 상속받는 설정 클래스를 추가
configuration/WebMvcConfiguration
package board.configuration;
import board.interceptor.LoggerInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggerInterceptor());
}
}
컨트롤러, 서비스, 매퍼의 메서드가 호출될 때 각 메서드의 경로와 이름을 로그로 출력하는 공통 모듈을 구현
build.gradle
implementation 'org.springframework:spring-aspects:6.2.2'
aop.LoggerAspect
package board.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
// SpringBoot에서는 @EnableAspectJAutoProxy 어노테이션을 추가하지 않아도 자동으로 AOP 설정을 활성화
@Aspect
@Slf4j
@Component
public class LoggerAspect {
@Pointcut("execution(* board..controller.*Controller.*(..)) || execution(* board..service.*ServiceImpl.*(..)) || execution(* board..mapper.*Mapper.*(..))")
private void loggerTarget() {
}
@Around("loggerTarget()")
public Object logPrinter(ProceedingJoinPoint joinPoint) throws Throwable {
String type = "";
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
if (className.indexOf("Controller") > -1) {
type = "[Controller]";
} else if (className.indexOf("Service") > -1) {
type = "[Service]";
} else if (className.indexOf("Mapper") > -1) {
type = "[Mapper]";
}
log.debug(type + " " + className + "." + methodName);
return joinPoint.proceed();
}
}
게시판 상세 조회 -> 조회수를 증가시키고, 게시판 정보를 조회
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
두 가지가 함께 처리되어야 함 => 트랜잭션 적용 대상
@Override
public BoardDto selectBoardDetail(int boardIdx) {
boardMapper.updateHitCnt(boardIdx);
int i = 10 / 0; ⇐ 오류를 발생시키는 코드를 추가
return boardMapper.selectBoardDetail(boardIdx);
}
게시판 목록 조회 -> 조회수를 확인 -> 게시판 상세 조회 -> 오류 발생 -> 게시판 목록 조회 -> 조회수를 확인
~~~~~~~~~~~~~~~~~~~~~~~~~~
조회수 증가를 확인
=> 트랜잭션 처리가 되고 있지 않음
@Transactoinal
선언적 트랜잭션 관리 => @Transactional 어노테이션을 이용해서 트랜잭션을 적용
어노테이션만 사용하면 되기 때문에 쉽게 적용할 수 있음
원하는 클래스 또는 메서드 단위로 트랜잭션을 설정하는 것이 가능
새로운 메서드 또는 클래스를 만들 때마다 어노테이션을 적용해야 함
외부 라이브러리를 사용하는 경우, 해당 라이브러리 코드를 편집할 수 없는 제한 사항이 발생 => AOP를 이용해서 트랜잭션을 구현
트랜잭션 => 한꺼번에 처리한다
@Transactional
@Service
public class BoardServiceImpl implements BoardService {
... (생략) ...
게시판 목록 조회 → 조회수 확인 → 게시판 상세 조회 → 오류 발생 → 게시판 목록 조회 → 조회수 확인
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
조회수가 증가하지 않는 것을 확인
=> rollback
=> 트랜잭션 처리가 되고 있음
// @Transactional
@Service
public class BoardServiceImpl implements BoardService {
@Transactional
@Override
public BoardDto selectBoardDetail(int boardIdx) {
boardMapper.updateHitCnt(boardIdx);
int i = 10 / 0; // <= 오류가 나는 코드
return boardMapper.selectBoardDetail(boardIdx);
}
... (생략) ...
뒤로가기를 해도 조회수가 동일하다. => 트랜잭션이 잘 적용되는구나 !
aop/TransactionAspect
package board.aop;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.interceptor.*;
import java.util.Arrays;
@Configuration
public class TransactionAspect {
// 트랜잭션 관리자
@Autowired
private PlatformTransactionManager transationManager;
// 트랜잭션 인터셉터 정의
// 트랜잭션 관리자를 사용해서 트랜잭션 시작, 커밋, 롤백 등의 처리를 수행
@Bean
TransactionInterceptor transactionAdvice() {
TransactionInterceptor transactionInterceptor = new TransactionInterceptor();
transactionInterceptor.setTransactionManager(transationManager);
// 모든 메서드에 동일한 트랜잭션 속성을 적용
MatchAlwaysTransactionAttributeSource source = new MatchAlwaysTransactionAttributeSource();
// 트랜잭션 속성을 정의 -> 트랜잭션 이름, 롤백 규칙
RuleBasedTransactionAttribute transactionAttribute = new RuleBasedTransactionAttribute();
transactionAttribute.setName("*");
transactionAttribute.setRollbackRules(Arrays.asList(new RollbackRuleAttribute(Exception.class)));
source.setTransactionAttribute(transactionAttribute);
transactionInterceptor.setTransactionAttributeSource(source);
return transactionInterceptor;
}
// AOP 포인트컷과 어드바이저 설정
@Bean
Advisor transactionAdviceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* board..service.*Impl.*(..))");
return new DefaultPointcutAdvisor(pointcut, transactionAdvice());
}
}
// @Transactional
@Service
public class BoardServiceImpl implements BoardService {
@Transactional
@Override
public BoardDto selectBoardDetail(int boardIdx) {
boardMapper.updateHitCnt(boardIdx);
int i = 10 / 0;
return boardMapper.selectBoardDetail(boardIdx);
}
... (생략) ...
create table t_file (
idx int(10) unsigned not null auto_increment comment '일련번호',
board_idx int(10) unsigned not null comment '게시글 번호',
original_file_name varchar(255) not null comment '원본 파일 이름',
stored_file_path varchar(500) not null comment '파일 저장 경로',
file_size int(15) unsigned not null comment '파일 크기',
created_id varchar(50) not null comment '작성자 아이디',
created_dt datetime not null comment '작성 일시',
updator_id varchar(50) null comment '수정자 아이디',
updator_dt datetime null comment '수정 시간',
deleted_yn char(1) not null default 'N' comment '삭제 여부',
primary key (idx));
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=/Users/letthem/uploads
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=30MB
터미널에서 mkdir -p ~/uploads
파일을 포함하면 무조건 multipart/form-data
이걸 써야한다.
enctype="multipart/form-data"
<input type="file" id="files" name="files" multiple="multiple" />
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<title>게시판</title>
<link rel="stylesheet" th:href="@{/css/board.css}" />
</html>
<body>
<div class="container">
<h2>게시판 등록</h2>
<form id="frm" method="post" action="/board/insertBoard.do" enctype="multipart/form-data">
<table class="board_detail">
<tr>
<td>제목</td>
<td><input type="text" id="title" name="title" /></td>
</tr>
<tr>
<td colspan="2"><textarea id="contents" name="contents"></textarea></td>
</tr>
</table>
<input type="file" id="files" name="files" multiple="multiple" />
<input type="submit" id="submit" value="저장" class="btn" />
</form>
</div>
</body>
// 글 저장 요청을 처리하는 메서드
@PostMapping("/board/insertBoard.do")
public String insertBoard(BoardDto boardDto, MultipartHttpServletRequest request) throws Exception {
boardService.insertBoard(boardDto, request);
return "redirect:/board/openBoardList.do"; // 리다이렉션 (요청을 다른 요청으로 바꾸는 것)
}
void insertBoard(BoardDto boardDto, MultipartHttpServletRequest request);
// @Transactional
@Slf4j
@Service
public class BoardServiceImpl implements BoardService {
@Autowired
private BoardMapper boardMapper;
@Override
public List<BoardDto> selectBoardList() {
return boardMapper.selectBoardList();
}
@Override
public void insertBoard(BoardDto boardDto, MultipartHttpServletRequest request) {
// 로그인한 사용자를 글쓴이로 설정
// TODO. 로그인한 사용자의 ID로 변경
// boardDto.setCreatedId("hong");
// boardMapper.insertBoard(boardDto);
if (!ObjectUtils.isEmpty(request)) {
// <input type="file" name="이 속성의 값" />
Iterator<String> fileTagNames = request.getFileNames();
while(fileTagNames.hasNext()) {
String fileTagName = fileTagNames.next();
// 하나의 <input type="file" multiple="multiple"> 태그를 통해서 전달된 파일들을 가져옮
List<MultipartFile> files = request.getFiles(fileTagName);
for (MultipartFile file : files) {
log.debug("File Information");
log.debug("- file name: " + file.getOriginalFilename());
log.debug("- file size: " + file.getSize());
log.debug("- content type: " + file.getContentType());
}
}
}
}
... (생략) ...
글쓰기 페이지에서 파일 첨부 후 저장했을 때 업로드한 파일 정보가 출력되는 것을 확인
package board.dto;
import lombok.Data;
@Data
public class BoardFileDto {
private int idx;
private int boardIdx;
private String originalFileName;
private String storedFilePath;
private String fileSize;
}
package board.common;
import java.io.File;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import board.dto.BoardFileDto;
@Component
public class FileUtils {
@Value("${spring.servlet.multipart.location}")
private String uploadDir;
// 요청을 통해서 전달받은 파일을 저장하고, 파일 정보를 반환하는 메서드
public List<BoardFileDto> parseFileInfo(int boardIdx, MultipartHttpServletRequest request) throws Exception {
if (ObjectUtils.isEmpty(request)) {
return null;
}
List<BoardFileDto> fileInfoList = new ArrayList<>();
// 파일을 저장할 디렉터리를 설정
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
ZonedDateTime now = ZonedDateTime.now();
String storedDir = uploadDir + "images\\" + now.format(dtf);
File fileDir = new File(storedDir);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
// 업로드 파일 데이터를 디렉터리에 저장하고 정보를 리스트에 저장
Iterator<String> fileTagNames = request.getFileNames();
while(fileTagNames.hasNext()) {
String fileTagName = fileTagNames.next();
List<MultipartFile> files = request.getFiles(fileTagName);
for (MultipartFile file : files) {
String originalFileExtension = "";
// 파일 확장자를 ContentType에 맞춰서 지정
if (!file.isEmpty()) {
String contentType = file.getContentType();
if (ObjectUtils.isEmpty(contentType)) {
break;
} else {
if (contentType.contains("image/jpeg")) {
originalFileExtension = ".jpg";
} else if (contentType.contains("image/png")) {
originalFileExtension = ".png";
} else if (contentType.contains("image/gif")) {
originalFileExtension = ".gif";
} else {
break;
}
}
// 저장에 사용할 파일 이름을 조합
String storedFileName = Long.toString(System.nanoTime()) + originalFileExtension;
String storedFilePath = storedDir + "\\" + storedFileName;
// 파일 정보를 리스트에 저장
BoardFileDto dto = new BoardFileDto();
dto.setBoardIdx(boardIdx);
dto.setFileSize(Long.toString(file.getSize()));
dto.setOriginalFileName(file.getOriginalFilename());
dto.setStoredFilePath(storedFilePath);
fileInfoList.add(dto);
// 파일 저장
fileDir = new File(storedFilePath);
file.transferTo(fileDir);
}
}
}
return fileInfoList;
}
}
@Slf4j
@Service
public class BoardServiceImpl implements BoardService {
@Autowired
private BoardMapper boardMapper;
@Autowired
private FileUtils fileUtils;
@Override
public List<BoardDto> selectBoardList() {
return boardMapper.selectBoardList();
}
@Override
public void insertBoard(BoardDto boardDto, MultipartHttpServletRequest request) {
// 로그인한 사용자를 글쓴이로 설정
// TODO. 로그인한 사용자의 ID로 변경
// boardDto.setCreatedId("hong");
// boardMapper.insertBoard(boardDto);
try {
List<BoardFileDto> fileInfoList = fileUtils.parseFileInfo(100, request);
} catch(Exception e) {
}
}
...(생략)...