[SpringBoot] TIL 081 - 23.11.21

유진·2023년 11월 20일
0
post-thumbnail

SpringBoot

board2-mapper.xml

: insert, update, delete

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="edu.kh.project.board.model.dao.BoardMapper2">
	
	<!-- 게시글 삽입 -->
	<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>
	
	
	<!-- 
		동적 SQL 중 <foreach>
		- 특정 sql 구문을 반복할 때 사용
		- 반복되는 사이에 구분자를 추가할 수 있음.
	
	 -->
	 
	 <!-- 이미지 리스트(여러개)삽입 -->
	 <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>
	 
	 
	 <!-- 게시글 수정 -->
	 <update id="boardUpdate">
	 	UPDATE "BOARD" SET
	 	BOARD_TITLE = #{boardTitle},
	 	BOARD_CONTENT = #{boardContent},
	 	B_UPDATE_DATE = SYSDATE
	 	WHERE BOARD_CODE = #{boardCode}
	 	AND BOARD_NO = #{boardNo}
	 </update>
	 
	 
	 <!-- 이미지 삭제 -->
	 <delete id="imageDelete">
	 	DELETE FROM "BOARD_IMG"
	 	WHERE BOARD_NO = #{boardNo}
	 	AND IMG_ORDER IN ( ${deleteList} )
	 </delete>
	 
	 
	<!-- 이미지 수정 -->
	<update id="imageUpdate">
		UPDATE "BOARD_IMG" SET
		IMG_PATH = #{imagePath},
		IMG_ORIGINAL = #{imageOriginal},
		IMG_RENAME = #{imageReName}
		WHERE BOARD_NO = #{boardNo}
		AND IMG_ORDER = #{imageOrder}
	</update>
	
	<!-- 이미지 삽입 -->
	<insert id="imageInsert">
		INSERT INTO "BOARD_IMG"
		VALUES(SEQ_IMG_NO.NEXTVAL, #{imagePath}, #{imageReName},
			#{imageOriginal}, #{imageOrder}, #{boardNo}
		)
	</insert>
	
	<!-- 게시글 삭제 -->
	<update id="boardDelete">
		UPDATE BOARD SET
		BOARD_DEL_FL = 'Y'
		WHERE BOARD_CODE = #{boardCode}
		AND BOARD_NO = #{boardNo}
	</update>
	 

</mapper>

BoardController2.java

package edu.kh.project.board.controller;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.service.BoardService;
import edu.kh.project.board.model.service.BoardService2;
import edu.kh.project.member.model.dto.Member;
import jakarta.servlet.http.HttpSession;

@Controller
@RequestMapping("/board2")
@SessionAttributes({"loginMember"})
public class BoardController2 {
	
	@Autowired
	private BoardService2 service; // 삽입,수정,삭제
	
