diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/bookmark/service/BookmarkService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/bookmark/service/BookmarkService.java index 83d85e7e..8716860f 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/bookmark/service/BookmarkService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/bookmark/service/BookmarkService.java @@ -3,12 +3,12 @@ import com.rollthedice.backend.domain.bookmark.entity.Bookmark; import com.rollthedice.backend.domain.bookmark.repository.BookmarkRepository; import com.rollthedice.backend.domain.member.entity.Member; +import com.rollthedice.backend.domain.news.exception.NewsNotFoundException; import com.rollthedice.backend.global.oauth2.service.AuthService; import com.rollthedice.backend.domain.news.dto.response.NewsResponse; import com.rollthedice.backend.domain.news.entity.News; import com.rollthedice.backend.domain.news.mapper.NewsMapper; import com.rollthedice.backend.domain.news.repository.NewsRepository; -import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -45,7 +45,7 @@ public void saveBookmark(Long newsId) { bookmarkRepository.save(Bookmark.builder() .member(member) .news(newsRepository.findById(newsId) - .orElseThrow(EntityNotFoundException::new)) + .orElseThrow(NewsNotFoundException::new)) .build()); } diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/debate/exception/DebateRoomNotFoundException.java b/backend/core/src/main/java/com/rollthedice/backend/domain/debate/exception/DebateRoomNotFoundException.java new file mode 100644 index 00000000..81af7671 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/debate/exception/DebateRoomNotFoundException.java @@ -0,0 +1,10 @@ +package com.rollthedice.backend.domain.debate.exception; + +import com.rollthedice.backend.global.error.exception.BusinessException; +import com.rollthedice.backend.global.error.ErrorCode; + +public class DebateRoomNotFoundException extends BusinessException { + public DebateRoomNotFoundException() { + super(ErrorCode.DEBATE_ROOM_NOT_FOUND_ERROR); + } +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/debate/service/DebateMessageService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/debate/service/DebateMessageService.java index 3649fb78..269bde39 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/debate/service/DebateMessageService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/debate/service/DebateMessageService.java @@ -3,10 +3,10 @@ import com.rollthedice.backend.domain.debate.dto.request.DebateMessageRequest; import com.rollthedice.backend.domain.debate.dto.response.DebateMessageResponse; import com.rollthedice.backend.domain.debate.entity.DebateRoom; +import com.rollthedice.backend.domain.debate.exception.DebateRoomNotFoundException; import com.rollthedice.backend.domain.debate.mapper.DebateMessageMapper; import com.rollthedice.backend.domain.debate.repository.DebateRoomRepository; import com.rollthedice.backend.domain.news.repository.DebateMessageRepository; -import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,7 +31,7 @@ public void saveAIDebateMessage(Long roomId, DebateMessageRequest request) { } private DebateRoom getDebateRoom(final Long roomId) { - return debateRoomRepository.findById(roomId).orElseThrow(EntityNotFoundException::new); + return debateRoomRepository.findById(roomId).orElseThrow(DebateRoomNotFoundException::new); } @Transactional diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/exception/MemberNotFoundException.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/exception/MemberNotFoundException.java new file mode 100644 index 00000000..22b9d723 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/exception/MemberNotFoundException.java @@ -0,0 +1,10 @@ +package com.rollthedice.backend.domain.member.exception; + +import com.rollthedice.backend.global.error.ErrorCode; +import com.rollthedice.backend.global.error.exception.BusinessException; + +public class MemberNotFoundException extends BusinessException { + public MemberNotFoundException() { + super(ErrorCode.MEMBER_NOT_FOUND_ERROR); + } +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/service/MemberService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/service/MemberService.java index 9e88af51..6b34086a 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/member/service/MemberService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/service/MemberService.java @@ -1,14 +1,13 @@ package com.rollthedice.backend.domain.member.service; import com.rollthedice.backend.domain.member.dto.MemberServiceDto; -import com.rollthedice.backend.domain.member.dto.SignUpDto; import com.rollthedice.backend.domain.member.dto.response.MemberResponse; import com.rollthedice.backend.domain.member.entity.Member; +import com.rollthedice.backend.domain.member.exception.MemberNotFoundException; import com.rollthedice.backend.domain.member.repository.MemberRepository; import com.rollthedice.backend.global.oauth2.service.AuthService; import com.rollthedice.backend.global.security.jwt.refresh.service.RefreshTokenService; import com.rollthedice.backend.global.security.jwt.service.JwtService; -import jakarta.persistence.EntityNotFoundException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -38,7 +37,7 @@ public void update(MemberServiceDto memberServiceDto) { @Transactional(readOnly = true) public Member findByEmail(String email) { - return memberRepository.findByEmail(email).orElseThrow(EntityNotFoundException::new); + return memberRepository.findByEmail(email).orElseThrow(MemberNotFoundException::new); } public MemberResponse getMemberInfo() { diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/news/api/NewsApi.java b/backend/core/src/main/java/com/rollthedice/backend/domain/news/api/NewsApi.java index c66a033d..edf03f94 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/news/api/NewsApi.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/news/api/NewsApi.java @@ -1,8 +1,12 @@ package com.rollthedice.backend.domain.news.api; +import com.rollthedice.backend.domain.news.dto.response.NewsDetailResponse; import com.rollthedice.backend.domain.news.dto.response.NewsResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import org.springframework.data.domain.Pageable; @@ -10,14 +14,36 @@ public interface NewsApi { @Operation( - summary = "요약 뉴스 조회", + summary = "요약 뉴스 전체 조회", description = "요약 뉴스를 페이지로 나누어 조회합니다.", security = {@SecurityRequirement(name = "access_token")}, tags = {"news"} ) @ApiResponse( responseCode = "200", - description = "OK" + description = "요청에 성공하였습니다." ) List getNews(Pageable pageable); + + @Operation( + summary = "요약 뉴스 상세 조회", + description = "하나의 요약 뉴스를 상세 조회합니다.", + security = {@SecurityRequirement(name = "access_token")}, + tags = {"news"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청에 성공하였습니다." + ), + @ApiResponse( + responseCode = "404", + description = "뉴스를 찾지 못했습니다." + ) + }) + NewsDetailResponse getDetailNews( + @Parameter(in = ParameterIn.PATH, description = "뉴스 ID", required = true) + Long newsId + ); + } diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/news/api/NewsController.java b/backend/core/src/main/java/com/rollthedice/backend/domain/news/api/NewsController.java index 83128e2f..e5e12daf 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/news/api/NewsController.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/news/api/NewsController.java @@ -1,5 +1,6 @@ package com.rollthedice.backend.domain.news.api; +import com.rollthedice.backend.domain.news.dto.response.NewsDetailResponse; import com.rollthedice.backend.domain.news.dto.response.NewsResponse; import com.rollthedice.backend.domain.news.service.NewsService; import lombok.RequiredArgsConstructor; @@ -21,4 +22,10 @@ public class NewsController implements NewsApi { public List getNews(final Pageable pageable) { return newsService.getNews(pageable); } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{newsId}") + public NewsDetailResponse getDetailNews(final @PathVariable Long newsId) { + return newsService.getDetailNews(newsId); + } } diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/news/dto/response/NewsDetailResponse.java b/backend/core/src/main/java/com/rollthedice/backend/domain/news/dto/response/NewsDetailResponse.java new file mode 100644 index 00000000..f4325632 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/news/dto/response/NewsDetailResponse.java @@ -0,0 +1,20 @@ +package com.rollthedice.backend.domain.news.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NewsDetailResponse { + private Long id; + private String url; + private String title; + private String content; + private String thumbnailUrl; + private String postDate; + private Boolean isBookmarked; +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/news/exception/NewsNotFoundException.java b/backend/core/src/main/java/com/rollthedice/backend/domain/news/exception/NewsNotFoundException.java new file mode 100644 index 00000000..e0953ca8 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/news/exception/NewsNotFoundException.java @@ -0,0 +1,12 @@ +package com.rollthedice.backend.domain.news.exception; + +import com.rollthedice.backend.global.error.exception.BusinessException; +import com.rollthedice.backend.global.error.ErrorCode; + +public class NewsNotFoundException extends BusinessException { + + public NewsNotFoundException() { + super(ErrorCode.NEWS_NOT_FOUND_ERROR); + } + +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/news/mapper/NewsMapper.java b/backend/core/src/main/java/com/rollthedice/backend/domain/news/mapper/NewsMapper.java index 1c29d5d9..501ebb2c 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/news/mapper/NewsMapper.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/news/mapper/NewsMapper.java @@ -1,5 +1,6 @@ package com.rollthedice.backend.domain.news.mapper; +import com.rollthedice.backend.domain.news.dto.response.NewsDetailResponse; import com.rollthedice.backend.domain.news.dto.response.NewsResponse; import com.rollthedice.backend.domain.news.entity.News; import org.mapstruct.Mapper; @@ -9,4 +10,6 @@ public interface NewsMapper { NewsResponse toResponse(final News news, boolean isBookmarked); + + NewsDetailResponse toDetailResponse(final News news, boolean isBookmarked); } diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/news/service/NewsService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/news/service/NewsService.java index 3ab16298..5d0efbf3 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/news/service/NewsService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/news/service/NewsService.java @@ -2,6 +2,8 @@ import com.rollthedice.backend.domain.bookmark.service.BookmarkService; import com.rollthedice.backend.domain.member.entity.Member; +import com.rollthedice.backend.domain.news.dto.response.NewsDetailResponse; +import com.rollthedice.backend.domain.news.exception.NewsNotFoundException; import com.rollthedice.backend.global.oauth2.service.AuthService; import com.rollthedice.backend.domain.news.contentqueue.ContentProducer; import com.rollthedice.backend.domain.news.dto.ContentMessageDto; @@ -10,7 +12,6 @@ import com.rollthedice.backend.domain.news.entity.News; import com.rollthedice.backend.domain.news.mapper.NewsMapper; import com.rollthedice.backend.domain.news.repository.NewsRepository; -import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; @@ -45,7 +46,7 @@ public List getNotCrawled() { @Transactional public void updateSummarizedNews(ContentMessageDto messageDto) { News news = newsRepository.findById(messageDto.getId()) - .orElseThrow(EntityNotFoundException::new); + .orElseThrow(NewsNotFoundException::new); news.updateSummarizedContent(messageDto.getContent()); } @@ -66,7 +67,9 @@ public List getNews(final Pageable pageable) { .collect(Collectors.toList()); } - public News getOneNews(Long newsId) { - return newsRepository.findById(newsId).orElseThrow(EntityNotFoundException::new); + public NewsDetailResponse getDetailNews(Long newsId) { + Member member = authService.getMember(); + final News news = newsRepository.findById(newsId).orElseThrow(NewsNotFoundException::new); + return newsMapper.toDetailResponse(news, bookmarkService.isBookmarked(member, news)); } } diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/error/ErrorCode.java b/backend/core/src/main/java/com/rollthedice/backend/global/error/ErrorCode.java new file mode 100644 index 00000000..93923466 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/error/ErrorCode.java @@ -0,0 +1,25 @@ +package com.rollthedice.backend.global.error; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버에 오류가 발생했습니다."), + CLOVA_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "CLOVA API 호출에 실패했습니다."), + + // MEMBER + MEMBER_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "회원 정보를 찾지 못했습니다." ), + + // NEWS + NEWS_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "뉴스를 찾지 못했습니다."), + + // DEBATE + DEBATE_ROOM_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "토론방을 찾지 못했습니다."); + + + private final HttpStatus status; + private final String message; +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/error/ErrorResponse.java b/backend/core/src/main/java/com/rollthedice/backend/global/error/ErrorResponse.java new file mode 100644 index 00000000..4d1cecbc --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/error/ErrorResponse.java @@ -0,0 +1,24 @@ +package com.rollthedice.backend.global.error; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.util.Collections; + +@Getter +public class ErrorResponse { + private HttpStatus status; + private String message; + + public ErrorResponse(HttpStatus status, String message) { + this.status = status; + this.message = message; + } + + public static ErrorResponse create(final ErrorCode errorCode) { + return new ErrorResponse( + errorCode.getStatus(), + errorCode.getMessage() + ); + } +} \ No newline at end of file diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/error/GlobalExceptionHandler.java b/backend/core/src/main/java/com/rollthedice/backend/global/error/GlobalExceptionHandler.java new file mode 100644 index 00000000..2f734a2f --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/error/GlobalExceptionHandler.java @@ -0,0 +1,24 @@ +package com.rollthedice.backend.global.error; + +import com.rollthedice.backend.global.error.exception.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + protected ResponseEntity handleRuntimeException(BusinessException e) { + final ErrorCode errorCode = e.getErrorCode(); + log.warn(e.getMessage()); + + return ResponseEntity + .status(errorCode.getStatus()) + .body(new ErrorResponse(errorCode.getStatus(), + errorCode.getMessage())); + } + +} \ No newline at end of file diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/error/exception/BusinessException.java b/backend/core/src/main/java/com/rollthedice/backend/global/error/exception/BusinessException.java new file mode 100644 index 00000000..f3df23c1 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/error/exception/BusinessException.java @@ -0,0 +1,14 @@ +package com.rollthedice.backend.global.error.exception; + +import com.rollthedice.backend.global.error.ErrorCode; +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException{ + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/error/exception/ExternalApiException.java b/backend/core/src/main/java/com/rollthedice/backend/global/error/exception/ExternalApiException.java new file mode 100644 index 00000000..6ea7082c --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/error/exception/ExternalApiException.java @@ -0,0 +1,17 @@ +package com.rollthedice.backend.global.error.exception; + +import com.rollthedice.backend.global.error.ErrorCode; +import lombok.Getter; + +import java.io.IOException; + +@Getter +public class ExternalApiException extends RuntimeException { + + private final ErrorCode errorCode; + + public ExternalApiException(ErrorCode errorCode) { + this.errorCode = errorCode; + } +} + diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/AuthService.java b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/AuthService.java index 3dd4beb4..649b8d58 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/AuthService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/AuthService.java @@ -3,12 +3,12 @@ import com.rollthedice.backend.domain.member.entity.Member; import com.rollthedice.backend.domain.member.entity.Role; import com.rollthedice.backend.domain.member.entity.SocialType; +import com.rollthedice.backend.domain.member.exception.MemberNotFoundException; import com.rollthedice.backend.domain.member.repository.MemberRepository; import com.rollthedice.backend.global.oauth2.dto.LoginRequest; import com.rollthedice.backend.global.oauth2.userInfo.OAuth2UserInfo; import com.rollthedice.backend.global.security.jwt.service.JwtService; import com.rollthedice.backend.global.query.QueryService; -import jakarta.persistence.EntityNotFoundException; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -54,7 +54,7 @@ public Member getMember() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - return memberRepository.findByEmail(userDetails.getUsername()).orElseThrow(EntityNotFoundException::new); + return memberRepository.findByEmail(userDetails.getUsername()).orElseThrow(MemberNotFoundException::new); } }