Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 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;
}
}
70 changes: 70 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,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;
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);
}

//수정기능
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;
}

}
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> {
}
65 changes: 65 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,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<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();
}

// 수정
@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);
}
}
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() 호출 검증
}

}