	@Autowired
	private BoardService boardService; // 목록, 상세 조회
	
	
	// 게시글 작성 화면 전환
	@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
			) throws IllegalStateException, IOException {
	
		// 파라미터 : 제목, 내용, 파일(0~5개)
		// 파일 저장 경로 : HttpSession
		// 세션 : 로그인한 회원의 번호
		// 리다이렉트 시 데이터 전달 : RedirectAttributes ra (message)
		
		/* List<MultipartFile>
		 * - 업로드된 이미지가 없어도 List에 MultipartFile 요소는 존재함.
		 * 
		 * - 단, 업로드된 이미지가 없는 MultipartFile 요소는
		 * 	 파일 크기(size)가 0 또는 파일명(getOriginalFileName()) "" 빈칸
		 * 
		 * */
		
		// 1. 로그인한 회원 번호를 얻어와 board에 세팅
		board.setMemberNo(loginMember.getMemberNo());
		
		// 2. boardCode도 board에 세팅
		board.setBoardCode(boardCode);
		
		
		
		// 게시글 삽입 서비스 호출 후 삽입된 게시글 번호 반환 받기
		int boardNo = service.boardInsert(board, images);
		
		
		// 게시글 삽입 성공 시
		// -> 방금 삽입한 게시글의 상세 조회 페이지 리다이렉트
		// -> /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;
		
	}
	
	// 게시글 수정 화면 전환
	@GetMapping("/{boardCode}/{boardNo}/update")   // /board2/2/2006/update?cp=1
	public String boardUpdate(
			@PathVariable("boardCode") int boardCode,
			@PathVariable("boardNo") int boardNo,
			Model model
			// Model : 데이터 전달용 객체 (기본 scope: request)
			) {
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("boardCode", boardCode);
		map.put("boardNo", boardNo);
		
		Board board = boardService.selectBoard(map);
		
		model.addAttribute("board", board);
		
		return "board/boardUpdate";
		
	}
	
	// 게시글 수정
	@PostMapping("/{boardCode}/{boardNo}/update")
	public String boardUpdate(
			Board board, // 커맨드 객체(name == 필드 경우 필드에 파라미터 세팅)
			@PathVariable("boardCode") int boardCode,
			@PathVariable("boardNo") int boardNo,
			@RequestParam(value="cp", required = false, defaultValue = "1") int cp, // 쿼리스트링 유지
			@RequestParam(value="images", required = false) List<MultipartFile> images, // 업로드된 파일 리스트
			@RequestParam(value="deleteList", required = false) String deleteList, // 삭제할 이미지 순서
			HttpSession session, // 서버 파일 저장 경로 얻어올 용도
			RedirectAttributes ra // 리다이렉트 시 값 전달용(message)
			) throws IllegalStateException, IOException {
		
		// 1) boardCode, boardNo를 커맨드 객체(board)에 세팅
		board.setBoardCode(boardCode);
		board.setBoardNo(boardNo);
		
		// board ( boardCode, boardNo, boardTitle, boardContent )
		
		// 2) 이미지 서버 저장경로, 웹 접근 경로
		String webPath = "/resources/images/board/";
		String filePath = session.getServletContext().getRealPath(webPath);
		
		// 3) 게시글 수정 서비스 호출
		int rowCount = service.boardUpdate(board, images, webPath, filePath, deleteList);
		
		// 4) 결과에 따라 message, path 설정
		String message = null;
		String path = "redirect:";
		
		
		if(rowCount > 0) {
			message = "게시글이 수정되었습니다";
			path += "/board/" + boardCode + "/" + boardNo + "?cp=" + cp;
		} else {
			message = "게시글 수정 실패";
			path += "update";
		}
		
		ra.addFlashAttribute("message", message);
		
		return path;
	}
	
	
	// 게시글 삭제 = DELETE 하지 않고 UPDATE 하기!
	@GetMapping("/{boardCode}/{boardNo}/delete")
	public String boardDelete(
			@PathVariable("boardCode") int boardCode,
			@PathVariable("boardNo") int boardNo,
			RedirectAttributes ra
			) {
		
		// boardCode, boardNo 서비스로 넘겨야함
		// map 으로 담아서 보내는걸 추천
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("boardCode", boardCode);
		map.put("boardNo", boardNo);
		
		int result = service.boardDelete(map);
		
		// 결과값이 > 0 라면
		// "삭제되었습니다" 
		// /board/{boardCode} = 게시글 목록으로 돌아감
		
		// else
		// "삭제 실패"
		// /board/{boardCode}/{boardNo}
		
		String message = null;
		String path = "redirect:";
		
		if(result > 0) {
			System.out.println("삭제되었습니다");
			
			message = "삭제되었습니다";
			path += "/board/" + boardCode;
			
		} else {
			System.out.println("삭제 실패");
			
			message = "삭제 실패";
			path += "/board/" + boardCode + boardNo;
			
		}
		
		ra.addFlashAttribute("message", message);
		
		return path;
	}
	
	
	

}

BoardService2.java

package edu.kh.project.board.model.service;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.springframework.web.multipart.MultipartFile;

import edu.kh.project.board.model.dto.Board;

public interface BoardService2 {

	/** 게시글 삽입
	 * @param board
	 * @param images
	 * @param webPath
	 * @param filePath
	 * @return boardNo
	 */
	int boardInsert(Board board, List<MultipartFile> images)
								throws IllegalStateException, IOException;

	/** 게시글 수정 서비스
	 * @param board
	 * @param images
	 * @param webPath
	 * @param filePath
	 * @param deleteList
	 * @return rowCount
	 */
	int boardUpdate(Board board, List<MultipartFile> images,
			String webPath, String filePath, String deleteList) throws IllegalStateException, IOException;

	/** 게시글 삭제 서비스
	 * @param map
	 * @return result
	 */
	int boardDelete(Map<String, Object> map);

}

BoardServiceImpl2.java

