472 lines
17 KiB
Java
472 lines
17 KiB
Java
/************************************************************
|
|
*
|
|
* @packageName : io.company.localhost.service
|
|
* @fileName : localbordService.java
|
|
* @author : 서지희
|
|
* @date : 25.01.07
|
|
* @description :
|
|
*
|
|
* ===========================================================
|
|
* DATE AUTHOR NOTE
|
|
* -----------------------------------------------------------
|
|
* 25.01.07 서지희 최초 생성
|
|
*
|
|
*************************************************************/
|
|
package io.company.localhost.service;
|
|
|
|
import java.io.IOException;
|
|
import java.math.BigInteger;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.github.pagehelper.PageHelper;
|
|
import com.github.pagehelper.PageInfo;
|
|
|
|
import io.company.localhost.common.dto.ApiResponse;
|
|
import io.company.localhost.common.dto.MapDto;
|
|
import io.company.localhost.mapper.localbordMapper;
|
|
import io.company.localhost.utils.AuthUtil;
|
|
import io.company.localhost.utils.BlobUtil;
|
|
import io.company.localhost.utils.PageUtil;
|
|
import io.company.localhost.vo.MemberVo;
|
|
import io.company.localhost.vo.UploadFile;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class localbordService {
|
|
private final localbordMapper boardMapper;
|
|
private final FileService fileService;
|
|
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
private final PasswordEncoder passwordEncoder;
|
|
|
|
public List<MapDto> selectNotices(MapDto map) {
|
|
List<MapDto> posts = boardMapper.selectNotices(map);
|
|
enrichPostsWithAdditionalData(posts);
|
|
return posts;
|
|
}
|
|
|
|
public PageInfo<MapDto> selectGeneralPosts(MapDto map) {
|
|
System.out.println(map);
|
|
int page = map.getString("page") != null ? Integer.parseInt(map.getString("page")) : 1;
|
|
int size = map.getString("size") != null ? Integer.parseInt(map.getString("size")) : 10;
|
|
|
|
String orderBy = map.getString("orderBy");
|
|
if (orderBy == null || (!orderBy.equals("date") && !orderBy.equals("views"))) {
|
|
map.put("orderBy", "date");
|
|
}
|
|
|
|
PageHelper.startPage(page, size);
|
|
|
|
List<MapDto> result = boardMapper.selectGeneralPosts(map);
|
|
enrichPostsWithAdditionalData(result);
|
|
|
|
return PageUtil.redefineNavigation(new PageInfo<>(result, size));
|
|
}
|
|
|
|
public void updateIncrementViewCount(Long boardId) {
|
|
boardMapper.updateIncrementViewCount(boardId);
|
|
}
|
|
|
|
public BigInteger insertBoard(MapDto map) {
|
|
// 익명게시판이면 회원 정보를 null로 설정
|
|
if ("300102".equals(String.valueOf(map.get("LOCBRDTYP")))) {
|
|
map.put("MEMBERSEQ", null);
|
|
}else {
|
|
Long userId = AuthUtil.getUser().getId();
|
|
map.put("MEMBERSEQ", userId);
|
|
}
|
|
boardMapper.insertBoard(map);
|
|
return (BigInteger) map.get("LOCBRDSEQ");
|
|
}
|
|
|
|
public void insertAttachment(MapDto map, MultipartFile file) {
|
|
String boardSeqStr = (String) map.get("CMNBRDSEQ");
|
|
Long boardSeq = Long.parseLong(boardSeqStr);
|
|
map.put("CMNBRDSEQ", boardSeq);
|
|
|
|
String newFilename = UUID.randomUUID().toString();
|
|
map.put("CMNFLENAM", newFilename);
|
|
String Path = fileService.boardUploadFile(file);
|
|
map.put("CMNFLEPAT", Path);
|
|
boardMapper.insertAttachment(map);
|
|
}
|
|
|
|
public void validateAttachmentsSize(List<MapDto> attachments) {
|
|
long totalSize = attachments.stream()
|
|
.mapToLong(attachment -> (Long) attachment.get("size"))
|
|
.sum();
|
|
|
|
if (totalSize > MAX_FILE_SIZE) {
|
|
throw new IllegalArgumentException("첨부파일의 총 용량이 5MB를 초과합니다.");
|
|
}
|
|
}
|
|
|
|
public MapDto selectBoardDetail(Long boardId) {
|
|
updateIncrementViewCount(boardId);
|
|
MapDto boardDetail = boardMapper.selectBoardDetail(boardId);
|
|
enrichBoardDetail(boardDetail);
|
|
|
|
return boardDetail;
|
|
}
|
|
|
|
public List<MapDto> selectAttachments(Long boardId) {
|
|
return boardMapper.selectAttachments(boardId);
|
|
}
|
|
|
|
public void deleteBoard(MapDto map) {
|
|
boardMapper.deleteCommentsByBoardId(map);
|
|
boardMapper.deleteBoard(map);
|
|
}
|
|
|
|
public int updateBoard(MapDto map) {
|
|
return boardMapper.updateBoard(map);
|
|
}
|
|
|
|
public void procReactToBoard(MapDto map) {
|
|
MapDto existingReaction = boardMapper.selectReaction(map);
|
|
|
|
if (existingReaction != null) {
|
|
boardMapper.updateReaction(map);
|
|
} else {
|
|
boardMapper.insertReaction(map);
|
|
}
|
|
}
|
|
|
|
public PageInfo<MapDto> selectComments(MapDto map) {
|
|
int page = map.getString("page") != null ? Integer.parseInt(map.getString("page")) : 1;
|
|
int size = map.getString("size") != null ? Integer.parseInt(map.getString("size")) : 10;
|
|
PageHelper.startPage(page, size);
|
|
|
|
List<MapDto> result = boardMapper.selectComments(map);
|
|
enrichCommentsWithAdditionalData(result); // 댓글 데이터 보강
|
|
return PageUtil.redefineNavigation(new PageInfo<>(result, size));
|
|
}
|
|
|
|
public List<MapDto> selectReply(MapDto map) {
|
|
return boardMapper.selectReply(map);
|
|
}
|
|
|
|
public void insertCommentOrReply(MapDto map) {
|
|
|
|
// 익명게시판이면 회원 정보를 null로 설정
|
|
if ("300102".equals(String.valueOf(map.get("LOCBRDTYP")))) {
|
|
map.put("MEMBERSEQ", null);
|
|
}else {
|
|
Long userId = AuthUtil.getUser().getId();
|
|
map.put("MEMBERSEQ", userId);
|
|
}
|
|
if (map.get("LOCCMTPNT") == null) {
|
|
map.put("LOCCMTPNT", null);
|
|
}
|
|
boardMapper.insertCommentOrReply(map);
|
|
|
|
}
|
|
|
|
public void updateComment(MapDto map) {
|
|
boardMapper.updateComment(map);
|
|
}
|
|
|
|
public void deleteComment(MapDto map) {
|
|
Long commentId = (Long) map.get("LOCCMTSEQ");
|
|
// 댓글이 대댓글이 있는지 확인
|
|
boolean hasReplies = boardMapper.selectReplyCount(commentId) > 0;
|
|
if (hasReplies) {
|
|
// 대댓글이 있는 경우, '삭제된 댓글입니다.'로 변경 (소프트 삭제)
|
|
boardMapper.updateSoftDeleteComment(commentId);
|
|
} else {
|
|
// 대댓글이 없는 경우, 완전 삭제
|
|
boardMapper.deleteComment(commentId);
|
|
}
|
|
checkAndDeleteParentComment(map);
|
|
}
|
|
|
|
private void checkAndDeleteParentComment(MapDto map) {
|
|
Long parentId = (Long) map.get("LOCCMTPNT");
|
|
if (parentId == null) return; // 부모가 없으면 종료
|
|
|
|
// 부모 댓글의 남아있는 대댓글 개수 확인
|
|
int remainingReplies = boardMapper.selectReplyCount(parentId);
|
|
|
|
if (remainingReplies == 0) {
|
|
// 남은 대댓글이 없으면 부모 댓글도 삭제
|
|
boardMapper.deleteComment(parentId);
|
|
}
|
|
}
|
|
|
|
public String selectCommentPassword(int commentId) {
|
|
return boardMapper.selectCommentPassword(commentId);
|
|
}
|
|
|
|
public String selectBoardPassword(int boardId) {
|
|
return boardMapper.selectBoardPassword(boardId);
|
|
}
|
|
|
|
public MapDto selectCommentById(int commentId) {
|
|
return boardMapper.selectCommentById(commentId);
|
|
}
|
|
|
|
public int selectCountComments(Long boardId) {
|
|
return boardMapper.selectCountComments(boardId);
|
|
}
|
|
|
|
public boolean selectIsAttachments(Long boardId) {
|
|
int count = boardMapper.selectIsAttachments(boardId);
|
|
return count > 0;
|
|
}
|
|
|
|
public MapDto selectCountBoardReactions(Long boardId) {
|
|
return boardMapper.selectCountBoardReactions(boardId);
|
|
}
|
|
|
|
public MapDto selectCountCommentReactions(Long boardId) {
|
|
return boardMapper.selectCountCommentReactions(boardId);
|
|
}
|
|
|
|
public static String procFirstImageUrl(String jsonContent) {
|
|
try {
|
|
ObjectMapper objectMapper = new ObjectMapper();
|
|
JsonNode rootNode = objectMapper.readTree(jsonContent);
|
|
|
|
// "ops" 배열 가져오기
|
|
JsonNode opsNode = rootNode.get("ops");
|
|
if (opsNode != null && opsNode.isArray()) {
|
|
for (JsonNode node : opsNode) {
|
|
JsonNode insertNode = node.get("insert");
|
|
if (insertNode != null && insertNode.has("image")) {
|
|
return insertNode.get("image").asText(); // 첫 번째 이미지 URL 반환
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
throw new RuntimeException("Failed to extract first image URL: " + e.getMessage(), e);
|
|
}
|
|
return null; // 이미지가 없는 경우
|
|
}
|
|
|
|
private boolean procIsValidJson(String json) {
|
|
try {
|
|
final ObjectMapper objectMapper = new ObjectMapper();
|
|
objectMapper.readTree(json); // JSON 파싱 시도
|
|
return true; // JSON이 유효하면 true 반환
|
|
} catch (Exception e) {
|
|
return false; // 유효하지 않은 경우 false 반환
|
|
}
|
|
}
|
|
|
|
private String procPlainTextFromJson(String jsonContent) {
|
|
StringBuilder plainTextBuilder = new StringBuilder();
|
|
try {
|
|
ObjectMapper objectMapper = new ObjectMapper();
|
|
JsonNode rootNode = objectMapper.readTree(jsonContent);
|
|
|
|
// JSON 배열인지 확인
|
|
if (!rootNode.isArray()) {
|
|
System.err.println("JSON content is not an array");
|
|
return "";
|
|
}
|
|
|
|
// JSON 노드 순회
|
|
for (JsonNode node : rootNode) {
|
|
JsonNode insertNode = node.get("insert");
|
|
|
|
// insert 노드가 텍스트인지 확인
|
|
if (insertNode != null && insertNode.isTextual()) {
|
|
String text = insertNode.asText();
|
|
|
|
// '\n' 제거하고 순수 텍스트만 추가
|
|
if (!text.trim().isEmpty() && !text.trim().equals("\n")) {
|
|
plainTextBuilder.append(text.trim());
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
throw new RuntimeException("Failed to extract plain text: " + e.getMessage(), e);
|
|
}
|
|
return plainTextBuilder.toString();
|
|
}
|
|
|
|
private void enrichBoardDetail(MapDto boardDetail) {
|
|
if(boardDetail == null) return;
|
|
|
|
long boardId = ((Number) boardDetail.get("id")).longValue();
|
|
boardDetail.put("hasAttachment", selectIsAttachments(boardId));
|
|
boardDetail.put("commentCount", selectCountComments(boardId));
|
|
MapDto reactions = selectCountBoardReactions(boardId);
|
|
boardDetail.put("likeCount", reactions.getOrDefault("likeCount", 0));
|
|
boardDetail.put("dislikeCount", reactions.getOrDefault("dislikeCount", 0));
|
|
|
|
// Blob 데이터를 문자열로 변환
|
|
Object content = boardDetail.get("content");
|
|
if (content != null) {
|
|
String contentString = BlobUtil.procBlobToString(content); // Blob을 문자열로 변환
|
|
boardDetail.put("content", contentString); // JSON 변환 가능
|
|
}
|
|
}
|
|
|
|
private void enrichPostsWithAdditionalData(List<MapDto> posts) {
|
|
for (MapDto post : posts) {
|
|
Object idObject = post.get("id");
|
|
if (idObject instanceof Number) {
|
|
long postId = ((Number) idObject).longValue();
|
|
post.put("commentCount", selectCountComments(postId));
|
|
post.put("hasAttachment", selectAttachments(postId));
|
|
MapDto reactions = selectCountBoardReactions(postId);
|
|
post.put("likeCount", reactions.getOrDefault("likeCount", 0));
|
|
post.put("dislikeCount", reactions.getOrDefault("dislikeCount", 0));
|
|
|
|
Object content = post.get("content");
|
|
String contentString = BlobUtil.procBlobToString(content);
|
|
post.put("content", contentString);
|
|
|
|
String firstImageUrl = procFirstImageUrl(contentString);
|
|
post.put("firstImageUrl", firstImageUrl);
|
|
|
|
String plainContent = procPlainTextFromJson(contentString);
|
|
post.put("plainContent", plainContent);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void enrichCommentsWithAdditionalData(List<MapDto> comments) {
|
|
for (MapDto comment : comments) {
|
|
Object idObject = comment.get("LOCCMTSEQ");
|
|
String userId = "";
|
|
|
|
// 프로필 이미지 추가
|
|
if(comment.containsKey("authorId")) {
|
|
userId = String.valueOf(comment.get("authorId"));
|
|
String profileImg = boardMapper.selectUserProfileImg(userId);
|
|
comment.put("profileImg", profileImg);
|
|
}
|
|
|
|
|
|
if (idObject instanceof Number) {
|
|
long commentId = ((Number) idObject).longValue();
|
|
|
|
MapDto reactions = boardMapper.selectCountCommentReactions(commentId);
|
|
comment.put("likeCount", reactions != null ? reactions.getOrDefault("likeCount", 0) : 0);
|
|
comment.put("dislikeCount", reactions != null ? reactions.getOrDefault("dislikeCount", 0) : 0);
|
|
|
|
Object content = comment.get("content");
|
|
comment.put("content", content);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 수정
|
|
*
|
|
* @param map
|
|
* @param files
|
|
* @throws IOException
|
|
*/
|
|
@Transactional
|
|
public ApiResponse<String> updateBoardWithFiles(MapDto map, List<MultipartFile> files) throws IOException {
|
|
int result = this.updateBoard(map); // 게시글 수정
|
|
Long userId = AuthUtil.getUser().getId();
|
|
|
|
if(result == 1) {
|
|
if(files != null && !files.isEmpty()) {
|
|
List<UploadFile> list = fileService.boardUploadFiles(files); // 파일 업로드
|
|
map.put("CMNFLEREG", userId);
|
|
map.put("list", list);
|
|
boardMapper.insertAttachments(map);
|
|
}
|
|
|
|
if(map.get("delFileIdx") != null) {
|
|
String[] array = String.valueOf(map.get("delFileIdx")).split(",");
|
|
List<String> delListInfo = this.selectDelFileInfo(array); // 삭제 정보 조회
|
|
for(String item : delListInfo) {
|
|
fileService.removeFile(item); // 파일 삭제
|
|
}
|
|
this.deleteFileInfo(array); // db 데이터 삭제
|
|
}
|
|
} else {
|
|
return ApiResponse.error(HttpStatus.INTERNAL_SERVER_ERROR, "게시물이 수정에 실패하였습니다.");
|
|
}
|
|
|
|
return ApiResponse.ok("게시물이 수정되었습니다.");
|
|
}
|
|
|
|
/**
|
|
* 첨부파일 데이터 삭제
|
|
*
|
|
* @param array
|
|
*/
|
|
private void deleteFileInfo(String[] array) {
|
|
boardMapper.deleteFileInfo(array);
|
|
}
|
|
|
|
/**
|
|
* 삭제 첨부파일 정보 조회
|
|
*
|
|
* @param array
|
|
* @return
|
|
*/
|
|
private List<String> selectDelFileInfo(String[] array) {
|
|
return boardMapper.selectDelFileInfo(array);
|
|
}
|
|
|
|
/**
|
|
* 게시글 수정 조회
|
|
*
|
|
* @param map
|
|
* @return
|
|
*/
|
|
public ApiResponse<MapDto> selectBoardDetail2(MapDto map) {
|
|
String password = map.getString("password"); // 입력한 비밀번호
|
|
Long boardId = map.getLong("boardId"); // 조회한 게시글 번호
|
|
MemberVo member = AuthUtil.getUser(); // 로그인 정보 조회
|
|
MapDto resultMap = boardMapper.selectBoardDetail2(boardId); // 게시글 정보 조회
|
|
if(resultMap == null) return ApiResponse.error(HttpStatus.NOT_FOUND, "해당 게시글이 없습니다");
|
|
String boardType = resultMap.getString("type"); // 게시글 타입
|
|
|
|
// 익명 게시글
|
|
if("300102".equals(boardType)) {
|
|
String hashedPassword = resultMap.getString("password");
|
|
|
|
if(StringUtils.hasText(password)) {
|
|
boolean isMatch = passwordEncoder.matches(password, hashedPassword);
|
|
if(!isMatch) return ApiResponse.error(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다");
|
|
} else {
|
|
return ApiResponse.error(HttpStatus.UNAUTHORIZED, "비밀번호를 입력하세요");
|
|
}
|
|
|
|
// 자유 게시글
|
|
} else if("300101".equals(boardType)) {
|
|
Long writerId = Long.valueOf(resultMap.getInt("authorId"));
|
|
if(member.getId() != writerId) return ApiResponse.error(HttpStatus.UNAUTHORIZED, "권한이 없습니다");
|
|
|
|
// 공지글
|
|
} else if("300103".equals(boardType)) {
|
|
if(!"ROLE_ADMIN".equals(member.getRole())) return ApiResponse.error(HttpStatus.UNAUTHORIZED, "권한이 없습니다");
|
|
|
|
} else {
|
|
log.error("게시글 카테고리 정보 없음");
|
|
return ApiResponse.error(HttpStatus.NOT_FOUND, "해당 게시글이 없습니다");
|
|
}
|
|
|
|
this.enrichBoardDetail(resultMap); // 추가정보
|
|
// 📌 첨부파일 목록 추가
|
|
List<MapDto> attachments = this.selectAttachments(boardId);
|
|
resultMap.put("attachments", attachments != null ? attachments : new ArrayList<>());
|
|
|
|
return ApiResponse.ok(resultMap);
|
|
}
|
|
|
|
} |