Java/Spring Framework

[ Spring ] 게시판 글쓰기

Mungwang 2023. 8. 31. 00:08

💎 Console

★★ 템플릿 리터럴( template literal ) ★★

js내에 '역따옴표(백틱)' 을 사용하면 ${} 표현식을 쓸수있고 결과는 문자열로 인식한다

장점 : 기존 문자열 내부에 표현식을 추가하려면, 문자열을 따옴표로 분리한후 + 연산자로 하나씩 연결해줘야했지만

              템플릿리터럴 즉 백틱을 사용하면 편리하고 가독성이 좋게 작성할수있다.

💎 Jsp

       
 <form action="/board2/${boardCode}/insert" method="POST"
            class="board-write" id="boardWriteFrm" enctype="multipart/form-data">

            <h1 class="board-name">${boardName}</h1>

            <!-- 제목 -->
            <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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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>
 

💎 JS

 
    // img 5개
    const preview = document.getElementsByClassName("preview");

    // file 5개
    const inputImage = document.getElementsByClassName("inputImage");

    // x버튼 5개
    const deleteImage = document.getElementsByClassName("delete-image");

    // -> 위에 얻어온 요소들의 개수가 같음 == 인덱스가 일치함

    for(let i = 0; i < inputImage.length; i++) {
       
        inputImage[i].addEventListener("change", e => {
       
            const file = e.target.files[0]; // 선택된 파일의 데이터

            if(file != undefined) { // 파일이 선택되었을 때

                const reader = new FileReader(); // 파일을 읽는 객체

                reader.readAsDataURL(file); // 지정된 파일을 읽은 후 result 변수에 URL 형식으로 저장

                reader.onload = e => { // 파일을 다 읽은 후 수행
                    preview[i].setAttribute("src", e.target.result);
                }

            } else { // 선택 후 취소 되었을 때
                // -> 선택된 파일 없음 => 미리보기 삭제
                preview[i].removeAttribute("src");
            }
       
        });

        // 미리보기 삭제 버튼(X버튼)
        deleteImage[i].addEventListener("click", e => {
           
            // 미리보기 이미지가 있을 경우
            if(preview[i].getAttribute("src") != "") {

                // 미리보기 삭제
                preview[i].removeAttribute("src");

                // input type="file" 태그의 value를 삭제
                // ** input type="file"의 value ""(빈칸)만 대입 가능 **
                inputImage[i].value = "";
            }
        })
    }

    // 게시글 등록 시 제목, 내용 작성 여부 검사
    const boardWirteFrm = document.getElementById("boardWriteFrm");
    const boardTitle = document.querySelector("[name='boardTitle']");
    const boardContent = document.querySelector("[name='boardContent']");
    boardWirteFrm.addEventListener("submit", e => {
       
        if(boardTitle.value.trim() == "") {
            alert("제목을 입력해주세요.");
            boardTitle.value = "";
            boardTitle.focus();
            e.preventDefault(); // form 기본 이벤트 제거
            return;
        }

        if(boardContent.value.trim() == "") {
            alert("내용을 입력해주세요.");
            boardContent.value = "";
            boardContent.focus();
            e.preventDefault(); // form 기본 이벤트 제거
            return;
        }

    })
 

💎 Spring

🔎 BoardController2

