Spring Framework (17) 게시글 작성 기능 - Day 64

jaehoon ahn·2025년 12월 18일

Spring boot

목록 보기
15/20
post-thumbnail

복습

  • Mybatis에서는 값을 담아줄 때, 테이블 컬럼명과 DTO 필드명이 같아야 한다
  • @RequiredArgsConstructor
    • 필드에 final 키워드가 붙어있으면 의존성 주입을 자동으로 해줌

      @RequiredArgsConstructor
      public class BookController{
      	private final BookService service;
      .
      .
      }
  • @ResponseBody
    • 응답 데이터를 본문에 실어서 요청한 곳으로 보내는 어노테이션
    • 비동기에서 사용

글쓰기

  • 글쓰기는 로그인이 되었을 때 활성화
  • 만약 로그인 하지않았다면 비활성화
 <div class="btn-area">
          <!-- 로그인 상태일 때만 글쓰기 버튼 노출 -->
          <button id="insertBtn" th:if="${session.loginMember}">글쓰기</button>
 </div>

boardList.js

  • 글쓰기 버튼을 클릭했을 때, 동기식 요청 보내기
/* 글쓰기 버튼 클릭 시 */
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

Controller

/**
	 * @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;
	}

Service

@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;
	}

edit-board-mapper.xml

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>

DB에서 사용할 수 있는 함수(여러 행을 INSERT할 때, 다중 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;
-- 여기까지 긁기
  • 여러 행을 INSERT할 때
  • 시퀀스를 수행해주는 함수

동적 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>

0개의 댓글