package edu.kh.project.board.model.service;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import edu.kh.project.board.model.exception.FileUploadException;
import edu.kh.project.board.model.exception.ImageDeleteException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import edu.kh.project.board.model.dao.BoardMapper2;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;
import edu.kh.project.common.utility.Util;

@Service
@PropertySource("classpath:/config.properties")
public class BoardServiceImpl2 implements BoardService2{
	
	@Autowired
	private BoardMapper2 mapper;
	
	@Value("${my.board.webpath}")
	private String webPath;
	
	@Value("${my.board.location}")
	private String filePath;
	

	// 게시글 삽입
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int boardInsert(Board board, List<MultipartFile> images) 
			throws IllegalStateException, IOException {
		
		/* XSS 방지 처리 더이상 하지 않아도 됨.
		 * 
		 * th:text ------> <h1>안녕하세요</h1> 태그 그대로 나와서 따로 처리하지 않아도 됨
		 * 
		 * 태그 그대로 해석하고 싶을때? th:utext  [(${text})]
		 * 
		 * */
		
		
		// 1. BOARD 테이블 INSERT 하기 (제목, 내용, 작성자, 게시판 코드)
		// -> boardNo (시퀀스로 생성한 번호) 반환 받기
		int result = mapper.boardInsert(board);
		
		// 실패 시 서비스 종료
		if(result == 0) return 0;
		
		// mapper.xml에서 selectKey 태그로 인해 세팅된 값 얻어오기
		int boardNo = board.getBoardNo();
		
		
		
		// 2. 게시글 삽입 성공 시
		// 업로드된 이미지가 있다면 BOARD_IMG 테이블에 삽입하는 DAO 호출
		if(boardNo > 0) { // 게시글 삽입 성공 시
			
			// List<MultipartFile> images
			// -> 업로드된 파일이 담긴 객체 MultipartFile이 5개 존재
			// -> 단, 업로드된 파일이 없어도 MultipartFile 객체는 존재
			
			// 실제 업로드된 파일의 정보를 기록할 List
			List<BoardImage> uploadList = new ArrayList<BoardImage>();
			
			// images에 담겨있는 파일 중 실제 업로드된 파일만 분류
			for(int i = 0; i < images.size(); i++) {
				
				// i번째 요소에 업로드된 파일이 있다면
				if(images.get(i).getSize() > 0) {
					
					BoardImage img = new BoardImage();
					
					// img 에 파일 정보를 담아서 uploadList 에 추가
					img.setImagePath(webPath); // 웹 접근 경로
					img.setBoardNo(boardNo); // 게시글 번호
					img.setImageOrder(i); // 이미지 순서

					// 파일 원본명
					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 호출
				result = mapper.insertImageList(uploadList);
				// result == 삽입된 행의 개수
				
				// 전체 insert 성공 여부 확인
				// 삽입된 행의 개수와 uploadList 의 개수가 같다면
				// == 전체 insert 성공
				if(result == uploadList.size()) {
					
					// 서버에 파일을 저장 (transferTo)
					
					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 실패
					
					// rollback 필요
					
					// @Transactional(rollbackFor = Exception.class)
					// -> 예외가 발생 해야지만 롤백
					
					// [결론]
					// 예외를 강제 발생 시켜서 rollback 해야된다
					// 사용자 정의 예외 만들어 발생시키자!
					
					
					throw new FileUploadException();
				}
				
			}
			
		}
		
		
		return boardNo;
	}

	// 게시글 수정 서비스
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int boardUpdate(Board board, List<MultipartFile> images, String webPath, String filePath,
			String deleteList) throws IllegalStateException, IOException {
		
		// 1. 게시글 제목/내용만 수정
		// 1) XSS 방지 처리
		board.setBoardTitle( Util.XSSHandling( board.getBoardTitle() ));
		board.setBoardContent( Util.XSSHandling( board.getBoardContent() ));
		
		// 2) DAO 호출
		int rowCount = mapper.boardUpdate(board);
		
		
		// 2. 게시글 부분이 수정 성공 했을 때
		if(rowCount > 0) {
			
			if(!deleteList.equals("")) { // 삭제할 이미지가 있다면
				
				// 3. deleteList에 작성된 이미지 모두 삭제
				Map<String, Object> deleteMap = new HashMap<String, Object>();
				deleteMap.put("boardNo", board.getBoardNo());
				deleteMap.put("deleteList", deleteList);
				
				rowCount = mapper.imageDelete(deleteMap);
				
				if(rowCount == 0) { // 이미지 삭제 실패 시 전체 롤백
									// -> 예외 강제로 발생
					
					throw new ImageDeleteException();
				}
		
			}
			
			
			// 4. 새로 업로드된 이미지 분류 작업
			// images : 실제 파일이 담긴 List
			//         -> input type="file" 개수만큼 요소가 존재
			//         -> 제출된 파일이 없어도 MultipartFile 객체가 존재
			
			List<BoardImage> uploadList = new ArrayList<>();
			
			for(int i=0 ; i<images.size(); i++) {
				
				if(images.get(i).getSize() > 0) { // 업로드된 파일이 있을 경우
					
					// BoardImage 객체를 만들어 값 세팅 후 
					// uploadList에 추가
					BoardImage img = new BoardImage();
					
					// img에 파일 정보를 담아서 uploadList에 추가
					img.setImagePath(webPath); // 웹 접근 경로
					img.setBoardNo(board.getBoardNo()); // 게시글 번호
					img.setImageOrder(i); // 이미지 순서
					
					// 파일 원본명
					String fileName = images.get(i).getOriginalFilename();
					
					img.setImageOriginal(fileName); // 원본명
					img.setImageReName( Util.fileRename(fileName) ); // 변경명    
					
					uploadList.add(img);
					
					// 오라클은 '다중 UPDATE를 지원하지 않기 때문에'
					// 하나씩 UPDATE 수행
					
					rowCount = mapper.imageUpdate(img);
					
					if(rowCount == 0) {
						// 수정 실패 == DB에 이미지가 없었다 
						// -> 이미지를 삽입
						rowCount = mapper.imageInsert(img);
					}
				}
			}
			
			
			// 5. uploadList에 있는 이미지들만 서버에 저장(transferTo())
			if(!uploadList.isEmpty()) {
				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)  );                    
				}
			}
		}
		
		
		return rowCount;
	}

	// 게시글 삭제 서비스
	@Override
	public int boardDelete(Map<String, Object> map) {
		return mapper.boardDelete(map);
	}
}

