Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<List<NewsResponseDto>> getAllNews() {
List<NewsResponseDto> newsList = newsService.getAllNews();
return ResponseEntity.ok(newsList);
}

@Operation(summary = "소식 등록", description = "새로운 소식을 등록")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "생성 완료"),
@ApiResponse(responseCode = "400", description = "유효하지 않은 요청 데이터",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request Body -> NewsRequestDto 역직렬화 과정에서 필드 값 Validation을 하기 위해서는 NewsRequestDto 선언부 앞에 @Valid 어노테이션을 추가하고 NewsRequestDto의 각 필드에 제약 조건을 설정하면 됩니다. ApplicantCreateRequestDto를 참고해보세요

content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"errorCode\": \"E003\", \"errorMessage\": \"유효하지 않은 입력입니다.\" }")))
})

@PostMapping
public ResponseEntity<NewsResponseDto> createNews(@Valid @RequestBody NewsRequestDto requestDto) {
NewsResponseDto responseDto = newsService.createNews(requestDto);
return ResponseEntity.status(201).body(responseDto);
}

}
28 changes: 28 additions & 0 deletions src/main/java/dmu/dasom/api/domain/news/dto/NewsRequestDto.java
Original file line number Diff line number Diff line change
@@ -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;
}
34 changes: 34 additions & 0 deletions src/main/java/dmu/dasom/api/domain/news/dto/NewsResponseDto.java
Original file line number Diff line number Diff line change
@@ -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 final Long id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO를 불변 객체로 만들 필요는 없으므로 필드에 final 키워드는 빼는 것이 좋아보입니다


@Schema(description = "뉴스 제목", example = "제목")
private final String title;

@Schema(description = "뉴스 내용", example = "내용")
private final String content;

@Schema(description = "작성일", example = "2025-02-14T12:00:00")
private final LocalDateTime createdAt;

@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg", nullable = true)
private final 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;
}
}
48 changes: 48 additions & 0 deletions src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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.NotNull;
import lombok.*;

@Getter
@Setter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setter를 사용하는 곳이 없다면 어노테이션은 제거해주세요

@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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지는 외부 DB에 저장하는 건가요?


// 뉴스 상태 업데이트
public void updateStatus(Status status) {
super.updateStatus(status);
}

// NewsEntity → NewsResponseDto 변환
public NewsResponseDto toResponseDto() {
return new NewsResponseDto(id, title, content, getCreatedAt(), imageUrl);
}
}
Original file line number Diff line number Diff line change
@@ -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<NewsEntity, Long> {
}
46 changes: 46 additions & 0 deletions src/main/java/dmu/dasom/api/domain/news/service/NewsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 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<NewsResponseDto> 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();
}

}
114 changes: 114 additions & 0 deletions src/test/java/dmu/dasom/api/domain/news/NewsServiceTest.java
Original file line number Diff line number Diff line change
@@ -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<NewsResponseDto> 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() 호출 검증
}

}