08_Spring_240425(목)_71일차(0) - ★BoardProject★ - 14. 게시판 - 게시글 삽입

soowagger·2024년 4월 26일

8_Spring

목록 보기
29/38

14. 게시글 삽입

✅ 글쓰기 버튼 클릭 시

EditBoardController

@Controller
@RequiredArgsConstructor
@RequestMapping("editBoard")
@Slf4j
public class EditBoardController {
	
	private final EditBoardService service;
	
	/** 게시글 작성 화면 전환
	 * @param boardCode
	 * @return board/boardWrite
	 */
	@GetMapping("{boardCode:[0-9]+}/insert")
	public String boardInsert(@PathVariable("boardCode") int boardCode) {
		
		return "board/boardWrite"; // templates/board/boardWrite.html 로 forward
	}
	
	
	/** 게시글 작성
	 * @param boardCode : 어떤 게시판에 작성할 글인지 구분
	 * @param inputBoard : 입력된 값(제목, 내용) 세팅되어 있음(커맨드 객체)
	 * @param loginMember : 로그인한 회원 번호를 얻어오는 용도
	 * @param images : 제출된 file 타입 input 태그 데이터들(이미지 파일...)
	 * @param ra : 리다이렉트 시 request scope로 데이터 전달
	 * @return
	 * @throws IOException 
	 * @throws IllegalStateException 
	 */
	@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
			) throws IllegalStateException, IOException {
		
		/* List<MultipartFile> images
		 * - 5개 모두 업로드 O => 0~4번 인덱스에 파일 저장됨
		 * - 5개 모두 업로드 X => 0~4번 인덱스에 파일 저장 X
		 * - 2번 인덱스만 업로드 -> 2번 인덱스만 파일 저장, 0/1/3/4번 인덱스는 저장 X
		 * 
		 * [문제점]
		 * - 파일이 선택되지 않은 input 태그도 제출되고 있음
		 *   (제출은 되어 있는데 데이터는 ""(빈칸))
		 * 
		 *   -> 파일 선택이 안된 input 태그 값을 서버에 저장하려고 하면
		 *      오류가 발생할 것이다!
		 *
		 * [해결방법]
		 * - 무작정 서버에 저장 X
		 * -> 제출된 파일이 있는지 확인하는 로직을 추가 구성
		 * 
		 * + List 요소의 index 번호 == IMG_ORDER 와 같음
		 * 
		 */
		
		// 1. boardCode, 로그인한 회원 번호를 inputBoard에 세팅
		inputBoard.setBoardCode(boardCode);
		inputBoard.setMemberNo(loginMember.getMemberNo());
		
		// 2. 서비스 메서드 호출 후 결과 반환 받기
		// -> 성공 시 [상세 조회]를 요청할 수 있도록 
		//    삽입된 게시글 번호를 반환 받기
		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";
			message = "게시글 작성 실패..";
	
		}
		
		ra.addFlashAttribute("message", message);
		
		return "redirect:" + path;
	}
}

DB 함수 vs MyBatis

DB 수행(CREATE ~ END까지)

BoardServiceImpl

// config.properties 값을 얻어와 필드에 저장
@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 {
	
	// 1. 게시글 부분(inputBoard)을 먼저 BOARD 테이블 INSERT 하기
	//    -> INSERT 결과로 작성된 게시글 번호(생성된 시퀀스 번호) 반환 받기
	int result = mapper.boardInsert(inputBoard);
	
	// result == INSERT 결과 (0/1)
	
	// 삽입 실패 시
	if(result == 0) return 0;
	
	// 삽입된 게시글의 번호를 변수로 저장
	// -> mapper.xml에서 <selectKey> 태그를 이용해서 생성된
	//    boardNo가 inputBoard에 저장된 상태!!! (얕은 복사 개념 이해 필수)
	int boardNo = inputBoard.getBoardNo();
	
	// 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);
			
			// 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;
	}
	
	// 선택한 파일이 존재할 경우
	// -> "BOARD_IMG" 테이블에 INSERT + 서버에 파일 저장
	
	// result == 삽입된 행의 개수 == uploadList.size()
	result = mapper.insertUploadList(uploadList);
	
	// 다중 INSERT  성공 확인 (uploadList에 저장된 값이 모두 정상 삽입 되었나)
	if(result == uploadList.size()) {
		
		// 서버에 파일 저장
		for(BoardImg img : uploadList) {
			
			img.getUploadFile()
				.transferTo(new File(folderPath + img.getImgRename()));
			
		}
		
	} else {
		// 부분적으로 삽입 실패 -> 전체 서비스 실패로판단
		// -> 이전에 삽입된 내용 모두 rollback
		
		// -> rollback 하는 방법
		// == RuntimeException 강제 발생 (@Transactional)
		throw new BoardInsertException("이미지가 정상 삽입되지 않음");
		
		
	}
	
	
	return boardNo;
}

BoardInsertException (throw)

public class BoardInsertException extends RuntimeException {

	
	public BoardInsertException() {
		super("게시글 삽입 중 예외 발생");
	}
	
	public BoardInsertException(String message) {
		super(message);
	}
}