BoardMapper2.java

package edu.kh.project.board.model.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;

@Mapper
public interface BoardMapper2 {
	
	/** 게시글 삽입
	 * @param board
	 * @return boardNo
	 */
	public int boardInsert(Board board);

	/** 이미지 리스트 삽입
	 * @param uploadList
	 * @return result
	 */
	public int insertImageList(List<BoardImage> uploadList);

	/** 게시글 수정
	 * @param board
	 * @return rowCount
	 */
	public int boardUpdate(Board board);

	/** 이미지 삭제
	 * @param deleteMap
	 * @return rowCount
	 */
	public int imageDelete(Map<String, Object> deleteMap);

	/** 이미지 수정
	 * @param img
	 * @return
	 */
	public int imageUpdate(BoardImage img);

	/** 이미지 삽입
	 * @param img
	 * @return rowCount
	 */
	public int imageInsert(BoardImage img);

	/** 게시글 삭제
	 * @param map
	 * @return
	 */
	public int boardDelete(Map<String, Object> map);
}


== Console에서 에러나지 않으면 백엔드 코드 잘 고쳐진 것!


boardDetail.html

<!DOCTYPE html>
<html lang="ko" xmlns="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${boardName}">게시판 이름</title>

    <link rel="stylesheet" th:href="@{/css/board/boardDetail-style.css}">
    <link rel="stylesheet" th:href="@{/css/board/comment-style.css}">

    <script th:src="@{https://kit.fontawesome.com/f7459b8054.js}" crossorigin="anonymous"></script>
