1) SpringBoot
1-1) 복습 예제
1-1-1) 파일 첨부 기능에 이미지 미리보기 및 다운로드 기능 추가
1-2) Ajax 예제
1-2-1) 테스트 예제
1) SpringBoot
1-1) 복습 예제
stream : 데이터가 흘러가는 통로를 의미함
< 입력 / 출력 >
"문자"와 "byte"의 차이점
- 입력이면서 문자 단위로 처리하는 것에는 "Reader"가 들어감
- 입력이면서 byte 단위로 처리하는 것에는 "Input"이 들어감
- 출력이면서 문자 단위로 처리하는 것에는 "Writer"가 들어감
- 출력이면서 byte 단위로 처리하는 것에는 "Output"이 들어감
1-1-1) 파일 첨부 기능에 이미지 미리보기 및 다운로드 기능 추가
(1) [src/main/java] - [net.datasa.web5.controller] 안에 BoardController.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.BoardDTO;
import net.datasa.web5.domain.dto.ReplyDTO;
import net.datasa.web5.security.AuthenticatedUser;
import net.datasa.web5.service.BoardService;
/**
* 게시판 관련 Controller
*/
@Slf4j
@RequestMapping("board")
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
//application.properties 파일의 게시판 관련 설정값
@Value("${board.pageSize}")
int pageSize;
@Value("${board.linkSize}")
int linkSize;
@Value("${board.uploadPath}")
String uploadPath;
// 메인화면에서 "board/list" 경로를 클릭했을 때 처리하는 메소드
// templates/boardView/list.html 파일로 포워딩
// 로그인 안 한 상태에서 해당 페이지의 "게시판"이라는 제목이 보여야 함
/**
* 게시판 목록을 조회하고 페이징 및 검색 기능을 제공
*
* @param model 모델 객체
* @param page 현재 페이지 (default: 0)
* @param searchType 검색 대상 (default: "")
* @param searchWord 검색어 (default: "")
* @return 글 목록 한 페이지
*/
@GetMapping("list")
public String list(Model model,
@RequestParam(name="page", defaultValue = "1") int page,
@RequestParam(name="searchType", defaultValue = "") String searchType,
@RequestParam(name="searchWord", defaultValue = "") String searchWord) {
log.debug("properties의 값 : pageSize={}, linkSize={}, uploadPath={}",
pageSize, linkSize, uploadPath);
log.debug("요청 파라미터 : page={}, searchType={}, searchWord={}",
page, searchType, searchWord);
// 서비스로 (페이지, 페이지당 글 수, 검색조건, 검색어) 전달하여 결과를 Page 객체로 받음
Page<BoardDTO> boardPage = boardService.getList(page, pageSize, searchType, searchWord);
// Page 객체 내의 정보들
log.debug("목록정보 getContent() : {}", boardPage.getContent());
log.debug("현재페이지 getNumber() : {}", boardPage.getNumber());
log.debug("전체 개수 getTotalElements() : {}", boardPage.getTotalElements());
log.debug("전체 페이지수 getTotalPages() : {}", boardPage.getTotalPages());
log.debug("한페이지당 글 수 getSize() : {}", boardPage.getSize());
log.debug("이전페이지 존재 여부 hasPrevious() : {}", boardPage.hasPrevious());
log.debug("다음페이지 존재 여부 hasNext() : {}", boardPage.hasNext());
// HTML로 포워딩하기 전에 모델에 필요 정보들을 저장
model.addAttribute("boardPage", boardPage);
model.addAttribute("page", page);
model.addAttribute("linkSize", linkSize);
model.addAttribute("searchType", searchType);
model.addAttribute("searchWord", searchWord);
// HTML로 포워딩
return "boardView/list";
/*
// 서비스에서 전체 글 목록 전달받음
List<BoardDTO> boardList = boardService.getList(searchWord);
log.debug("전달된 글 목록 : {}", boardList);
// 글 목록을 모델에 저장하고 HTML로 포워딩하여 출력
model.addAttribute("boardList", boardList);
return "boardView/list";
*/
}
/**
* 글쓰기 폼으로 이동
* @return 글쓰기 폼을 출력하는 HTML 파일
* */
@GetMapping("write")
public String write() {
return "boardView/writeForm";
}
/**
* 글 저장
* @param boardDTO 작성한 글 정보 (제목, 내용)
* @param user 로그인한 사용자 정보
* @return 게시판 글목록 경로
*/
@PostMapping("write")
public String write(@AuthenticationPrincipal AuthenticatedUser user,
@ModelAttribute BoardDTO boardDTO,
@RequestParam("upload") MultipartFile upload) {
log.debug("전달된 글정보 : {}", boardDTO);
// 작성한 글에 사용자 아이디 추가
boardDTO.setMemberId(user.getUsername());
log.debug("저장할 글 정보 : {}", boardDTO);
if (upload != null) {
log.debug("파일 존재 여부 : {}", upload.isEmpty());
log.debug("파라미터 이름 : {}", upload.getName());
log.debug("파일의 이름 : {}", upload.getOriginalFilename());
log.debug("크기 : {}", upload.getSize());
log.debug("파일 종류 : {}", upload.getContentType());
}
try {
boardService.saveWrite(boardDTO, uploadPath, upload);
return "redirect:list";
}
catch (Exception e) {
e.printStackTrace();
return "boardView/writeForm";
}
}
/**
* 게시글 상세보기
* @param model 모델
* @param boardNum 조회할 글 번호
* @return 게시글 상세보기 HTML 경로
*/
@GetMapping("read")
public String read(@RequestParam("boardNum") int boardNum,
Model model) {
try {
BoardDTO dto = boardService.getBoard(boardNum);
log.debug("선택한 글 정보 : {}", dto);
// 해당 게시글을 모델에 저장하고 HTML로 포워딩하여 출력
model.addAttribute("boardDTO", dto);
return "boardView/readForm";
} catch (Exception e) {
// e.printStackTrace() : 예외가 발생할 시 Console에 에러를 출력해주는 역할을 함
e.printStackTrace();
return "redirect:list";
}
}
/**
* 본인 게시글 삭제
* @param boardNum 삭제할 글 번호
* @param user 로그인한 사용자 정보
* @return 게시판 글 목록 보기 경로
* */
@GetMapping("delete")
public String delete(@RequestParam("boardNum") int boardNum,
@AuthenticationPrincipal AuthenticatedUser user) {
try {
// 삭제할 글 번호와 로그인한 아이디를 서비스로 전달하여 본인 글인 경우에만 삭제
boardService.delete(boardNum, user.getUsername(), uploadPath);
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:list";
}
/**
* 게시글 수정 폼으로 이동
* @param boardNum 수정할 글번호
* @param user 로그인한 사용자 정보
* @return 수정폼 HTML
*/
@GetMapping("update")
public String update(@RequestParam("boardNum") int boardNum,
@AuthenticationPrincipal AuthenticatedUser user,
Model model) {
try {
BoardDTO boardDTO = boardService.getBoard(boardNum);
if (!user.getUsername().equals(boardDTO.getMemberId())) {
throw new RuntimeException("수정 권한이 없습니다.");
}
model.addAttribute("board", boardDTO);
return "boardView/updateForm";
}
catch (Exception e) {
e.printStackTrace();
return "redirect:list";
}
}
/**
* 게시글 수정 처리
* @param boardDTO 수정할 글 정보
* @param user 로그인한 사용자 정보
* @return 수정폼 HTML
*/
// update form에서 글 내용 수정하고 "수정" 버튼 누르면 받아서 수정된 글 저장
@PostMapping("update")
public String update(@ModelAttribute BoardDTO boardDTO,
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam("upload") MultipartFile upload) {
try {
boardService.update(boardDTO, user.getUsername(), uploadPath, upload);
return "redirect:read?boardNum=" + boardDTO.getBoardNum();
}
catch (Exception e) {
e.printStackTrace();
return "redirect:list";
}
}
/**
* 리플 쓰기
* @param replyDTO 저장할 리플 정보
* @param user 로그인 사용자 정보
* @return 게시글 보기 경로로 이동
*/
// read form에서 리플 입력란에 내용 입력 후 "확인" 버튼 누르면 새로 입력한 리플 저장
@PostMapping("rippleWrite")
public String rippleWrite(@ModelAttribute ReplyDTO replyDTO,
@AuthenticationPrincipal AuthenticatedUser user) {
replyDTO.setMemberId(user.getUsername());
boardService.rippleWrite(replyDTO);
return "redirect:read?boardNum=" + replyDTO.getBoardNum();
}
/**
* 리플 삭제
* @param replyDTO 삭제할 리플번호와 본문 글번호
* @param user 로그인한 사용자 정보
* @return 게시글 상세보기 경로
*/
@GetMapping("rippleDelete")
public String rippleDelete(@ModelAttribute ReplyDTO replyDTO,
@AuthenticationPrincipal AuthenticatedUser user) {
try {
log.debug("삭제할 replyDTO : {}", replyDTO);
boardService.rippleDelete(replyDTO.getReplyNum(), user.getUsername());
}
catch (Exception e) {
e.printStackTrace();
}
return "redirect:read?boardNum=" + replyDTO.getBoardNum();
}
/**
* 첨부파일 다운로드
* @param boardNum 글 번호
* @param response 응답정보
* */
@GetMapping("download")
public void download(@RequestParam("boardNum") Integer boardNum
, HttpServletResponse response) {
boardService.download(boardNum, response, uploadPath);
}
}
(2) [src/main/java] - [net.datasa.web5.service] 안에 BoardService.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.service;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;
import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.BoardDTO;
import net.datasa.web5.domain.dto.ReplyDTO;
import net.datasa.web5.domain.entity.BoardEntity;
import net.datasa.web5.domain.entity.MemberEntity;
import net.datasa.web5.domain.entity.ReplyEntity;
import net.datasa.web5.repository.BoardRepository;
import net.datasa.web5.repository.MemberRepository;
import net.datasa.web5.repository.ReplyRepository;
/**
* 게시판 관련 서비스
* */
@Slf4j
@RequiredArgsConstructor
@Service
// @Transactional : Commit, Rollback을 감지하는 역할을 하는 Annotation
@Transactional
public class BoardService {
private final BoardRepository boardRepository;
private final MemberRepository memberRepository;
private final ReplyRepository replyRepository;
/**
* 게시판 글 저장
* @param boardDTO 저장할 글 정보
* @param uploadPath 파일 저장할 경로
* @param upload 업로드한 파일
* */
public void saveWrite(BoardDTO boardDTO, String uploadPath, MultipartFile upload) throws IOException {
MemberEntity memberEntity = memberRepository.findById(boardDTO.getMemberId())
.orElseThrow(() -> new EntityNotFoundException("회원 아이디가 없습니다."));
BoardEntity entity = new BoardEntity();
entity.setMember(memberEntity);
entity.setTitle(boardDTO.getTitle());
entity.setContents(boardDTO.getContents());
// 첨부파일이 있는지 확인
if (upload != null && !upload.isEmpty()) {
// 저장할 경로의 디렉토리가 있는지 확인 -> 없으면 생성
File directoryPath = new File(uploadPath);
if (!directoryPath.isDirectory()) {
directoryPath.mkdirs();
}
// 저장할 파일명 생성
// 내 이력서.doc -> 20240806_6156df53-a49b-4419-a336-cb6da0fa9640.doc
String originalName = upload.getOriginalFilename();
String extension = originalName.substring(originalName.lastIndexOf(".")); // 확장자를 가져옴
String dateString = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String uuidString = UUID.randomUUID().toString();
String fileName = dateString + "_" + uuidString + extension;
// 파일 이동
try {
File file = new File(uploadPath, fileName);
upload.transferTo(file);
// entity에 원래 파일명, 저장된 파일명 추가
entity.setOriginalName(originalName);
entity.setFileName(fileName);
} catch (IOException e) {
e.printStackTrace();
}
}
log.debug("저장되는 entity : {}", entity);
// DB에 저장
boardRepository.save(entity);
}
/**
* 검색 결과 글 목록을 지정한 한 페이지 분량의 Page 객체로 리턴
* @param page 현재 페이지
* @param pageSize 페이지당 글 수
* @param searchType 검색조건(제목 검색: title, 본문 검색: contents, 작성자 검색: id)
* @param searchWord 검색어
* @return 게시글 목록 정보
* */
public Page<BoardDTO> getList(int page, int pageSize, String searchType, String searchWord) {
// 조회 조건을 담은 Pageable 객체 생성
// page의 경우, 배열의 인덱스처럼 0부터 시작하기에 1페이지(인덱스로 0페이지에 해당)를 가져오기 위해 "page - 1"로 작성함
Pageable p = PageRequest.of(page - 1, pageSize, Sort.Direction.DESC, "boardNum");
Page<BoardEntity> entityPage;
// repository의 메소드로 Pageable 전달하여 조회. Page 리턴받음.
if (searchType.equals("title")) {
entityPage = boardRepository.findByTitleContaining(searchWord, p);
} else if (searchType.equals("contents")) {
entityPage = boardRepository.findByContentsContaining(searchWord, p);
} else if (searchType.equals("id")) {
entityPage = boardRepository.findByMember_MemberId(searchWord, p);
} else {
entityPage = boardRepository.findAll(p);
}
log.debug("조회된 결과 Entity 페이지 : {}", entityPage.getContent());
// entityPage 객체 내의 Entity들을 DTO 객체로 변환하여 새로운 Page 객체 생성
Page<BoardDTO> dtoPage = entityPage.map(this::convertToDTO);
return dtoPage;
}
/**
* DB에서 조회한 게시글 정보인 BoardEntity 객체를 BoardDTO 객체로 변환
* @param entity 게시글 정보 Entity 객체
* @return 게시글 정보 DTO 개체
*/
private BoardDTO convertToDTO(BoardEntity entity) {
return BoardDTO.builder()
.boardNum(entity.getBoardNum())
.memberId(entity.getMember() != null ? entity.getMember().getMemberId() : null)
.memberName(entity.getMember() != null ? entity.getMember().getMemberName() : null)
.title(entity.getTitle())
.contents(entity.getContents())
.viewCount(entity.getViewCount())
.likeCount(entity.getLikeCount())
.originalName(entity.getOriginalName())
.fileName(entity.getFileName())
.createDate(entity.getCreateDate())
.updateDate(entity.getUpdateDate())
.build();
}
/**
* ReplyEntity객체를 ReplyDTO 객체로 변환
* @param entity 리플 정보 Entity 객체
* @return 리플 정보 DTO 객체
*/
private ReplyDTO convertToReplyDTO(ReplyEntity entity) {
return ReplyDTO.builder()
.replyNum(entity.getReplyNum())
.boardNum(entity.getBoard().getBoardNum())
.memberId(entity.getMember().getMemberId())
.memberName(entity.getMember().getMemberName())
.contents(entity.getContents())
.createDate(entity.getCreateDate())
.build();
}
/**
* 게시글 1개 조회
* @param boardNum 글번호
* @return the BoardDTO 글 정보
* @throws EntityNotFoundException 게시글이 없을 때 예외
*/
// localhost:8888/board/read?boardNum=1
public BoardDTO getBoard(int boardNum) {
BoardEntity entity = boardRepository.findById(boardNum)
.orElseThrow(() -> new EntityNotFoundException("해당 번호의 글이 없습니다."));
entity.setViewCount(entity.getViewCount() + 1);
log.debug("{}번 게시물 조회 결과 : {}", boardNum, entity);
BoardDTO dto = convertToDTO(entity);
List<ReplyDTO> replyDTOList = new ArrayList<ReplyDTO>();
for (ReplyEntity replyEntity : entity.getReplyList()) {
ReplyDTO replyDTO = convertToReplyDTO(replyEntity);
replyDTOList.add(replyDTO);
}
dto.setReplyList(replyDTOList);
return dto;
}
/**
* 게시글 삭제
* @param boardNum 삭제할 글번호
* @param username 로그인한 아이디
*/
public void delete(int boardNum, String username, String uploadPath) {
BoardEntity boardEntity = boardRepository.findById(boardNum)
.orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다."));
if (!boardEntity.getMember().getMemberId().equals(username)) {
throw new RuntimeException("삭제 권한이 없습니다.");
}
// 첨부파일이 있으면 삭제
if (boardEntity.getFileName() != null && !boardEntity.getFileName().isEmpty()) {
File file = new File(uploadPath, boardEntity.getFileName());
file.delete();
}
boardRepository.delete(boardEntity);
}
/**
* 게시글 수정
* @param boardDTO 수정할 글정보
* @param username 로그인한 아이디
*/
public void update(BoardDTO boardDTO, String username, String uploadPath, MultipartFile upload) {
BoardEntity boardEntity = boardRepository.findById(boardDTO.getBoardNum())
.orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다."));
if (!boardEntity.getMember().getMemberId().equals(username)) {
throw new RuntimeException("수정 권한이 없습니다.");
}
log.debug("boardDTO 수정 전 첨부 파일 확인용 : {}", boardDTO);
//전달된 정보 수정
boardEntity.setTitle(boardDTO.getTitle());
boardEntity.setContents(boardDTO.getContents());
// 수정하면서 새로 첨부한 파일이 있으면(새로 첨부한 파일에 관한 정보는 "upload"에 담겨 있다.)
if (upload != null && !upload.isEmpty()) {
// 그 전에 업로드한 기존 파일이 있으면 먼저 파일 삭제
if (boardEntity.getFileName() != null && !boardEntity.getFileName().isEmpty()) {
File file = new File(uploadPath, boardEntity.getFileName());
file.delete();
}
// 새로운 파일의 이름을 바꿔서 복사
String originalName = upload.getOriginalFilename();
String extension = originalName.substring(originalName.lastIndexOf(".")); // 확장자를 가져옴
String dateString = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String uuidString = UUID.randomUUID().toString();
String updateFileName = dateString + "_" + uuidString + extension;
String updateOriginName = upload.getOriginalFilename();
// 파일 이동
try {
File file = new File(uploadPath, updateFileName);
upload.transferTo(file);
// Entity에 새 파일의 원래이름, 저장된 이름 추가
boardEntity.setOriginalName(updateOriginName);
boardEntity.setFileName(updateFileName);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 리플 저장
* @param replyDTO 작성한 리플 정보
* @throws EntityNotFoundException 사용자 정보가 없을 때 예외
*/
public void rippleWrite(ReplyDTO replyDTO) {
MemberEntity memberEntity = memberRepository.findById(replyDTO.getMemberId())
.orElseThrow(() -> new EntityNotFoundException("사용자 아이디가 없습니다."));
BoardEntity boardEntity = boardRepository.findById(replyDTO.getBoardNum())
.orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다."));
ReplyEntity entity = ReplyEntity.builder()
.board(boardEntity)
.member(memberEntity)
.contents(replyDTO.getContents())
.build();
log.debug("저장되는 entity : {}", entity);
// DB에 저장
replyRepository.save(entity);
}
/**
* 리플 삭제
* @param replyNum 삭제할 리플 번호
* @param username 로그인한 아이디
*/
public void rippleDelete(Integer replyNum, String username) {
ReplyEntity replyEntity = replyRepository.findById(replyNum)
.orElseThrow(() -> new EntityNotFoundException("리플이 없습니다."));
if (!replyEntity.getMember().getMemberId().equals(username)) {
throw new RuntimeException("삭제 권한이 없습니다.");
}
replyRepository.delete(replyEntity);
}
/**
* 파일 다운로드
* @param boardNum 게시글 번호
* @param response 응답정보
* @param uploadPath 파일 저장 경로
* */
public void download(Integer boardNum, HttpServletResponse response, String uploadPath) {
// 글 번호로 게시글 정보 DB에서 조회
BoardEntity boardEntity = boardRepository.findById(boardNum)
.orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다."));
// 응답정보의 헤더에 파일명 추가
// 원래의 파일명
try {
response.setHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode(boardEntity.getOriginalName(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 저장된 파일 경로(C:/upload/240806_wi3yriu2roiwuoeruqo.jpg)
String fullPath = uploadPath + "/" + boardEntity.getFileName();
// 서버의 파일을 읽을 입력 스트림과 클라이언트에게 전달할 출력 스트림
FileInputStream fileIn = null;
ServletOutputStream fileOut = null;
try {
// 파일의 저장 경로에서 파일 읽기
fileIn = new FileInputStream(fullPath);
// 읽은 파일 정보를 response 객체를 통해 출력
fileOut = response.getOutputStream();
// Spring의 파일 관련 유틸 이용하여 출력
FileCopyUtils.copy(fileIn, fileOut);
// 스트림 닫기
fileIn.close();
fileOut.close();
} catch (IOException e) {
e.printStackTrace();
}
// 응답정보의 헤더에 파일명 추가
// 파일의 저장 경로에서 파일 읽기
// 읽은 파일 정보를 response 객체를 통해 출력
// 스트림 닫기
}
/**
* 게시판의 글 목록 조회
* @return 글 목록 정보
* */
/*
public List<BoardDTO> getList(String searchWord) {
// BoardRepository의 메소드를 호출하여 게시판의 모든 글 정보를 조회
// Entity의 개수만큼 반복하면서 Entity의 값을 BoardDTO 객체를 생성하여 저장
// 생성된 BoardDTO 객체를 ArrayList에 저장
// 최종 완성된 ArrayList 객체를 리턴함
// Sort : SQL의 order by 구문을 만들어주는 역할
Sort sort = Sort.by(Sort.Direction.DESC, "boardNum");
List<BoardEntity> entityList = boardRepository.findByTitleContaining(searchWord, sort);
// List<BoardEntity> entityList = boardRepository.findTop3ByTitleContainingOrderByBoardNum(searchWord);
// List<BoardEntity> entityList = boardRepository.findAll(sort);
List<BoardDTO> boardDTOList = new ArrayList<>();
// 반복문으로 Entity 객체를 DTO로 변환해서 ArrayList에 저장
for (BoardEntity entity : entityList) {
BoardDTO dto = BoardDTO.builder()
.boardNum(entity.getBoardNum())
.memberId(entity.getMember().getMemberId())
.memberName(entity.getMember().getMemberName())
.title(entity.getTitle())
.viewCount(entity.getViewCount())
.createDate(entity.getCreateDate())
.updateDate(entity.getUpdateDate())
.build();
boardDTOList.add(dto);
}
return boardDTOList;
}
*/
}
(3) [src/main/resources] - [templates.boardView] 안에 readForm.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>게시글 읽기</title>
<style>
a {
text-decoration-line: none;
color: black;
font-weight: bold;
}
#readArea {
width: 900px;
margin: 0 auto;
text-align: center;
}
#readForm {
display: flex;
flex-direction: column;
justify-content: center;
}
#formArea {
display: flex;
justify-content: center;
margin-bottom: 50px;
}
#boardRead {
width: 800px;
}
#boardRead, #boardRead tr, #boardRead td {
border: 2px solid black;
border-collapse: collapse;
padding: 10px;
margin: 20px 0 15px 0;
}
th {
width: 100px;
height: 40px;
background-color: grey;
color: white;
}
#boardRead td {
width: 500px;
height: 40px;
text-align: left;
}
#boardRead #fileImage {
display: inline-block;
width: 100px;
height: 100px;
}
#replyForm {
margin: 50px 0;
}
#rippleList {
width: 800px;
margin: 0 auto;
border-left: 2px solid black;
border-collapse: collapse;
}
#rippleList .visible {
border: 2px solid black;
border-collapse: collapse;
}
#rippleList th {
width: 120px;
}
#rippleContents {
width: 470px;
}
#rippleDate {
width: 150px;
}
#rippleDel {
border: none;
}
</style>
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<script>
$(document).ready(function() {
//글 삭제
$('#deleteBtn').click(function() {
let boardNum = $(this).data('num');
if (confirm('삭제하시겠습니까?')) {
location.href = 'delete?boardNum=' + boardNum;
}
});
//글 수정
$('#updateBtn').click(function() {
let boardNum = $(this).data('num');
location.href = 'update?boardNum=' + boardNum;
});
//리플 작성
$('#replyForm').submit(function() {
if ($('#contents').val().length < 5) {
alert('리플 내용을 5자 이상으로 입력하세요.');
$('#contents').focus();
$('#contents').select();
return false;
}
return true;
});
});
//리플 삭제
function rippleDelete(replyNum, boardNum) {
if (confirm('삭제하시겠습니까?')) {
location.href = `rippleDelete?replyNum=${replyNum}&boardNum=${boardNum}`;
}
}
</script>
</head>
<body>
<div id="readArea">
<h1>[ 게시글 읽기 ]</h1>
<div id="readForm">
<div id="formArea">
<table id="boardRead">
<tr>
<th>작성자</th>
<td th:text="${boardDTO.memberId}"></td>
</tr>
<tr>
<th>작성일</th>
<td th:text="${#temporals.format(boardDTO.createDate, 'yyyy.MM.dd HH:mm:ss')}"></td>
</tr>
<tr>
<th>조회수</th>
<td th:text="${boardDTO.viewCount}"></td>
</tr>
<tr>
<th>제목</th>
<td th:text="${boardDTO.title}"></td>
</tr>
<tr>
<th>내용</th>
<td th:text="${boardDTO.contents}"></td>
</tr>
<tr>
<th>파일첨부</th>
<td>
<img th:src="|download?boardNum=${boardDTO.boardNum}|" id="fileImage">
<a th:href="@{/board/download(boardNum=${boardDTO.boardNum})}"
th:text="${boardDTO.originalName}"></a>
</td>
</tr>
</table>
</div>
<!-- 글 수정/삭제(본인글일 때만 보임) -->
<div id="btnArea" th:if="${#authentication.name} == ${boardDTO.memberId}">
<input type="button" id="updateBtn" value="수정" th:data-num="${boardDTO.boardNum}">
<input type="button" id="deleteBtn" value="삭제" th:data-num="${boardDTO.boardNum}">
</div>
<!-- 리플 작성 폼(로그인했을 때만 보임) -->
<div id="rippleArea" sec:authorize="isAuthenticated()">
<form th:action="@{/board/rippleWrite}" method="post" id="replyForm">
<span>리플내용 </span>
<input type="text" name="contents" id="contents">
<!-- controller의 rippleWrite 메소드(PostMapping)에 글 번호를 넘겨주기 위해 추가한 내용이다. -->
<input type="hidden" name="boardNum" id="boardNum" th:value="${boardDTO.boardNum}" />
<!-- <button>저장</button> -->
<input type="submit" value="저장" />
</form>
</div>
<!-- 리플 목록 출력(다 보임) -->
<table id="rippleList">
<tr th:each="reply : ${boardDTO.replyList}">
<th th:text="${reply.memberId}" class="visible"></th>
<td th:text="${reply.contents}" id="rippleContents" class="visible"></td>
<td th:text="${#temporals.format(reply.createDate, 'yy.MM.dd HH:mm')}" id="rippleDate" class="visible"></td>
<td th:if="${#authentication.name} == ${reply.memberId}" id="rippleDel">
<a th:href="|javascript:rippleDelete(${reply.replyNum}, ${reply.boardNum})|" id="rippleDelBtn">X</a>
</td>
</tr>
</table>
</div>
</div>
</body>
</html>
(4) 결과 화면
첫 접속 화면 : 메인 페이지, 게시판 글 목록 페이지는 로그인 안해도 볼 수 있음(현재는 글 작성 및 수정 기능 구현을 위해 아래 게시판 글 목록 화면은 로그인을 한 상태이다!)
글 읽기 화면 : 파일 첨부된 각 목록 화면을 보여줌(이미지 파일)
- 글 읽기는 로그인 여부에 관계없이 확인 가능함
- 각각의 글 정보 화면(글 읽기)에서 첨부된 이미지 파일 다운로드 가능함
1-2) Ajax 예제
1-2-1) 테스트 예제
(1) [src/main/java] - [net.datasa.test_ajax.controller] 안에 HomeController.java 파일 생성 후 아래와 같이 작성
package net.datasa.test_ajax.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
public class HomeController {
// 메인화면
@GetMapping({"", "/"})
public String home() {
return "home";
}
}
(2) [src/main/resources] - [templates] 안에 home.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test_ajax</title>
</head>
<body>
<h1>[ Ajax 예제 ]</h1>
<p>
<a href="ajax1">Ajax 테스트 페이지 1</a>
</p>
<p>
<a href="#"></a>
</p>
<p>
<a href="#"></a>
</p>
</body>
</html>
(3) [src/main/java] - [net.datasa.test_ajax.controller] 안에 AjaxController.java 파일 생성 후 아래와 같이 작성
package net.datasa.test_ajax.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
@Controller
public class AjaxController {
@GetMapping("ajax1")
public String ajax1() {
return "ajax1";
}
@ResponseBody
@GetMapping("ajaxtest1")
public void ajaxtest1() {
log.debug("AjaxController의 ajaxtest1() 메소드 실행함");
}
}
(4) [src/main/resources] - [templates] 안에 ajax1.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test_ajax</title>
<script src="/js/jquery-3.7.1.min.js"></script>
<script>
function test1() {
alert("test1() 실행");
// 비동기적으로 서버 요청 보내기(http://localhost:8888/ajaxtest1)
$.ajax({
url: "ajaxtest1",
type: "get",
success: function() { alert("성공"); },
error: function() { alert("실패"); }
});
alert("test1() 끝");
}
</script>
</head>
<body>
<h1>[ Ajax 테스트 1 ]</h1>
여기는 ajax.html입니다.
<p>
<a href="javascript:test1()">서버로 요청 보내기</a>
</p>
</body>
</html>
(5) 결과 화면
첫 접속 화면
Ajax 테스트 페이지 1 화면
"서버로 요청 보내기" 문구 클릭 시 화면
'SpringBoot' 카테고리의 다른 글
SpringBoot(33) - 학생 성적 관리 예제(학생 성적 입력 / 목록 출력 / 수정 및 삭제) (0) | 2024.08.09 |
---|---|
SpringBoot(32) - Ajax 예제2(서버로 Ajax 요청 및 문자열 보내기 / 서버에서 문자열 받기 / 값 및 연산자 전달하여 계산하기) (0) | 2024.08.08 |
SpringBoot(30) - 복습 예제(글 작성 및 수정 파일 첨부 기능 추가) (0) | 2024.08.06 |
SpringBoot(29) - 복습 예제(글 읽기 / 글 수정 및 삭제 / 댓글 작성 및 삭제 기능 업데이트) (0) | 2024.08.05 |
SpringBoot(28) - 복습 예제(글 읽기 기능 업데이트 / 글 수정 및 삭제 / 댓글 작성 및 삭제) (0) | 2024.08.03 |