
필드에 final 키워드가 붙어있으면 의존성 주입을 자동으로 해줌
@RequiredArgsConstructor
public class BookController{
private final BookService service;
.
.
}

<div class="btn-area">
<!-- 로그인 상태일 때만 글쓰기 버튼 노출 -->
<button id="insertBtn" th:if="${session.loginMember}">글쓰기</button>
</div>
/* 글쓰기 버튼 클릭 시 */
const insertBtn = document.querySelector("#insertBtn");
// 글쓰기 버튼이 존재할 때 (로그인 상태인 경우)
// if(insertBtn == null)일 수 있기 때문에 조건문 작성
// 조건문을 작성하지 않으면 에러 발생
if (insertBtn != null) {
insertBtn.addEventListener("click", () => {
// get 방식 요청
// /editBoard/1/insert
// <script th:inline="javascript">
// const boardCode = /*[[${boardCode}]]*/ "게시판 코드 번호";
// </script>
// html 파일에서 타임리프 문법으로 boardCode 작성
// html : 타임리프
// "${java에서 가지고온 값 사용 가능}"
// JavaScript : ""로 안되고 백틱으로만 사용 가능
// `${JS변수 사용 가능}`
// 동기식 요청 : location을 이용
// 비동기식 요청 : fetch() API을 이용
location.href = `/editBoard/${boardCode}/insert`;
});
}
// /editBoard/1/insert
/**
* @param boardCode
* @param inputBoard : 입력된 값(제목, 내용) 세팅되어 있음 (커맨드 객체)
* @param loginMember : 로그인한 회원 번호를 얻어오는 용도(세션에 등록되어있음)
* @param images : 제출된 file 타입 input태그가 전달한 데이터들 (이미지 파일..)
* @return
*/
@PostMapping("{boardCode:[0-9]+}/insert")
public String boardInsert(@PathVariable("boardCode") int boardCode, Board inputBoard,
@SessionAttribute("loginMember") Member loginMember, @RequestParam("images") List<MultipartFile> images,
RedirectAttributes ra) throws IllegalStateException, IOException {
log.debug("images : " + images);
// [MultipartFile, MultipartFile,MultipartFile,MultipartFile,MultipartFile]
// images에 사진이 아무것도 업로드 되지 않았을 때 MultipartFile이 넘어오는데 빈값(empty)으로 넘어온다
/*
* ** List<MultipartFile> images는 실제 제출된 파일이 있던 없던 무조건 길이 5의 MultipartFile이 요소로
* 있는 List가 제출됨 **
*
* - 5개 모두 업로드 O -> 0 ~ 4 인덱스에 실제 파일 들어간 MultipartFile이 저장됨 - 5개 모두 업로드 X -> 0 ~
* 4 인덱스에 비어있는 MultipartFile이 저장됨 - 2번 인덱스만 업로드 된다면 -> 2번 인덱스 파일이 저장된
* MultipartFile, 나머지 0, 1, 3, 4번 인덱스에는 MultipartFile 비어있음
*
* - 무작정 서버에 저장 X -> List의 각 인덱스에 들어있는 MultipartFile에 실제로 제출된 파일이 있는지 확인하는 로직을
* 구성
*
* + List 요소의 index 번호 == DB상 BOARD_IMG 테이블의 IMG_ORDER와 같음
*
*
*
*/
// 1. boardCode, 로그인한 회원 번호를 inputBoard에 세팅
inputBoard.setBoardCode(boardCode);
inputBoard.setMemberNo(loginMember.getMemberNo());
// -> inputBoard 총 네가지 세팅됨(boardTitle, boardContent, boardCode, memberNo)
// 2. 서비스 호출 후 결과 반환 받기
// -> 성공 시 [상세 조회]를 요청할 수 있도록
// 삽입된 게시글 번호를 반환받기
// 등록하고자 하는 최종 게시글 번호
// redirect를 해줘야 하기 때문
int boardNo = service.boardInsert(inputBoard, images);
// 3. 서비스 결과에 따라 message, 리다이렉트 할 경로 지정
String path = null;
String message = null;
if (boardNo > 0) {
path = "/board/" + boardCode + "/" + boardNo;
message = "게시글이 작성되었습니다!";
} else {
path = "insert"; // 상대경로 / editBoard/1/insert -> redirect이므로 get
// 다시 작성하고있는 페이지로 돌아가기
message = "게시글 작성 실패";
}
ra.addFlashAttribute("message", message);
return "redirect:" + path;
}
@Value("${my.board.web-path}")
private String webPath;
@Value("${my.board.folder-path}")
private String folderPath;
// 게시글 작성 서비스
@Override
public int boardInsert(Board inputBoard, List<MultipartFile> images) throws IllegalStateException, IOException {
// 게시글 제목, 상세내용 -> BOARD 테이블
// 게시글의 이미지들 -> BOARD_IMG
// BOARD 테이블에 제목, 상세내용을 먼저 등록해주고,
// 그다음 작성한 글의 번호를 가져와서 삽입을 해야함
// 1. 게시글 부분 (inputBoard)을 먼저
// BOARD 테이블에 INSERT 하기
// -> INSERT 된 게시글의 번호(시퀀스 번호) 반환받기
int result = mapper.boardInsert(inputBoard);
// inputBoard에는 boardNo가 selectKey를 통해 필드에 값을 수정한 값이 존재
// result의 결과 행의 갯수를 가지고 다음 과정 진행 여부 결정
// 삽입 실패 시 return 0
if (result == 0) {
return 0;
}
// 삽입 성공 시
// 삽입된 게시글의 번호를 변수로 저장
int boardNo = inputBoard.getBoardNo();
// -> mapper.xml에서 <selectKey> 태그를 이용해서 생성된
// boardNo가 inputBoard에 세팅된 상태!!(얕은 복사 개념 이해 필수)
// 2. 업로드된 이미지가 실제로 존재할 경우
// 업로드된 이미지만 별도로 저장하여
// BOARD_IMG 테이블에 삽입하는 코드 작성
// 실제로 업로드된 이미지만 모아둘 List 생성
List<BoardImg> uploadList = new ArrayList<>();
// images 리스트에서 하나씩 꺼내어 파일이 있는지 검사
for (int i = 0; i < images.size(); i++) {
// 실제 파일이 제출된 경우
if (!images.get(i).isEmpty()) {
// 원본명
String originalName = images.get(i).getOriginalFilename();
// 변경명
String rename = Utility.fileRename(originalName);
// 모든 값을 저장할 BoardImg DTO로 객체 생성
BoardImg img = BoardImg.builder().imgOriginalName(originalName).imgRename(rename).imgPath(webPath)
.boardNo(boardNo).imgOrder(i).uploadFile(images.get(i)).build();
// 요소의 index 번호 == DB상 BOARD_IMG 테이블의 IMG_ORDER와 같음
// uploadList에 추가
uploadList.add(img);
}
}
// uploadList가 비어있는 경우 == 실제로 제출된 파일이 하나도 없다
if(uploadList.isEmpty()) {
return boardNo;
}
// 제출된 파일이 존재할 경우
// -> "BOARD_IMG" 테이블에 INSERT + 서버에 파일 저장
result = mapper.insertUploadList(uploadList);
// result == 삽입된 행의 갯수 == uploadList.size()
// 다중 INSERT 성공 확인
if(uploadList.size() == result) {
// 서버에 파일 저장
for(BoardImg img : uploadList) {
img.getUploadFile().transferTo(new File(folderPath + img.getImgRename()));
}
} else {
// < 부분적으로 삽입 실패했을 경우 >
// ex) uploadList에 2개 저장
// -> 1개 삽입 성공 1개는 실패
// -> 전체 서비스 실패로 판단
// -> 이전에 삽입된 내용 모두 rollback
// -> rollback 하는 방법
// == RuntimeException 강제 발생 (@Transactional 기본 RuntimeException)
throw new RuntimeException();
}
return boardNo;
}
MyBatis에서 제공하는 기능
useGeneratedKeys 속성 :
DB 내부적으로 생성한 키(시퀀스)를 전달된 파라미터의 필드로 대입 가능한지 여부를
지정하는 속성
-> 즉, selectKey를 사용할건지 안할건지
<selectKey> 태그 : INSERT/UPDATE 시 사용할 키(시퀀스)를 조회해서
파라미터의 지정된 필드에 대입하는 역할의 태그
order 속성 : 메인 SQL이 수행되기 전/후에 selectKey가 수행되도록 지정
전 : BEFORE / 후 : AFTER
keyProperty 속성 : selectKey의 조회 결과를 저장할 파라미터의
필드명을 작성
<insert id="boardInsert" useGeneratedKeys="true"
parameterType="Board">
<selectKey order="BEFORE" resultType="_int"
keyProperty="boardNo">
SELECT SEQ_BOARD_NO.NEXTVAL FROM DUAL
</selectKey>
<!-- order="BEFORE" 메인 sql을 수행하기 전에 수행하라는 의미 현재 메인 sql은 insert, 즉 insert보다
먼저 수행하라는 의미 -->
INSERT INTO "BOARD"
VALUES(#{boardNo}, #{boardTitle}, #{boardContent},
DEFAULT, DEFAULT, DEFAULT, DEFAULT,
#{boardCode}, #{memberNo}
)
<!-- 시퀀스가 아닌, boardNo를 작성하면 된다! -->
</insert>
-- SEQ_IMG_NO 시퀀스의 다음 값을 반환하는 함수 생성
-- 전체 드래그 ALT+X
CREATE OR REPLACE FUNCTION NEXT_IMG_NO
-- 반환형
RETURN NUMBER
-- 사용할 변수
IS IMG_NO NUMBER;
BEGIN
SELECT SEQ_IMG_NO.NEXTVAL
INTO IMG_NO
FROM DUAL;
RETURN IMG_NO;
END;
-- 여기까지 긁기

동적 SQL 중
동적 SQL 중 <foreach>
- Mybatis에서 제공하는 향상된 for문
- 특정 SQL 구문을 반복할 때 사용
- 반복 사이에 구분자(separator)를 추가할 수 있음
[지원하는 속성]
collection : 반복할 객체의 타입 작성(list, set, map...)
item : collection에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수
index : 현재 반복 접근중인 인덱스 (0,1,2,3,4 ..)
open : 반복 전에 출력할 sql
close : 반복 종료 후에 출력한 sql
separator : 반복 사이사이 구분자 ->
<insert id="insertUploadList" parameterType="list">
INSERT INTO "BOARD_IMG"
<foreach collection="list" item="img" separator=" UNION "
open="(" close=")"
>
SELECT NEXT_IMG_NO(),
#{img.imgPath},
#{img.imgOriginalName},
#{img.imgRename},
#{img.imgOrder},
#{img.boardNo}
FROM DUAL
</foreach>
</insert>