</head>
<body>
    <main>
        <th:block th:replace="~{common/header}"></th:block>

        <section class="board-detail" th:object="${board}">  
            <!-- 제목 -->
            <h1 class="board-title" th:utext="|*{boardTitle}  <span> - ${boardName}</span>|"></h1>

            <!-- 프로필 + 닉네임 + 작성일 + 조회수 -->
            <div class="board-header">
                <div class="board-writer">

                    
					<!-- 프로필 이미지가 없을 경우 기본 이미지 출력 -->
					<img th:unless="*{profileImage}" th:src="@{/images/user.png}">     

					
					<!-- 프로필 이미지가 있을 경우 등록한 이미지 출력 -->
					<img th:if="*{profileImage}" th:src="*{profileImage}" />
                    

                    <span th:text="*{memberNickname}">닉네임</span>

                    <!-- 좋아요 하트 -->
                    <span class="like-area">
                    
						<!-- th:classappend : 클래스 추가 -->
						<!-- 삼항 연산자 ==== 조건 ? 참 : 거짓 -->
                        <i class="fa-heart"    
							th:classappend="${likeCheck} ? fa-solid : fa-regular"
						id="boardLike"></i>

                        <span th:text="*{likeCount}">좋아요 개수</span>
                    </span>

                </div>

                <div class="board-info">
                    <p> <span>작성일</span> [[*{boardCreateDate}]] </p>     

                    <!-- 수정한 게시글인 경우 -->

                    <p th:if="*{boardUpdateDate}"> 
						<span>마지막 수정일</span> [[*{boardUpdateDate}]]
					</p>   

                    <p> <span>조회수</span> [[${board.readCount}]] </p>                    
                </div>
            </div>

            <!-- 이미지가 있을 경우 -->
            <th:block th:if="${#lists.size(board.imageList) > 0}">
            
	            <!-- 썸네일 영역(썸네일이 있을 경우) -->
	            <!--
	            	- 이미지는 IMG_ORDER 오름차순 정렬된다
	            	- IMG_ORDER의 값이 0인 이미지 썸네일이다
	            	-> imageList에 썸네일이 있다면
	            		조회되었을때 IMG_ORDER가 0인 이미지가
	            		imageList[0]에 저장되어 있을 것이다.
	             -->
	             <th:block th:if="${thumbnail}"> <!-- 복잡한 로직 웬만해서는 타임리프 사용 X -->
		            <h5>썸네일</h5>
		            <div class="img-box">
		                <div class="boardImg thumbnail">

		                    <img th:src="|${thumbnail.imagePath}${thumbnail.imageReName}|">
		                    
							<a th:href="|${thumbnail.imagePath}${thumbnail.imageReName}|" 
		                    	th:download="${thumbnail.imageOriginal}"
		                    >다운로드</a>         

		                </div>
		            </div>
	             </th:block>
	             
				 <!-- 일반 이미지가 있는 경우 -->
				 <th:block th:if="${#lists.size(board.imageList) > start}">
				 
					 <!-- 업로드 이미지 영역 -->
					 <h5>업로드 이미지</h5>
					 <div class="img-box">
						 
						 <th:block th:each="i : ${#numbers.sequence(start, #lists.size(board.imageList)-1, 1)}">
							 <div class="boardImg"
								 th:with="path=|${board.imageList[i].imagePath}${board.imageList[i].imageReName}|">
							   
								 <img th:src="${path}">
	 
								 <a th:href="${path}" th:download="${board.imageList[i].imageOriginal}">다운로드</a>                
							 </div>
						 </th:block>

					 </div>
				 
				 </th:block>
            </th:block>
            
            

            <!-- 내용 -->
            <div class="board-content">[[${board.boardContent}]]</div>


            <!-- 버튼 영역-->
            <div class="board-btn-area">
            
            	<!-- 로그인한 회원과 게시글 작성자 번호가 같은 경우 -->
				<!-- 객체?.필드명 : 안전 탐색 연산자(Safe Navigation Operator)
					객체가 null인지 탐색해서 null 이 아닐 경우 필드 접근
					(NullPointerException 방지)
				-->
				<!-- <th:block th:if="${session.loginMember != null and session.loginMember.memberNo == board.memberNo}" ></th:block> -->
            	<th:block th:if="${session.loginMember?.memberNo == board.memberNo}">
	                <button id="updateBtn">수정</button>
	                <button id="deleteBtn">삭제</button>
            	</th:block>


                <button id="goToListBtn">목록으로</button>
            </div>


        </section>

        <!-- 댓글 include-->
        <th:block th:replace="~{board/comment}"></th:block>
    </main>

    <th:block th:replace="~{common/footer}"></th:block>

	<script th:inline="javascript">
		console.log(/*[[${board}]]*/ "board객체");

		const boardNo = /*[[${board.boardNo}]]*/ "게시글번호";
		
		const loginMemberNo = /*[[${session.loginMember?.memberNo}]]*/ "로그인 회원번호";

		if(loginMemberNo == null) loginMemberNo = "";
	
		console.log(boardNo);
		console.log(loginMemberNo);

	</script>
	
	<script th:src="@{/js/board/boardDetail.js}"></script>
	<script th:src="@{/js/board/comment.js}"></script>
	