// 게시글 작성 화면 전환
   @GetMapping("/{boardCode:[0-9]+}/insert")
   public String boardInsert(@PathVariable("boardCode") int boardCode) {
      // @PathVariable : 주소 값 가져오기 + request scope에 값 올리기
      return "board/boardWrite";
   }
   
   // 게시글 작성
   @PostMapping("/{boardCode:[0-9]+}/insert")
   public String boardInsert(
         @PathVariable("boardCode") int boardCode,
         Board board, // 커맨드 객체(필드에 파라미터 담겨있음!)
         @RequestParam(value="images", required=false) List<MultipartFile> images,
         @SessionAttribute("loginMember") Member loginMember,
         RedirectAttributes ra,
         HttpSession session
         ) throws IllegalStateException, IOException {

         // 파라미터 : 제목, 내용, 파일(0~5개)
         // 파일 저장 경로 : HttpSession
         // 세션 : 로그인한 회원의 번호
         // 리다이렉트 시 데이터 전달 : RedirectAttributes
         // 작성 성공 시 이동할 게시판 코드 : @PathVariable("boardCode")
      
         /* List<MultipartFile>
          * - 업로드된 이미지가 없어도 List에 요소 MultipartFile 객체가 추가됨
          * 
          * - 단, 업로드된 이미지가 없는 MultipartFile 객체는
          *   파일크기(size)가 0 또는 파일명(getOriginalFileName())이 ""
          * 
          * */

      // 1. 로그인한 회원 번호를 얻어와 board에 세팅
      board.setMemberNo( loginMember.getMemberNo() );
      
      // 2. boardCode도 board에 세팅
      board.setBoardCode(boardCode);
      
      // 3. 업로드된 이미지 서버에 실제로 저장되는 경로
      //    + 웹에서 요청 시 이미지를 볼 수 있는 경로(웹 접근경로)
      String webPath = "/resources/images/board/";
      String filePath = session.getServletContext().getRealPath(webPath);
      
      // 게시글 삽입 서비스 호출 후 삽입된 게시글 번호 반환 받기
      int boardNo = service.boardInsert(board, images, webPath, filePath);
      
      // 게시글 삽입 성공 시
      // -> 방금 삽입한 게시글의 상세 조회 페이지 리다이렉트
      // -> /board/{boardCode}/{boardNo}
      
      String message = null;
      String path = "redirect:";
      
      if(boardNo > 0) { // 성공 시
         
         message = "게시글이 등록되었습니다.";
         path += "/board/" + boardCode + "/" + boardNo;
               
      } else {
         message = "게시글 등록 실패ㅠㅠ.";
         path += "insert";
      }
      
      ra.addFlashAttribute("message", message);
      
      return path;
   }

🔎 BoardService2

🔎 BoardServiceImpl2

// 게시글 삽입
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int boardInsert(Board board, List<MultipartFile> images, String webPath, String filePath) throws IllegalStateException, IOException {

		// 0. XSS 방지 처리
		board.setBoardTitle( Util.XSSHandling( board.getBoardTitle() ) );
		board.setBoardContent( Util.XSSHandling( board.getBoardContent() ) );

		// 1. BOARD 테이블 INSERT 하기 (제목, 내용, 작성자, 게시판코드)
		// -> boardNo(시퀀스로 생성한 번호) 반환 받기
		int boardNo = dao.boardInsert(board);

		// 2. 게시글 삽입 성공 시
		//    업로드된 이미지가 있다면 BOARD_IMG 테이블에 삽입하는 DAO 호출
		if(boardNo > 0) { // 게시글 삽입 성공 시

			// List<MultipartFile> images
			// -> 업로드된 파일이 담긴 객체 MultipartFile이 5개 존재
			// -> 단, 업로드된 파일이 없어도 MultipartFile 객체는 존재

			// 실제 업로드된 파일의 정보를 기록할 List
			List<BoardImage> uploadList = new ArrayList<>();

			// images에 담겨있는 파일 중 실제 업로드된 파일만 분류
			for(int i = 0; i < images.size(); i++) {

				if(images.get(i).getSize() > 0) {

					BoardImage img = new BoardImage();

					// img에 파일 정보를 담아서 uploadList에 추가
					img.setImagePath(webPath); // 웹 접근 경로
					img.setBoardNo(boardNo); // 게시글 번호
					img.setImageOrder(i); // 이미지 순서

					// 파일 원본명0
					String fileName = images.get(i).getOriginalFilename();
					img.setImageOriginal(fileName); // 원본명
					img.setImageReName(Util.fileRename(fileName)); // 변경명

					uploadList.add(img);
				}
			}// 분류 for문 종료

			// 분류 작업 후 uploadList가 비어있지 않은 경우
			// == 업로드한 파일이 있다
			if(!uploadList.isEmpty()) {

				// BOARD_IMG 테이블에 INSERT하는 DAO 호출
				int result = dao.insertImageList(uploadList);
				// result == 삽입된 행의 개수 == uploadList.size()

				// 삽입된 행의 개수와 uploadList의 개수가 같다면
				// == 전체 insert 성공
				if(result == uploadList.size()) { 

					// 서버에 파일을 저장(transferTo())

					// images      : 실제 파일이 담긴 객체 리스트
					//            (업로드 안된 인덱스 빈칸)

					// uploadList : 업로드된 파일의 정보 리스트
					//            (원본명, 변경명, 순서, 경로, 게시글 번호)

					// 순서 == images 업로드된 인덱스

					for(int i = 0; i < uploadList.size(); i++) {

						int index = uploadList.get(i).getImageOrder();

						// 파일로 변환
						String rename = uploadList.get(i).getImageReName();

						images.get(index).transferTo(new File(filePath + rename));

					}


				} else { // 일부 또는 전체 insert 실패

					// ** 웹 서비스 수행 중 1개라도 실패하면 전체 실패 **
					// -> rollback 필요

					// @Transactional(rollbackFor = Exception.class)
					// -> 예외가 발생해야지만 롤백

					// [결론]
					// 예외를 강제 발생 시켜서 rollback 해야된다
					// -> 사용자 정의 예외 생성
					throw new FileUploadException(); // 예외 강제 발생

				}
			}
		}

		return boardNo;
	}

