로그인한 사람만 게시글 쓸 수 있게(로그인 안하면 글쓰기 버튼 보이지 않게)
boardList.html
<div class="btn-area">
<!-- 로그인 상태일 때만 글쓰기 버튼 노출 -->
<button id="insertBtn" th:if="${session.loginMember}">글쓰기</button>
</div>
<script src="/js/board/boardList.js"></script>
get 방식 요청
/editBoard/게시판코드/insert
-> 게시판코드 사용하려면 html 에서 requestScope 에 실려있는 boardCode를 세팅해서 넘겨줘야함
boardList.html 하단에 script 태그로 boardCode 세팅해주기
<script th:inline="javascript">
const boardCode = /*[[${boardCode}]]*/ "게시판 코드 번호";
</script>
boardList.js
html에서 세팅해서 넘겨준 값으로 요청 주소 작성
/* 글쓰기 버튼 클릭 시 */
const insertBtn = document.querySelector("#insertBtn");
// 글쓰기 버튼이 존재할 때 (로그인 상태인 경우)
if(insertBtn != null) {
insertBtn.addEventListener("click", () => {
// get 방식 요청
// /editBoard/게시판코드/insert
location.href = `/editBoard/${boardCode}/insert`;
});
};
@PathVariable 요청 주소 얻어오기
EditBoardController
/** 게시글 작성 화면 전환
* @param boardCode
* @return "board/boardWrite"
*/
@GetMapping("{boardCode:[0-9]+}/insert")
public String boardInsert(@PathVariable("boardCode") int boardCode) {
// /editBoard/1/insert => PathVariable로 얻어오기
return "board/boardWrite"; // templates/board/boardWrite.html 로 forward
}