</body>
</html>

BoardController.java

package edu.kh.project.board.controller;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;
import edu.kh.project.board.model.service.BoardService;
import edu.kh.project.member.model.dto.Member;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@SessionAttributes({"loginMember"})
@RequestMapping("/board")
@Controller
public class BoardController {
	
	@Autowired
	private BoardService service;
	
	/*  목록 조회 : /board/1?cp=1 (cp: current page(현재페이지))
	 *  상세 조회 : /board/1/1500?cp=1
	 *  
	 *  ** 컨트롤러 따로 생성 **
	 *  삽입 : /board2/1/insert
	 *  수정 : /board2/1/1500/update
	 *  삭제 : /board2/1/1500/delete
	 * */
	
	/*
	 * ******** @PathVariable 사용 시 문제점과 해결법 ********
	 * 
	 * 문제점 : 요청 주소와 @PathVariable로 가져다 쓸 주소와 레벨이 같다면
	 * 		구분하지 않고 모두 매핑되는 문제가 발생
	 * 
	 * 해결방법 : @PathVariable 지정 시 정규 표현식 사용
	 * {키:정규표현식}
	 * 
	 * 
	 * @PathVariable : URL 경로에 있는 값을 매개변수로 이용할 수 있게하는 어노테이션
	 * + request scope에 세팅
	 * 
	 * /board/1    /board?code=1 -> 용도의 차이점이 존재
	 * 
	 * - 자원(resource) 구분/식별
	 * ex) github.com/cmhinst
	 * ex) github.com/testUser
	 * ex) /board/1 -> 1번(공지사항) 게시판
	 * ex) /board/2 -> 2번(자유 게시판) 게시판
	 * 
	 * query string 을 사용하는 경우            -> 부가적인 옵션이라고 생각하기!
	 * - 검색, 정렬, 필터링
	 * ex) search.naver.com?query=날씨
	 * ex) search.naver.com?query=종로맛집
	 * ex) /board2/insert?code=1
	 * ex) /board2/insert?code=2
	 * -> 삽입이라는 공통된 동작 수행
	 * 		단, code에 따라 어디에 삽입할지 지정(필터링)
	 * 
	 * ex) /board/list?order=recent (최신순)
	 * ex) /board/list?order=most   (인기순)
	 * 
	 * */
	
	// 게시글 목록 조회
	@GetMapping("/{boardCode:[0-9]+}") // boardCode는 1자리 이상 숫자
									   // @PathVariable : 주소를 값 자체로 쓸 수 있는 것
	public String selectBoardList( @PathVariable("boardCode") int boardCode,
								@RequestParam(value="cp", required = false, defaultValue = "1") int cp,
								Model model, // 데이터 전달용 객체
								@RequestParam Map<String, Object> paramMap // 파라미터 전부 다 담겨있음(검색 시)  // {"key":"t", "query":"test"}
							) {
		
		// boardCode 확인
		//System.out.println("boardCode : " + boardCode);
		
		if( paramMap.get("key") == null ) { // 검색어가 없을 때 (검색 X)
			
			// 게시글 목록 조회 서비스 호출
			Map<String, Object> map = service.selectBoardList(boardCode, cp);
			
			// 조회 결과를 request scope에 세팅 후 forward
			model.addAttribute("map", map);
			
		} else { // 검색어가 있을 때 (검색 O)
			
			paramMap.put("boardCode", boardCode); // paramMap = key, query, boardCode
			
			Map<String, Object> map = service.selectBoardList(paramMap, cp); // 오버로딩 적용
			
			model.addAttribute("map", map);
		}
		
		return "board/boardList";
	}
	
	
	// @PathVariable : 주소에 지정된 부분을 변수에 저장
	//				 + request scope 세팅
	