edit-board-mapper.xml

	<!-- 
 		동적 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"
			open="(" close= ")" separator=" UNION ">
			SELECT NEXT_IMG_NO(),
			#{img.imgPath}, 
			#{img.imgOriginalName}, 
			#{img.imgRename}, 
			#{img.imgOrder}, 
			#{img.boardNo} 
			FROM DUAL
		</foreach>
		
	
	</insert>

boardWrite.js

/* 선택된 이미지 미리보기 */
const previewList = document.querySelectorAll(".preview"); // img 태그 5개
const inputImageList = document.querySelectorAll(".inputImage"); // input 태그 5개
const deleteImageList = document.querySelectorAll(".delete-image"); // x버튼 5개



// 이미지 선택 이후 취소를 누를 경우를 대비한 백업 이미지
// (백업 원리 -> 복제품으로 기존 요소를 대체함)
const backupInputList = new Array(inputImageList.length);


/* ***** input 태그 값 변경 시(파일 선택 시) 실행할 함수 ***** */
/**
 * @param inputImage : 파일이 선택된 input 태그
 * @param order : 이미지 순서
 */
const changeImageFn = (inputImage, order) => {

  // byte단위로 10MB 지정
  const maxSzie = 1024 * 1024 * 10;

  // 업로드된 파일 정보가 담긴 객체를 얻어와 변수에 저장
  const file = inputImage.files[0];


  // ------------- 파일 선택 -> 취소 해서 파일이 없는 경우 ----------------
  if(file == undefined){
    console.log("파일 선택 취소됨");

    // 같은 순서(order)번째 backupInputList 요소를 얻어와 대체하기

    /* 한 번 화면에 추가된 요소는 재사용(다른 곳에 또 추가) 불가능 */

    // 백업본을 한 번 더 복제
    const temp = backupInputList[order].cloneNode(true);

    inputImage.after(temp); // 백업본을 다음 요소로 추가
    inputImage.remove();    // 원본을 삭제
    inputImage = temp;      // 원본 변수에 백업본을 참조할 수 있게 대입

    // 백업본에 없는 이벤트 리스너를 다시 추가
    inputImage.addEventListener("change", e => {
      changeImageFn(e.target, order);
    })

    return;
  }


  // ---------- 선택된 파일의 크기가 최대 크기(maxSize) 초과 ---------

  if(file.size > maxSzie){
    alert("10MB 이하의 이미지를 선택해주세요");

    // 해당 순서의 backup 요소가 없거나, 
    // 요소는 있는데 값이 없는 경우 == 아무 파일도 선택된적 없을 때
    if(backupInputList[order] == undefined
        || backupInputList[order].value == ''){

      inputImage.value = ""; // 잘못 업로드된 파일 값 삭제
      return;
    }

    // 이전에 정상 선택 -> 다음 선택에서 이미지 크기 초과한 경우
    // 백업본을 한 번 더 복제
    const temp = backupInputList[order].cloneNode(true);

    inputImage.after(temp); // 백업본을 다음 요소로 추가
    inputImage.remove();    // 원본을 삭제
    inputImage = temp;      // 원본 변수에 백업본을 참조할 수 있게 대입
 
    // 백업본에 없는 이벤트 리스너를 다시 추가
    inputImage.addEventListener("change", e => {
     changeImageFn(e.target, order);
    })

    return;
  }


  // ------------ 선택된 이미지 미리보기 --------------

  const reader = new FileReader(); // JS에서 파일을 읽고 저장하는 객체

  // 선택된 파일을 JS로 읽어오기 -> reader.result 변수에 저장됨
  reader.readAsDataURL(file);

  reader.addEventListener("load", e => {
    const url = e.target.result;

    // img 태그(.preview)에 src 속성으로 url 값을 대입
    previewList[order].src = url;

    // 같은 순서 backupInputList에 input태그를 복제해서 대입
    backupInputList[order] = inputImage.cloneNode(true);
  });

}





for(let i=0 ; i<inputImageList.length ; i++){

  // **** input태그에 이미지가 선택된 경우(값이 변경된 경우) ****
  inputImageList[i].addEventListener("change", e => {
    changeImageFn(e.target, i);
  })


  // **** x 버튼 클릭 시 ****
  deleteImageList[i].addEventListener("click", () => {

    // img, input, backup의 인덱스가 모두 일치한다는 특징을 이용

    previewList[i].src       = ""; // 미리보기 이미지 제거
    inputImageList[i].value  = ""; // input에 선택된 파일 제거
    backupInputList[i].value = ""; // 백업본 제거
  });

}


// 작성 폼 유효성 검사
document.querySelector("#boardWriteFrm")
  .addEventListener("submit", e => {

  const boardTitle = document.querySelector("[name='boardTitle']");
  const boardContent = document.querySelector("[name='boardContent']");

  if(boardTitle.value.trim().length == 0){
    alert("제목을 작성해 주세요");
    boardTitle.focus();
    e.preventDefault();
    return;
  }

  if(boardContent.value.trim().length == 0){
    alert("내용을 작성해 주세요");
    boardContent.focus();
    e.preventDefault();
    return;
  }

});
profile

0개의 댓글