boardWrite.html
<!-- <form th:action="@{/editBoard/{boardCode}/insert(boardCode=${boardCode})}" -->
<form action="insert"
method="POST" class="board-write" id="boardWriteFrm"
enctype="multipart/form-data">
form 태그 상대경로로 작성
현재 주소 : http://localhost/editBoard/1/insert
주소 창에 뜨는 URL 자체는 똑같음 (요청 방법이 다름 POST)
boardList 에서는 Get 요청으로 매핑해줬었음
<!-- 제목 -->
<h1 class="board-title">
<input type="text" name="boardTitle" placeholder="제목" value="">
</h1>
<!-- 썸네일 영역 -->
<h5>썸네일</h5>
<div class="img-box">
<div class="boardImg thumbnail">
<label for="img0">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img0" accept="image/*">
<span class="delete-image">×</span>
</div>
</div>
<!-- 업로드 이미지 영역 -->
<h5>업로드 이미지</h5>
<div class="img-box">
<div class="boardImg">
<label for="img1">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img1" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img2">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img2" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img3">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img3" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img4">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img4" accept="image/*">
<span class="delete-image">×</span>
</div>
</div>
<!-- 내용 -->
<div class="board-content">
<textarea name="boardContent"></textarea>
</div>
<!-- 버튼 영역 -->
<div class="board-btn-area">
<button type="submit" id="writebtn">등록</button>
</div>
</form>
EditBoardController
/** 게시글 작성
* @param boardCode : 어떤 게시판에 작성할 글인지 구분
* @param inputBoard : 입력된 값(제목, 내용) 세팅되어있음 (커맨드 객체)
* @param loginMember : 로그인한 회원 번호를 얻어오는 용도
* @param images : 제출된 file 타입 input 태그 데이터들 (이미지 파일)
* @param ra : 리다이렉트 시 request scope 로 데이터 전달
* @return
*/
@PostMapping("{boardCode:[0-9]+}/insert")
public String boardInsert(
@PathVariable("boardCode") int boardCode,
@ModelAttribute Board inputBoard,
@SessionAttribute("loginMember") Member loginMember,
@RequestParam("images") List<MultipartFile> images,
RedirectAttributes ra
) {
// @ModelAttribute 는 생략 가능
// 1. boardCode, 로그인한 회원 번호를 inputBoard에 세팅
inputBoard.setBoardCode(boardCode);
inputBoard.setMemberNo(loginMember.getMemberNo());
// 2. 서비스 메서드 호출 후 결과 반환 받기
// -> 성공 시 [상세 조회]를 요청할 수 있도록
// 삽입된 게시글의 번호를 반환 받기
int boardNo = service.boardInsert(inputBoard, images);
return "";
}
List<MultipartFile> images
[문제점]
[해결 방법]
EditBoardServiceImpl
@Override
public int boardInsert(Board inputBoard, List<MultipartFile> images) {
int result = mapper.boardInsert(inputBoard);
edit-board-mapper.xml
useGeneratedKeys 속성 : DB 내부적으로 생성한 키 (시퀀스)를 전달된 파라미터의 필드로 대입 가능 여부 지정
(selectKey를 사용하기 위한 속성)
동적 SQL
<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>
INSERT INTO "BOARD"
VALUES( #{boardNo},
#{boardTitle},
#{boardContent},
DEFAULT, DEFAULT, DEFAULT, DEFAULT,
#{boardCode},
#{memberNo}
)
</insert>
selectKey 에서 시퀀스 넘버가 생성돼서 boardNo 에 세팅됨 INSERT 구문에서 그 번호를 써야함
SEQ_BOARD_NO.NEXTVAL 로 작성하면 시퀀스가 또 생성돼서 둘이 달라짐
inputBoard 주소값만 복사해서 넘겨준 거 (얕은 복사)
inputBoard 안에 boardNo (시퀀스넘버) setting 된 상태
ServiceImpl 에 있는 inputBoard 도 같은 주소를 보고있음 == boardNo에 시퀀스 번호가 들어가져 있는 상태다.
result 는 INSERT 결과 (실패0/성공1)
ServiceImpl
// 삽입 실패 시
if(result == 0) return 0;
int boardNo = inputBoard.getBoardNo();
// 실제 업로드된 이미지의 정보를 모아둔 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);
// 모든 값을 저장할 DTO 생성 (BoardImg - Builder 패턴 사용)
}
}
BoardImg 에 imgPath 세팅해주기 위해서 config.properties 에서 값 얻어오기
@PropertySource("classpath:/config.properties")
public class EditBoardServiceImpl implements EditBoardService {
// config.properties 값을 얻어와 필드에 저장
@Value("${my.board.web-path}")
private String webPath; // /images/board/
@Value("${my.board.folder-path}")
private String folderPath; // C:/uploadFiles/board/
// IMG_ORDER == i (인덱스 == 순서)
// 모든 값을 저장할 DTO 생성 (BoardImg - Builder 패턴 사용)
BoardImg img = BoardImg.builder()
.imgOriginalName(originalName)
.imgRename(rename)
.imgPath(webPath)
.boardNo(boardNo)
.imgOrder(i)
.uploadFile(images.get(i))
.build();
uploadList.add(img);
}
}
// 선택한 파일이 없을 경우(제목이랑 내용은 보여줘야함)
if(uploadList.isEmpty()) {
return boardNo;
}
uploadList 가 만약 3개라면 3개 INSERT 해줘야함
INSERT INTO BOARD_IMG
(
SELECT SEQ_IMG_NO.NEXTVAL, '경로1', '원본1', '변경1', 1, 2001 FROM DUAL
UNION
SELECT SEQ_IMG_NO.NEXTVAL, '경로2', '원본2', '변경3', 1, 2001 FROM DUAL
UNION
SELECT SEQ_IMG_NO.NEXTVAL, '경로3', '원본2', '변경3', 1, 2001 FROM DUAL
)
이렇게 작성하면 에러 발생
SQL Error [2287] [42000]: ORA-02287: 시퀀스 번호는 이 위치에 사용할 수 없습니다
-> 하나의 서브쿼리 안에서 여러 번 SEQ 생성 안됨
시퀀스 생성하는 함수를 생성
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;
SELECT NEXT_IMG_NO() FROM DUAL;
alt+x 로 함수 수행
조회 결과