	// 게시글 상세 조회  //   /board/1/1500
	@GetMapping("/{boardCode}/{boardNo}")
	public String boardDetail(
				@PathVariable("boardCode") int boardCode,
				@PathVariable("boardNo") int boardNo,
				Model model, // 데이터 전달용 객체
				RedirectAttributes ra, // 리다이렉트 시 데이터 전달 객체
				@SessionAttribute(value="loginMember", required = false) Member loginMember,
				// 세션에서 loginMember를 얻어오는데 없으면 null, 있으면 회원정보 저장
				
				// 쿠키를 이용한 조회 수 증가에서 사용
				HttpServletRequest req,
				HttpServletResponse resp
				) throws ParseException {
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("boardCode", boardCode);
		map.put("boardNo", boardNo);
		
		// 게시글 상세 조회 서비스 호출
		Board board = service.selectBoard(map);
		
		String path = null;
		
		if(board != null) { // 조회 결과가 있을 경우
			
			// ----------------------------------------------------------
			// 현재 로그인 상태인 경우
			// 로그인한 회원이 해당 게시글에 좋아요를 눌렀는지 확인
			
			// 로그인 상태인 경우
			if(loginMember != null) {
				// 회원번호를 얻어와야해요
				// map(boardCode, boardNo, memberNo)
				map.put("memberNo", loginMember.getMemberNo());
				
				
				// 좋아요 여부 확인 서비스 호출
				int result = service.boardLikeCheck(map);
				
				// 결과값을 통해 분기처리
				// 누른적이 있을 경우 처리
				// "likeCheck"
				if(result > 0) model.addAttribute("likeCheck", "on"); // 누른적이 있을 경우 "likeCheck" 키 값에 "on" 값을 넣겠다.
				
				
			}
			
			// ---------------------------------------------------------
			
			// 쿠키를 이용한 조회 수 증가 처리
			
			// 1) 비회원(로그인 안한 상태인 사람) 또는 로그인한 회원의 글이 아닌경우
			if(loginMember == null ||
					loginMember.getMemberNo() != board.getMemberNo() ) {
				
				// 2) 쿠키 얻어오기
				Cookie c = null;
				
				// 요청에 담겨있는 모든 쿠키 얻어오기
				Cookie[] cookies = req.getCookies(); // Cookies ['아이디 저장', '다른 쿠키', ..., 'readBoardNo']
				
				if(cookies != null) { // 쿠키가 존재할 경우
					
					// 쿠키 중 "readBoardNo" 라는 쿠키를 찾아서 c에 대입
					for(Cookie cookie : cookies) {
						if(cookie.getName().equals("readBoardNo")) {
							c = cookie;
							break;
						}
					}
					
				}
				
				
				// 3) 기존 쿠키가 없거나 ( c == null )
				//	  존재는 하나 현재 게시글 번호가
				//	  쿠키에 저장되지 않은 경우  ( 해당 게시글 본적 없음 )
				
				// 결과 저장용 변수 선언
				int result = 0;
				
				if(c == null) {
					// 쿠키가 존재 X -> 하나 새로 생성
					c = new Cookie("readBoardNo", "|" + boardNo + "|");
					
					// 조회수 증가 서비스 호출
					result = service.updateReadCount(boardNo);
				} else {
					// 현재 게시글 번호가 쿠키에 있는지 확인
					
					// Cookie.getValue() : 쿠키에 저장된 모든 값을 읽어옴 -> String으로 반환
					
					// String.indexOf("문자열")
					// : 찾는 문자열이 String 몇번 인덱스에 존재하는지 반환
					// 단, 없으면 -1 반환
					
					if(c.getValue().indexOf("|" + boardNo + "|") == -1) {
						// 현재 게시글 번호가 쿠키에 없다면
						
						// 기존 값에 게시글 번호 추가해서 다시 세팅
						c.setValue( c.getValue() + "|" + boardNo + "|" );
						
						// 조회수 증가 서비스 호출
						result = service.updateReadCount(boardNo);
						
					}
				} // 3) 종료
				
				// 쿠키의 수명 세팅
				if(result > 0) {
					board.setReadCount(board.getReadCount() + 1);
					// 조회된 board 조회 수와 DB 조회 수 동기화
					
					// 적용 경로 설정
					c.setPath("/"); //  "/" 이하 경로 요청 시 쿠키 서버로 전달
					
					// 수명 지정
					Calendar cal = Calendar.getInstance(); // 싱글톤 패턴(미리 만들어진 하나의 객체만을 이용)
					cal.add(cal.DATE, 1);
					
					// 날짜 표기법 변경 객체 (DB의 TO_CHAR()와 비슷)
					SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
					
					// java.util.Date
					Date a = new Date(); // 현재 시간
					
					Date temp = new Date(cal.getTimeInMillis()); // 내일 (24시간 후)
					// 2023-05-11 12:16:10
					
					Date b = sdf.parse(sdf.format(temp)); // 내일 0시 0분 0초
					
					
					// 내일 0시 0분 0초 - 현재 시간
					long diff = (b.getTime()  -  a.getTime()) / 1000; 
					// -> 내일 0시 0분 0초까지 남은 시간을 초단위로 반환
					
					c.setMaxAge((int)diff); // 수명 설정
					
					resp.addCookie(c); // 응답 객체를 이용해서
									   // 클라이언트에게 전달
				}
				
				
			}
			
			
			// ---------------------------------------------------------
			
			path = "board/boardDetail"; // forward 할 jsp 경로
			model.addAttribute("board", board);
			
			// 게시글에 이미지가 있을 경우
			//if(!board.getImageList().isEmpty())
			if(board.getImageList().size() > 0) {
				
				BoardImage thumbnail = null;
				
				// 0번 인덱스 이미지의 순서가 0인 경우 == 썸네일
				if(board.getImageList().get(0).getImageOrder() == 0) {
					thumbnail = board.getImageList().get(0);
				}
				
				model.addAttribute("thumbnail", thumbnail); // 썸네일 없으면 null
				
				// 썸네일 있으면 start = 1
				//		  없으면 start = 0
				model.addAttribute("start", thumbnail != null ? 1 : 0);
				
			}
			
			
		} else { // 조회 결과가 없을 경우
			path = "redirect:/board/" + boardCode;
			// 게시판 첫페이지로 리다이렉트
			
			ra.addFlashAttribute("message", "해당 게시글이 존재하지 않습니다");
			
		}
		
		return path;
		
	}
	
