diff --git a/src/main/java/dmu/dasom/api/domain/news/controller/NewsController.java b/src/main/java/dmu/dasom/api/domain/news/controller/NewsController.java new file mode 100644 index 0000000..9b1a598 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/news/controller/NewsController.java @@ -0,0 +1,52 @@ +package dmu.dasom.api.domain.news.controller; + +import dmu.dasom.api.domain.news.dto.NewsRequestDto; +import dmu.dasom.api.domain.news.dto.NewsResponseDto; +import dmu.dasom.api.domain.news.service.NewsService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; + +@Tag(name = "NEWS API", description = "다솜소식 API") +@RestController +@RequestMapping("/api/news") +public class NewsController { + + private final NewsService newsService; + + public NewsController(NewsService newsService) { + this.newsService = newsService; + } + + @Operation(summary = "소식 조회", description = "리스트로 조회") + @ApiResponse(responseCode = "200", description = "정상 응답", + content = @Content(mediaType = "application/json")) + @GetMapping + public ResponseEntity> getAllNews() { + List newsList = newsService.getAllNews(); + return ResponseEntity.ok(newsList); + } + + @Operation(summary = "소식 등록", description = "새로운 소식을 등록") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "생성 완료"), + @ApiResponse(responseCode = "400", description = "유효하지 않은 요청 데이터", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{ \"errorCode\": \"E003\", \"errorMessage\": \"유효하지 않은 입력입니다.\" }"))) + }) + + @PostMapping + public ResponseEntity createNews(@Valid @RequestBody NewsRequestDto requestDto) { + NewsResponseDto responseDto = newsService.createNews(requestDto); + return ResponseEntity.status(201).body(responseDto); + } + +} \ No newline at end of file diff --git a/src/main/java/dmu/dasom/api/domain/news/dto/NewsRequestDto.java b/src/main/java/dmu/dasom/api/domain/news/dto/NewsRequestDto.java new file mode 100644 index 0000000..a9eed7b --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/news/dto/NewsRequestDto.java @@ -0,0 +1,28 @@ +package dmu.dasom.api.domain.news.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "NewsRequestDto", description = "뉴스 생성 요청 DTO") +public class NewsRequestDto { + + @NotBlank + @Size(max = 100) + @Schema(description = "뉴스 제목", example = "새로운 뉴스 제목") + private String title; + + @NotBlank + @Schema(description = "뉴스 내용", example = "새로운 뉴스 내용") + private String content; + + @Size(max = 255) + @Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg", nullable = true) + private String imageUrl; +} \ No newline at end of file diff --git a/src/main/java/dmu/dasom/api/domain/news/dto/NewsResponseDto.java b/src/main/java/dmu/dasom/api/domain/news/dto/NewsResponseDto.java new file mode 100644 index 0000000..da4d3af --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/news/dto/NewsResponseDto.java @@ -0,0 +1,34 @@ +package dmu.dasom.api.domain.news.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Schema(name = "NewsResponseDto", description = "뉴스 응답 DTO") +public class NewsResponseDto { + + @Schema(description = "소식 ID", example = "1") + private Long id; + + @Schema(description = "뉴스 제목", example = "제목") + private String title; + + @Schema(description = "뉴스 내용", example = "내용") + private String content; + + @Schema(description = "작성일", example = "2025-02-14T12:00:00") + private LocalDateTime createdAt; + + @Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg", nullable = true) + private String imageUrl; + + public NewsResponseDto(Long id, String title, String content, LocalDateTime createdAt, String imageUrl) { + this.id = id; + this.title = title; + this.content = content; + this.createdAt = createdAt; + this.imageUrl = imageUrl; + } +} \ No newline at end of file diff --git a/src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java b/src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java new file mode 100644 index 0000000..f2bab7a --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java @@ -0,0 +1,70 @@ +package dmu.dasom.api.domain.news.entity; + +import dmu.dasom.api.domain.common.BaseEntity; +import dmu.dasom.api.domain.common.Status; +import dmu.dasom.api.domain.news.dto.NewsResponseDto; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.*; + +@Getter +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "뉴스 엔티티") +public class NewsEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "뉴스 ID", example = "1") + private Long id; + + @NotNull + @Schema(description = "뉴스 제목", example = "뉴스 예제 제목") + @Column(nullable = false, length = 100) + private String title; + + @NotNull + @Schema(description = "뉴스 내용", example = "뉴스 예제 내용") + @Column(columnDefinition = "TEXT", nullable = false) + private String content; + + @Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg") + @Column(length = 255) + private String imageUrl; + + // 뉴스 상태 업데이트 + public void updateStatus(Status status) { + super.updateStatus(status); + } + + // NewsEntity → NewsResponseDto 변환 + public NewsResponseDto toResponseDto() { + return new NewsResponseDto(id, title, content, getCreatedAt(), imageUrl); + } + + //수정기능 + public void update(String title, String content, String imageUrl) { + if (title == null || title.isBlank()) { + throw new IllegalArgumentException("제목은 필수입니다"); + } + if (title.length() > 100) { + throw new IllegalArgumentException("제목은 100자까지"); + } + if (content == null || content.isBlank()) { + throw new IllegalArgumentException("내용은 필수입니다"); + } + if (imageUrl != null && imageUrl.length() > 255) { + throw new IllegalArgumentException("이미지 URL은 255자까지"); + } + + this.title = title; + this.content = content; + this.imageUrl = imageUrl; + } + +} \ No newline at end of file diff --git a/src/main/java/dmu/dasom/api/domain/news/repository/NewsRepository.java b/src/main/java/dmu/dasom/api/domain/news/repository/NewsRepository.java new file mode 100644 index 0000000..641503e --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/news/repository/NewsRepository.java @@ -0,0 +1,9 @@ +package dmu.dasom.api.domain.news.repository; + +import dmu.dasom.api.domain.news.entity.NewsEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NewsRepository extends JpaRepository { +} diff --git a/src/main/java/dmu/dasom/api/domain/news/service/NewsService.java b/src/main/java/dmu/dasom/api/domain/news/service/NewsService.java new file mode 100644 index 0000000..f531df2 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/news/service/NewsService.java @@ -0,0 +1,65 @@ +package dmu.dasom.api.domain.news.service; + +import dmu.dasom.api.domain.news.dto.NewsRequestDto; +import dmu.dasom.api.domain.news.dto.NewsResponseDto; +import dmu.dasom.api.domain.news.entity.NewsEntity; +import dmu.dasom.api.domain.news.repository.NewsRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class NewsService { + + private final NewsRepository newsRepository; + + public NewsService(NewsRepository newsRepository) { + this.newsRepository = newsRepository; + } + + // 전체 조회 + public List getAllNews() { + return newsRepository.findAll().stream() + .map(NewsEntity::toResponseDto) + .collect(Collectors.toList()); + } + + // 개별 조회 + public NewsResponseDto getNewsById(Long id) { + return newsRepository.findById(id) + .map(NewsEntity::toResponseDto) + .orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id)); + } + + // 생성 + public NewsResponseDto createNews(NewsRequestDto requestDto) { + NewsEntity news = NewsEntity.builder() + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .imageUrl(requestDto.getImageUrl()) + .build(); + + NewsEntity savedNews = newsRepository.save(news); + return savedNews.toResponseDto(); + } + + // 수정 + @Transactional + public NewsResponseDto updateNews(Long id, NewsRequestDto requestDto) { + NewsEntity news = newsRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id)); + + news.update(requestDto.getTitle(), requestDto.getContent(), requestDto.getImageUrl()); + return news.toResponseDto(); + } + + // 삭제 + @Transactional + public void deleteNews(Long id) { + NewsEntity news = newsRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id)); + + newsRepository.delete(news); + } +} \ No newline at end of file diff --git a/src/test/java/dmu/dasom/api/domain/news/NewsServiceTest.java b/src/test/java/dmu/dasom/api/domain/news/NewsServiceTest.java new file mode 100644 index 0000000..166cbac --- /dev/null +++ b/src/test/java/dmu/dasom/api/domain/news/NewsServiceTest.java @@ -0,0 +1,114 @@ +package dmu.dasom.api.domain.news; + +import dmu.dasom.api.domain.news.dto.NewsRequestDto; +import dmu.dasom.api.domain.news.dto.NewsResponseDto; +import dmu.dasom.api.domain.news.entity.NewsEntity; +import dmu.dasom.api.domain.news.repository.NewsRepository; +import dmu.dasom.api.domain.news.service.NewsService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NewsServiceTest { + + @Mock + private NewsRepository newsRepository; + + @InjectMocks + private NewsService newsService; + + @Test + @DisplayName("뉴스 전체 조회 테스트") + void getAllNews() { + // Given + NewsEntity news1 = NewsEntity.builder().id(1L).title("뉴스1").content("내용1").imageUrl("url1").build(); + NewsEntity news2 = NewsEntity.builder().id(2L).title("뉴스2").content("내용2").imageUrl("url2").build(); + + when(newsRepository.findAll()).thenReturn(List.of(news1, news2)); + + // When + List newsList = newsService.getAllNews(); + + // Then + assertThat(newsList).hasSize(2); + assertThat(newsList.get(0).getTitle()).isEqualTo("뉴스1"); + verify(newsRepository, times(1)).findAll(); + } + + @Test + @DisplayName("뉴스 개별 조회 - 성공") + void getNewsById_Success() { + // Given + Long id = 1L; + NewsEntity news = NewsEntity.builder().id(id).title("뉴스1").content("내용1").imageUrl("url1").build(); + + when(newsRepository.findById(id)).thenReturn(Optional.of(news)); + + // When + NewsResponseDto responseDto = newsService.getNewsById(id); + + // Then + assertThat(responseDto.getId()).isEqualTo(id); + assertThat(responseDto.getTitle()).isEqualTo("뉴스1"); + verify(newsRepository, times(1)).findById(id); + } + + @Test + @DisplayName("뉴스 개별 조회 - 실패 (존재하지 않는 ID)") + void getNewsById_NotFound() { + // Given + Long id = 999L; + + when(newsRepository.findById(id)).thenReturn(Optional.empty()); + + // When & Then + Exception exception = assertThrows(IllegalArgumentException.class, () -> newsService.getNewsById(id)); + assertThat(exception.getMessage()).isEqualTo("해당 뉴스가 존재하지 않습니다. ID: " + id); + verify(newsRepository, times(1)).findById(id); + } + + @Test + @DisplayName("뉴스 생성 테스트") + void createNews() { + // Given + NewsRequestDto requestDto = new NewsRequestDto("새 뉴스", "새 내용", "새 이미지"); + + NewsEntity news = NewsEntity.builder() + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .imageUrl(requestDto.getImageUrl()) + .build(); + + NewsEntity savedNews = NewsEntity.builder() + .id(1L) + .title(news.getTitle()) + .content(news.getContent()) + .imageUrl(news.getImageUrl()) + .build(); + + when(newsRepository.save(any(NewsEntity.class))).thenReturn(savedNews); + + // When + NewsResponseDto responseDto = newsService.createNews(requestDto); + + // Then + assertThat(responseDto.getId()).isEqualTo(1L); // 저장된 뉴스의 ID 할당확인 + assertThat(responseDto.getTitle()).isEqualTo("새 뉴스"); + assertThat(responseDto.getContent()).isEqualTo("새 내용"); + assertThat(responseDto.getImageUrl()).isEqualTo("새 이미지"); + + verify(newsRepository, times(1)).save(any(NewsEntity.class)); // save() 호출 검증 + } + +}