🔎 BoardDAO2

🔎 board-mapper.xml

💥 useGeneratedKeys 속성 : DB 내부적으로 생성한 키 (시퀀스)를 전달된 파라미터의 필드로 대입 가능 여부

*************** 동적 SQL*************** : 프로그램 수행중 SQL을 변경하는 기능( mybatis의 가장 강력기능!!)

<selectKey> 태그 : INSERT/UPDATE 시 사용할 키(시퀀스)를 조회해서 파라미터의 지정된 필드에 대입

order 속성 : 메인 SQL이 수행되기 전/후에 selectKey 가 수행되도록 지정  전 : BEFORE 후 : AFTER 

keyProperty 속성 : selectKey 조회 결과를 저장할 파라미터의 필드

	<insert id="boardInsert" parameterType="Board"
		useGeneratedKeys="true">

		<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,
		#{memberNo},
		#{boardCode})
	</insert>

	<!-- 이미지 리스트(여러 개) 삽입 -->
	<insert id="insertImageList" parameterType="list">
		INSERT INTO BOARD_IMG
		SELECT SEQ_IMG_NO.NEXTVAL, A.*
		FROM (

		<foreach collection="list" item="img" separator="UNION ALL ">
			SELECT #{img.imagePath} IMG_PATH,
			#{img.imageReName} IMG_RENAME,
			#{img.imageOriginal} IMG_ORIGINAL,
			#{img.imageOrder} IMG_ORDER,
			#{img.boardNo} BOARD_NO
			FROM DUAL
		</foreach>
		) A
	</insert>

💥 동적 SQL 중 <foreach> - 특정 SQL 구문을 반복할 때 사용  - 반복되는 사이에 구분자(separator)를 추가할 수 있음. 
collection : 반복할 객체의 타입 작성(list, set, map...) 
item : collection에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수 
index : 현재 반복 접근중인 인덱스 (0,1,2,3,4 ..) 
open : 반복 전에 출력할 sql 
close : 반복 종료 후에 출력한 sql 
separator : 반복 사이사이 구분자

 

UNION ALL : 각 쿼리의 모든 결과를 포함한 합집합 (중복제거 안함) 반복할떄마다 맨밑에 붙어줘서

SQL에 INSERT시 반복문 돌았던만큼에 결과값이 한번에 INSERT 된다.