	// 좋아요 처리
	@PostMapping("/like")
	@ResponseBody // 반환되는 값이 비동기 요청한 곳으로 돌아가게 함
	public int like(@RequestBody Map<String, Integer> paramMap) {
		return service.like(paramMap);
	}
	
	
	
	

}

comment.html

<div id="commentArea">
    <!-- 댓글 목록 -->
    <div class="comment-list-area">
        
        <ul id="commentList">
        
        	
			<!-- 부모/자식 댓글 -->         
			<!-- 삼항 연산자 === 거짓일 때 딱히 해야할게 없다면 생략해도 상관없음! -->
			<li class="comment-row"
				th:each="comment : ${board.commentList}"
				th:classappend="${comment.parentNo} != 0 ? child-comment"
				th:object="${comment}"
			>
				
				<p class="comment-writer">

					<!-- 프로필 이미지 -->
					<!-- 없을 경우 기본 이미지 -->
					<img th:unless="*{profileImage}" th:src="@{/images/user.png}">
				
					<!-- 있을 경우 프로필 이미지 -->
					<img th:if="*{profileImage}" th:src="*{profileImage}">

					
					<!-- 닉네임 -->
					<span>[[*{memberNickname}]]</span>
					
					<!-- 작성일 -->
					<span class="comment-date" th:text="*{commentCreateDate}">작성일</span>
				</p>
				
				<!-- 댓글 내용 -->
				<p class="comment-content" th:text="*{commentContent}">내용</p>


				<!-- 버튼 영역 -->
				<div class="comment-btn-area">
					<button th:onclick="|showInsertComment(*{commentNo}, this)|">답글</button>   
						
					<!-- 로그인한 회원과 댓글 작성자가 같은 경우 -->
					<th:block th:if="${session.loginMember?.memberNo == comment.memberNo}">
						<button th:onclick="|showUpdateComment(*{commentNo}, this)|">수정</button>
						<button th:onclick="|deleteComment(*{commentNo})|">삭제</button>
					</th:block>
					
				</div>
			</li>
        	

        </ul>
    </div>
    

    <!-- 댓글 작성 부분 -->
    <div class="comment-write-area">
        <textarea id="commentContent"></textarea>
        <button id="addComment">
            댓글<br>
            등록
        </button>
 
    </div>

</div>

boardWrite.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${boardName}">게시판 이름</title>

    <link rel="stylesheet" th:href="@{/css/board/boardWrite-style.css}">
</head>
<body>
    <main>
        <th:block th:replace="~{common/header}"></th:block>

        <form th:action="@{insert}" method="POST" class="board-write" id="boardWriteFrm" enctype="multipart/form-data">  
            <h1 class="board-name" th:text="${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>

    </main>

    <th:block th:replace="~{common/footer}"></th:block>

    <script th:src="@{/js/board/boardWrite.js}"></script>

</body>
</html>

0개의 댓글