Spring Boot Board Project_22 게시글 삽입

송지윤·2024년 4월 25일
0

Spring Framework

목록 보기
55/65

로그인한 사람만 게시글 쓸 수 있게(로그인 안하면 글쓰기 버튼 보이지 않게)

1. html 에서 로그인한 멤버가 있을 때만 글쓰기 버튼 보이게 설정해주기

boardList.html

			<div class="btn-area">
				<!-- 로그인 상태일 때만 글쓰기 버튼 노출 -->
				<button id="insertBtn" th:if="${session.loginMember}">글쓰기</button>
			</div>

<script src="/js/board/boardList.js"></script>

2. 글쓰기 버튼 js 에서 얻어와서 이벤트 걸어주기 (글쓰기 버튼 눌렀을 때 boardWrite.html 넘어가게 설정해주기)

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`;
    });
};

3. editBoard 로 시작하는 요청 받기 위해 새로운 Controller 생성 후 Controller에서 boardWrite.html 로 보내주기

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

게시판 글 작성 시 image 글 입력 따로 (insert 2번 진행, 테이블이 다름)

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">&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>

1. html 에서 넘겨준 값을 EditBoardController 매개변수로 받아서 Service 호출

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

2. Service 로직처리

List<MultipartFile> images

  • 이미지 5개 모두 업로드 o => 0 ~ 4번 인덱스에 파일 모두 저장됨
  • 이미지 5개 모두 업로드 X => 0 ~ 4번 인덱스에 파일 저장 X
  • 2번 인덱스만 이미지 업로드 => 2번 인덱스만 파일 저장, 0/1/3/4번 인덱스는 저장 X

[문제점]

  • 파일이 선택되지 않은 input 태그들도 제출되고 있다.
    ( 제출은 되어있는데 실제로 들어온 데이터는 "" (빈칸) )
    -> 파일 선택이 안된 input 태그 값을 서버에 저장하려고 하면 오류 발생

[해결 방법]

  • 무작정 서버에 저장 X
    -> 제출된 파일이 있는지 확인하는 로직을 추가 구성 + List 요소의 index 번호 == BOARD_IMG 테이블 IMG_ORDER 와 같음

2-1. 게시글 부분을 먼저 BOARD 테이블 INSERT 하기 -> INSERT 한 결과로 작성된 게시글 번호(생성된 시퀀스 번호) 반환 받기

EditBoardServiceImpl

	@Override
	public int boardInsert(Board inputBoard, List<MultipartFile> images) {

		int result = mapper.boardInsert(inputBoard);

edit-board-mapper.xml

useGeneratedKeys 속성 : DB 내부적으로 생성한 키 (시퀀스)를 전달된 파라미터의 필드로 대입 가능 여부 지정
(selectKey를 사용하기 위한 속성)

동적 SQL

  • 프로그램 수행 중 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();
		

2-2. 돌아온 결과값 가지고 이미지에 대한 로직 처리

		// 실제 업로드된 이미지의 정보를 모아둔 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;
		}

xml 구문 해석 (sql 함수)

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 로 함수 수행

조회 결과

  • CREATE OR REPLACE FUNCTION NEXT_IMG_NO: 이 줄은 NEXT_IMG_NO라는 함수의 생성 또는 대체를 시작합니다.
  • RETURN NUMBER: 이 줄은 함수가 숫자를 반환할 것임을 명시합니다.
  • IS IMG_NO NUMBER;: 이 부분은 IMG_NO라는 지역 변수를 숫자형으로 선언합니다.
  • BEGIN: 이 부분은 함수의 실행 가능한 섹션의 시작을 표시합니다.
  • SELECT SEQ_IMG_NO.NEXTVAL INTO IMG_NO FROM DUAL;: 이 줄은 SEQ_IMG_NO 시퀀스에서 다음 값을 선택하고 그 값을 IMG_NO 변수에 할당합니다. DUAL 테이블은 Oracle에서 가짜 열을 선택하는 데 사용되는 더미 테이블입니다.
  • RETURN IMG_NO;: 이 줄은 IMG_NO의 값을 반환합니다. 즉, 시퀀스의 다음 값입니다.
  • END;: 이 부분은 함수의 끝을 표시합니다.

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
);

2-3. uploadList 가 비어있지 않다면 이미지 INSERT 하는 구문 mapper 호출

ServiceImpl

		// 이미지 있다면 (image INSERT)
		// 선택한 파일이 존재할 경우
		// -> "BOARD_IMG" 테이블에 INSERT + 서버에 파일 저장
		
		result = mapper.insertUploadList(uploadList);

edit-board-mapper.xml

위에서 작성한 SQL 문을 mybatis 형태로 변경해서 사용

동적 SQL 중 <foreach>

  • Mybatis에서 제공하는 향상된 for문
  • 특정 SQL 구문을 반복할 때 사용
  • 반복 사이에 구분자(separator)를 추가할 수 있음

[지원하는 속성]
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>

2-4. mapper 호출 후 결과값 가지고 제대로 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);
	}
}

3. service 에서 돌려준 값 Controller에서 처리 후 return

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

4. 로그인 안해도 주소입력하면 들어가지는 것 filter

FilterConfig 에 구문 추가

String[] filteringURL = {"/myPage/*", "/editBoard/*"};

0개의 댓글