/************************************************************ * * @packageName : io.company.localhost.controller.api * @fileName : BoardController.java * @author : 서지희 * @date : 25.01.07 * @description : 게시판 * * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 25.01.07 서지희 최초 생성 * *************************************************************/ package io.company.localhost.controller.api; import java.io.IOException; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.DeleteMapping; 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.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import com.github.pagehelper.PageInfo; import io.company.localhost.common.annotation.Member; import io.company.localhost.common.annotation.ParameterCheck; import io.company.localhost.common.annotation.ReqMap; import io.company.localhost.common.dto.ApiResponse; import io.company.localhost.common.dto.MapDto; import io.company.localhost.common.exception.NotFoundHandler; import io.company.localhost.service.commoncodService; import io.company.localhost.service.localbordService; import io.company.localhost.utils.AuthUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @RestController @RequestMapping("/api/board") @RequiredArgsConstructor @Slf4j public class BoardController { private final localbordService boardService; private final commoncodService commoncodService; private final PasswordEncoder passwordEncoder; /** * 공지사항 목록 조회 * @ReqMap map 요청 파라미터 (searchKeyword) * @return 전체 공지사항 목록 */ @Member @ParameterCheck @GetMapping("/notices") public ApiResponse> getNotices(@ReqMap MapDto map) { // size를 안전하게 Integer로 변환하여 MapDto에 다시 넣기 Object sizeObj = map.get("size"); Integer size = null; if (sizeObj instanceof String) { size = Integer.parseInt((String) sizeObj); }else { size = null; } map.put("size", size); return ApiResponse.ok(boardService.selectNotices(map)); } /** * 자유/익명 게시판 목록 조회 * @ReqMap map 요청 파라미터 (page, searchKeyword, orderBy, size) * @return 페이징된 자유/익명 게시판 목록 */ @Member @ParameterCheck @GetMapping("/general") public ApiResponse> getGeneralPosts(@ReqMap MapDto map) { return ApiResponse.ok(boardService.selectGeneralPosts(map)); } /** * 게시물 작성 * @ReqMap map 요청 파라미터 (LOCBRDTTL, LOCBRDCON, MEMBERSEQ, LOCBRDTYP, * LOCBRDPWD(익명일 때만), LOCBRDCAT(지식커뮤니티만)) * @return 작성된 게시물의 ID */ @Member @ParameterCheck @PostMapping public ApiResponse createBoard(@ReqMap MapDto map) { if (map.containsKey("LOCBRDPWD") && !map.getString("LOCBRDPWD").trim().isEmpty()) { // 빈 값 체크 String rawPassword = map.getString("LOCBRDPWD"); String hashedPassword = passwordEncoder.encode(rawPassword); map.put("LOCBRDPWD", hashedPassword); } return ApiResponse.ok(boardService.insertBoard(map)); } /** * 게시물 상세보기 * @param boardId 게시물 ID * @return 게시물 상세정보 */ @Member @ParameterCheck @GetMapping("/{boardId}") public ApiResponse getBoardDetail(@PathVariable("boardId") Long boardId) { MapDto board = boardService.selectBoardDetail(boardId); if (board == null) { //throw new NotFoundHandler("게시물 ID " + boardId + "을(를) 찾을 수 없습니다."); String errMessage = "게시물 ID " + boardId + "을(를) 찾을 수 없습니다."; return ApiResponse.error(HttpStatus.NOT_FOUND, errMessage); } // 📌 첨부파일 목록 추가 List attachments = boardService.selectAttachments(boardId); board.put("attachments", attachments != null ? attachments : new ArrayList<>()); return ApiResponse.ok(board); } /** * 게시물 수정 조회(익명 게시글은 비밀번호 값 필수) * * @param boardId * @param map * @return */ @Member @ParameterCheck @PostMapping("/{boardId}") public ApiResponse getBoardDetail2(@PathVariable("boardId") Long boardId, @ReqMap MapDto map) { map.put("boardId", boardId); return boardService.selectBoardDetail2(map); } /** * 파일 다운로드 API * @param path 파일 경로 * @return 파일 데이터 (바이너리 응답) */ @GetMapping("/download") public ResponseEntity downloadFile(@RequestParam String path) { try { Path filePath = Paths.get(path).normalize(); Resource resource = new UrlResource(filePath.toUri()); if (!resource.exists() || !resource.isReadable()) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } String contentType = Files.probeContentType(filePath); String fileName = filePath.getFileName().toString(); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"") .header(HttpHeaders.CONTENT_TYPE, contentType != null ? contentType : "application/octet-stream") .body(resource); } catch (IOException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } /** * 게시물 삭제 * @ReqMap map 수정 데이터 (LOCBRDSEQ) * @return 삭제 결과 메시지 */ @Member @ParameterCheck @DeleteMapping("/{boardId}") public ApiResponse deleteBoard(@ReqMap MapDto map) { return boardService.deleteBoard(map); } /** * 게시물 수정 * * @param map 수정 데이터 (LOCBRDTTL, LOCBRDCON, LOCBRDSEQ, delListInfo) * @param files 첨부파일 정보 * @return * @throws IOException */ @Member @PutMapping("/{boardId}") public ApiResponse updateBoard(@ReqMap MapDto map, @RequestPart(value = "files", required = false) List files) throws IOException { return boardService.updateBoardWithFiles(map, files); } /** * 첨부파일 추가 * @ReqMap map 요청 파라미터 (CMNFLEREG, CMNFLESIZ, CMNFLEEXT, CMNFLEORG, CMNFLENAM, CMNFLEPAT, CMNBRDSEQ) * @return 첨부파일 저장 결과 메시지 */ @Member @ParameterCheck @PostMapping("/{CMNBRDSEQ}/attachments") public ApiResponse uploadAttachment(@ReqMap MapDto map, @RequestParam("file") MultipartFile file) { try { Long userId = AuthUtil.getUser().getId(); map.put("CMNFLEREG", userId); boardService.insertAttachment(map, file); return ApiResponse.ok("첨부파일이 저장되었습니다."); } catch (Exception e) { return ApiResponse.ok("첨부파일 저장 실패: " + e.getMessage()); } } /** * 게시물, 댓글 좋아요/싫어요 추가 * @ReqMap map 데이터 (LOCCMTSEQ, MEMBERSEQ, LOCGOBGOD, LOCGOBBAD, LOCBRDSEQ) * @return 반응 추가 결과 메시지 */ @Member @ParameterCheck @PostMapping("/{LOCBRDSEQ}/{LOCCMTSEQ}/reaction") public ApiResponse reactToBoard(@ReqMap MapDto map) { Long userId = AuthUtil.getUser().getId(); map.put("MEMBERSEQ", userId); boardService.procReactToBoard(map); return ApiResponse.ok("반응이 성공적으로 처리되었습니다."); } /** * 댓글 조회 * @ReqMap map 수정 데이터 (LOCBRDSEQ, page) * @return 댓글 */ @Member @ParameterCheck @GetMapping("/{boardId}/comments") public ApiResponse> selectComments(@ReqMap MapDto map) { // 댓글조회 PageInfo comments = boardService.selectComments(map); return ApiResponse.ok(comments); } /** * 대댓글 조회 * @ReqMap map 수정 데이터 (LOCCMTPNT) * @return 대댓글 */ @Member @ParameterCheck @GetMapping("/{boardId}/reply") public ApiResponse> getComments(@ReqMap MapDto map) { List replies = boardService.selectReply(map); return ApiResponse.ok(replies); } /** * 댓글/대댓글 작성 * @param boardId 게시물 ID * @ReqMap map 댓글 데이터 (LOCBRDSEQ, LOCCMTRPY, LOCCMTPNT, LOCCMTPWD, MEMBERSEQ ) * @return 작성 결과 메시지 */ @Member @ParameterCheck @PostMapping("/{LOCBRDSEQ}/comment") public ApiResponse addCommentOrReply(@ReqMap MapDto map) { if (map.containsKey("LOCCMTPWD") && !map.getString("LOCCMTPWD").trim().isEmpty()) { // 빈 값 체크 String rawPassword = map.getString("LOCCMTPWD"); String hashedPassword = passwordEncoder.encode(rawPassword); map.put("LOCCMTPWD", hashedPassword); } boardService.insertCommentOrReply(map); return ApiResponse.ok("댓글 또는 대댓글이 작성되었습니다."); } /** * 댓글/대댓글 수정 * @param commentId 댓글 ID * @ReqMap map 수정 데이터 (LOCCMTSEQ, LOCCMTRPY) * @return 수정 결과 메시지 */ @Member @ParameterCheck @PutMapping("/comment/{commentId}") public ApiResponse updateComment(@ReqMap MapDto map) { boardService.updateComment(map); return ApiResponse.ok("댓글이 수정되었습니다."); } /** * 댓글/대댓글 삭제 * @param commentId 댓글 ID * @return 삭제 결과 메시지 */ @Member @ParameterCheck @DeleteMapping("/comment/{commentId}") public ApiResponse deleteComment(@PathVariable("commentId") Long commentId, @RequestParam(value = "LOCCMTPNT") Long parentId) { MapDto map = new MapDto(); map.put("LOCCMTSEQ", commentId); map.put("LOCCMTPNT", parentId); boardService.deleteComment(map); return ApiResponse.ok("댓글이 삭제되었습니다."); } /** * 게시물 비밀번호 확인 (해싱된 비밀번호 비교 적용) * @ReqMap map 수정 데이터 (LOCBRDSEQ, LOCBRDPWD) * @return 비밀번호 확인 결과 */ @Member @ParameterCheck @PostMapping("/{boardId}/password") public ApiResponse checkBoardPassword(@ReqMap MapDto map) { // boardId 데이터 타입 변환 Object boardIdObj = map.get("LOCBRDSEQ"); int boardId = 0; if (boardIdObj instanceof Integer) { boardId = (Integer) boardIdObj; } else if (boardIdObj instanceof String) { boardId = Integer.parseInt((String) boardIdObj); } String rawPassword = map.getString("LOCBRDPWD"); String storedHashedPassword = boardService.selectBoardPassword(boardId); if (storedHashedPassword == null) { throw new NotFoundHandler("해당 게시물이 존재하지 않습니다."); } boolean isMatch = passwordEncoder.matches(rawPassword, storedHashedPassword); if (!isMatch) { //throw new InvalidPasswordException("비밀번호가 일치하지 않습니다."); return ApiResponse.error(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."); } return ApiResponse.ok(true); } /** * 댓글 비밀번호 확인 (해싱된 비밀번호 비교 적용) * @ReqMap map 수정 데이터 (LOCCMTSEQ, LOCCMTPWD) * @return 비밀번호 확인 결과 */ @Member @ParameterCheck @PostMapping("/comment/{commentId}/password") public ApiResponse checkCommentPassword(@ReqMap MapDto map) { // commentId 데이터 타입 변환 Object commentIdObj = map.get("LOCCMTSEQ"); int commentId = 0; if (commentIdObj instanceof Integer) { commentId = (Integer) commentIdObj; } else if (commentIdObj instanceof String) { commentId = Integer.parseInt((String) commentIdObj); } String rawPassword = map.getString("LOCCMTPWD"); String storedHashedPassword = boardService.selectCommentPassword(commentId); if (storedHashedPassword == null) { throw new NotFoundHandler("해당 댓글이 존재하지 않습니다."); } boolean isMatch = passwordEncoder.matches(rawPassword, storedHashedPassword); if (!isMatch) { //throw new InvalidPasswordException("비밀번호가 일치하지 않습니다."); return ApiResponse.error(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."); } return ApiResponse.ok(true); } /** * 카테고리 목록 조회 * @return 카테고리 리스트 */ @GetMapping("/categories") public ApiResponse> SelectCategories() { List categories = commoncodService.selectCategoryList(); return ApiResponse.ok(categories); } }