SEQ 자리를 함수로 대체해서 수행
INSERT INTO BOARD_IMG
(
SELECT NEXT_IMG_NO(), '경로1', '원본1', '변경1', 1, 2001 FROM DUAL
UNION
SELECT NEXT_IMG_NO(), '경로2', '원본2', '변경3', 1, 2001 FROM DUAL
UNION
SELECT NEXT_IMG_NO(), '경로3', '원본2', '변경3', 1, 2001 FROM DUAL
);
ServiceImpl
// 이미지 있다면 (image INSERT)
// 선택한 파일이 존재할 경우
// -> "BOARD_IMG" 테이블에 INSERT + 서버에 파일 저장
result = mapper.insertUploadList(uploadList);
edit-board-mapper.xml
위에서 작성한 SQL 문을 mybatis 형태로 변경해서 사용
동적 SQL 중 <foreach>
[지원하는 속성]
collection : 반복할 객체의 타입 작성(list, set, map...)
item : collection에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수
index : 현재 반복 접근중인 인덱스 (0,1,2,3,4 ..)
open : 반복 전에 출력할 sql (
close : 반복 종료 후에 출력한 sql )
separator : 반복 사이사이 구분자
(UNION 쓸 때 앞 뒤로 공백 써줘야함)
java => List
mybatis => list
List size 만큼 행이 만들어져서 insert 됨
<!-- 게시글 이미지 모두 삽입 -->
<insert id="insertUploadList" parameterType="list">
INSERT INTO BOARD_IMG
<foreach collection="list" item="img"
open="(" close=")" separator=" UNION ">
SELECT NEXT_IMG_NO(),
#{img.imgPath},
#{img.imgOriginalName},
#{img.imgRename},
#{img.imgOrder},
#{img.boardNo}
FROM DUAL
</foreach>
</insert>
result == 삽입된 행의 개수 == uploadList.size()
result 값과 uploadList.size() 가 다르면 몇 개 INSERT 안된거임
ServiceImpl
// 다중 INSERT 성공 확인 (uploadList에 저장된 값이 모두 정상 삽입 되었나)
if(result == uploadList.size()) {
// 성공 시 서버에 파일 저장 (uploadFiles)
for(BoardImg img : uploadList) {
img.getUploadFile().transferTo(new File(folderPath+img.getImgRename()));
}
부분적으로 삽입 실패 -> 전체 서비스 실패로 판단
-> 이전에 삽입된 내용 모두 rollback
-> rollback 하는 방법
== RuntimeException 을 강제 발생
(@Transactional 에 의해 rollback 됨)
throw new RuntimeException(); -> 이렇게 쓰면 어디서 에러났는지 모름
} else {
// 사용자 정의 예외
throw new BoardInsertException("이미지가 정상 삽입되지 않음");
}
return boardNo;
}
BoardInsertException 클래스 생성
// 게시글 삽입 중 문제 발생 시 사용할 사용자 정의 예외
public class BoardInsertException extends RuntimeException {
public BoardInsertException() {
super("게시글 삽입 중 예외 발생");
}
public BoardInsertException(String message) {
super(message);
}
}
EditBoardController
String path = null;
String message = null;
if(boardNo > 0) {
path = "/board/" + boardCode + "/" + boardNo; // 상세 조회 /board/1/2000
message = "게시글이 작성 되었습니다.";
} else {
path = "insert";
message = "게시글 작성 실패";
}
ra.addFlashAttribute("message", message);
return "redirect:" + path;
}
FilterConfig 에 구문 추가
String[] filteringURL = {"/myPage/*", "/editBoard/*"};