Skip to content

Commit 2310796

Browse files
authored
[feat] 다솜소식 API 구현
* feat: 다솜소식 API 구현
1 parent b0c1f93 commit 2310796

File tree

7 files changed

+372
-0
lines changed

7 files changed

+372
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package dmu.dasom.api.domain.news.controller;
2+
3+
import dmu.dasom.api.domain.news.dto.NewsRequestDto;
4+
import dmu.dasom.api.domain.news.dto.NewsResponseDto;
5+
import dmu.dasom.api.domain.news.service.NewsService;
6+
import io.swagger.v3.oas.annotations.tags.Tag;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
9+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
10+
import io.swagger.v3.oas.annotations.media.Content;
11+
import io.swagger.v3.oas.annotations.media.ExampleObject;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.web.bind.annotation.*;
14+
15+
import jakarta.validation.Valid;
16+
import java.util.List;
17+
18+
@Tag(name = "NEWS API", description = "다솜소식 API")
19+
@RestController
20+
@RequestMapping("/api/news")
21+
public class NewsController {
22+
23+
private final NewsService newsService;
24+
25+
public NewsController(NewsService newsService) {
26+
this.newsService = newsService;
27+
}
28+
29+
@Operation(summary = "소식 조회", description = "리스트로 조회")
30+
@ApiResponse(responseCode = "200", description = "정상 응답",
31+
content = @Content(mediaType = "application/json"))
32+
@GetMapping
33+
public ResponseEntity<List<NewsResponseDto>> getAllNews() {
34+
List<NewsResponseDto> newsList = newsService.getAllNews();
35+
return ResponseEntity.ok(newsList);
36+
}
37+
38+
@Operation(summary = "소식 등록", description = "새로운 소식을 등록")
39+
@ApiResponses(value = {
40+
@ApiResponse(responseCode = "201", description = "생성 완료"),
41+
@ApiResponse(responseCode = "400", description = "유효하지 않은 요청 데이터",
42+
content = @Content(mediaType = "application/json",
43+
examples = @ExampleObject(value = "{ \"errorCode\": \"E003\", \"errorMessage\": \"유효하지 않은 입력입니다.\" }")))
44+
})
45+
46+
@PostMapping
47+
public ResponseEntity<NewsResponseDto> createNews(@Valid @RequestBody NewsRequestDto requestDto) {
48+
NewsResponseDto responseDto = newsService.createNews(requestDto);
49+
return ResponseEntity.status(201).body(responseDto);
50+
}
51+
52+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dmu.dasom.api.domain.news.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Size;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
@Schema(name = "NewsRequestDto", description = "뉴스 생성 요청 DTO")
14+
public class NewsRequestDto {
15+
16+
@NotBlank
17+
@Size(max = 100)
18+
@Schema(description = "뉴스 제목", example = "새로운 뉴스 제목")
19+
private String title;
20+
21+
@NotBlank
22+
@Schema(description = "뉴스 내용", example = "새로운 뉴스 내용")
23+
private String content;
24+
25+
@Size(max = 255)
26+
@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg", nullable = true)
27+
private String imageUrl;
28+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package dmu.dasom.api.domain.news.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.Getter;
5+
6+
import java.time.LocalDateTime;
7+
8+
@Getter
9+
@Schema(name = "NewsResponseDto", description = "뉴스 응답 DTO")
10+
public class NewsResponseDto {
11+
12+
@Schema(description = "소식 ID", example = "1")
13+
private Long id;
14+
15+
@Schema(description = "뉴스 제목", example = "제목")
16+
private String title;
17+
18+
@Schema(description = "뉴스 내용", example = "내용")
19+
private String content;
20+
21+
@Schema(description = "작성일", example = "2025-02-14T12:00:00")
22+
private LocalDateTime createdAt;
23+
24+
@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg", nullable = true)
25+
private String imageUrl;
26+
27+
public NewsResponseDto(Long id, String title, String content, LocalDateTime createdAt, String imageUrl) {
28+
this.id = id;
29+
this.title = title;
30+
this.content = content;
31+
this.createdAt = createdAt;
32+
this.imageUrl = imageUrl;
33+
}
34+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package dmu.dasom.api.domain.news.entity;
2+
3+
import dmu.dasom.api.domain.common.BaseEntity;
4+
import dmu.dasom.api.domain.common.Status;
5+
import dmu.dasom.api.domain.news.dto.NewsResponseDto;
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
import jakarta.persistence.*;
8+
import jakarta.validation.constraints.NotBlank;
9+
import jakarta.validation.constraints.NotNull;
10+
import jakarta.validation.constraints.Size;
11+
import lombok.*;
12+
13+
@Getter
14+
@Entity
15+
@NoArgsConstructor
16+
@AllArgsConstructor
17+
@Builder
18+
@Schema(description = "뉴스 엔티티")
19+
public class NewsEntity extends BaseEntity {
20+
21+
@Id
22+
@GeneratedValue(strategy = GenerationType.IDENTITY)
23+
@Schema(description = "뉴스 ID", example = "1")
24+
private Long id;
25+
26+
@NotNull
27+
@Schema(description = "뉴스 제목", example = "뉴스 예제 제목")
28+
@Column(nullable = false, length = 100)
29+
private String title;
30+
31+
@NotNull
32+
@Schema(description = "뉴스 내용", example = "뉴스 예제 내용")
33+
@Column(columnDefinition = "TEXT", nullable = false)
34+
private String content;
35+
36+
@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg")
37+
@Column(length = 255)
38+
private String imageUrl;
39+
40+
// 뉴스 상태 업데이트
41+
public void updateStatus(Status status) {
42+
super.updateStatus(status);
43+
}
44+
45+
// NewsEntity → NewsResponseDto 변환
46+
public NewsResponseDto toResponseDto() {
47+
return new NewsResponseDto(id, title, content, getCreatedAt(), imageUrl);
48+
}
49+
50+
//수정기능
51+
public void update(String title, String content, String imageUrl) {
52+
if (title == null || title.isBlank()) {
53+
throw new IllegalArgumentException("제목은 필수입니다");
54+
}
55+
if (title.length() > 100) {
56+
throw new IllegalArgumentException("제목은 100자까지");
57+
}
58+
if (content == null || content.isBlank()) {
59+
throw new IllegalArgumentException("내용은 필수입니다");
60+
}
61+
if (imageUrl != null && imageUrl.length() > 255) {
62+
throw new IllegalArgumentException("이미지 URL은 255자까지");
63+
}
64+
65+
this.title = title;
66+
this.content = content;
67+
this.imageUrl = imageUrl;
68+
}
69+
70+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package dmu.dasom.api.domain.news.repository;
2+
3+
import dmu.dasom.api.domain.news.entity.NewsEntity;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
@Repository
8+
public interface NewsRepository extends JpaRepository<NewsEntity, Long> {
9+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package dmu.dasom.api.domain.news.service;
2+
3+
import dmu.dasom.api.domain.news.dto.NewsRequestDto;
4+
import dmu.dasom.api.domain.news.dto.NewsResponseDto;
5+
import dmu.dasom.api.domain.news.entity.NewsEntity;
6+
import dmu.dasom.api.domain.news.repository.NewsRepository;
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
9+
import java.util.List;
10+
import java.util.stream.Collectors;
11+
12+
@Service
13+
public class NewsService {
14+
15+
private final NewsRepository newsRepository;
16+
17+
public NewsService(NewsRepository newsRepository) {
18+
this.newsRepository = newsRepository;
19+
}
20+
21+
// 전체 조회
22+
public List<NewsResponseDto> getAllNews() {
23+
return newsRepository.findAll().stream()
24+
.map(NewsEntity::toResponseDto)
25+
.collect(Collectors.toList());
26+
}
27+
28+
// 개별 조회
29+
public NewsResponseDto getNewsById(Long id) {
30+
return newsRepository.findById(id)
31+
.map(NewsEntity::toResponseDto)
32+
.orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id));
33+
}
34+
35+
// 생성
36+
public NewsResponseDto createNews(NewsRequestDto requestDto) {
37+
NewsEntity news = NewsEntity.builder()
38+
.title(requestDto.getTitle())
39+
.content(requestDto.getContent())
40+
.imageUrl(requestDto.getImageUrl())
41+
.build();
42+
43+
NewsEntity savedNews = newsRepository.save(news);
44+
return savedNews.toResponseDto();
45+
}
46+
47+
// 수정
48+
@Transactional
49+
public NewsResponseDto updateNews(Long id, NewsRequestDto requestDto) {
50+
NewsEntity news = newsRepository.findById(id)
51+
.orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id));
52+
53+
news.update(requestDto.getTitle(), requestDto.getContent(), requestDto.getImageUrl());
54+
return news.toResponseDto();
55+
}
56+
57+
// 삭제
58+
@Transactional
59+
public void deleteNews(Long id) {
60+
NewsEntity news = newsRepository.findById(id)
61+
.orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id));
62+
63+
newsRepository.delete(news);
64+
}
65+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package dmu.dasom.api.domain.news;
2+
3+
import dmu.dasom.api.domain.news.dto.NewsRequestDto;
4+
import dmu.dasom.api.domain.news.dto.NewsResponseDto;
5+
import dmu.dasom.api.domain.news.entity.NewsEntity;
6+
import dmu.dasom.api.domain.news.repository.NewsRepository;
7+
import dmu.dasom.api.domain.news.service.NewsService;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.ExtendWith;
11+
import org.mockito.InjectMocks;
12+
import org.mockito.Mock;
13+
import org.mockito.junit.jupiter.MockitoExtension;
14+
15+
import java.util.List;
16+
import java.util.Optional;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.junit.jupiter.api.Assertions.assertThrows;
20+
import static org.mockito.Mockito.*;
21+
22+
@ExtendWith(MockitoExtension.class)
23+
class NewsServiceTest {
24+
25+
@Mock
26+
private NewsRepository newsRepository;
27+
28+
@InjectMocks
29+
private NewsService newsService;
30+
31+
@Test
32+
@DisplayName("뉴스 전체 조회 테스트")
33+
void getAllNews() {
34+
// Given
35+
NewsEntity news1 = NewsEntity.builder().id(1L).title("뉴스1").content("내용1").imageUrl("url1").build();
36+
NewsEntity news2 = NewsEntity.builder().id(2L).title("뉴스2").content("내용2").imageUrl("url2").build();
37+
38+
when(newsRepository.findAll()).thenReturn(List.of(news1, news2));
39+
40+
// When
41+
List<NewsResponseDto> newsList = newsService.getAllNews();
42+
43+
// Then
44+
assertThat(newsList).hasSize(2);
45+
assertThat(newsList.get(0).getTitle()).isEqualTo("뉴스1");
46+
verify(newsRepository, times(1)).findAll();
47+
}
48+
49+
@Test
50+
@DisplayName("뉴스 개별 조회 - 성공")
51+
void getNewsById_Success() {
52+
// Given
53+
Long id = 1L;
54+
NewsEntity news = NewsEntity.builder().id(id).title("뉴스1").content("내용1").imageUrl("url1").build();
55+
56+
when(newsRepository.findById(id)).thenReturn(Optional.of(news));
57+
58+
// When
59+
NewsResponseDto responseDto = newsService.getNewsById(id);
60+
61+
// Then
62+
assertThat(responseDto.getId()).isEqualTo(id);
63+
assertThat(responseDto.getTitle()).isEqualTo("뉴스1");
64+
verify(newsRepository, times(1)).findById(id);
65+
}
66+
67+
@Test
68+
@DisplayName("뉴스 개별 조회 - 실패 (존재하지 않는 ID)")
69+
void getNewsById_NotFound() {
70+
// Given
71+
Long id = 999L;
72+
73+
when(newsRepository.findById(id)).thenReturn(Optional.empty());
74+
75+
// When & Then
76+
Exception exception = assertThrows(IllegalArgumentException.class, () -> newsService.getNewsById(id));
77+
assertThat(exception.getMessage()).isEqualTo("해당 뉴스가 존재하지 않습니다. ID: " + id);
78+
verify(newsRepository, times(1)).findById(id);
79+
}
80+
81+
@Test
82+
@DisplayName("뉴스 생성 테스트")
83+
void createNews() {
84+
// Given
85+
NewsRequestDto requestDto = new NewsRequestDto("새 뉴스", "새 내용", "새 이미지");
86+
87+
NewsEntity news = NewsEntity.builder()
88+
.title(requestDto.getTitle())
89+
.content(requestDto.getContent())
90+
.imageUrl(requestDto.getImageUrl())
91+
.build();
92+
93+
NewsEntity savedNews = NewsEntity.builder()
94+
.id(1L)
95+
.title(news.getTitle())
96+
.content(news.getContent())
97+
.imageUrl(news.getImageUrl())
98+
.build();
99+
100+
when(newsRepository.save(any(NewsEntity.class))).thenReturn(savedNews);
101+
102+
// When
103+
NewsResponseDto responseDto = newsService.createNews(requestDto);
104+
105+
// Then
106+
assertThat(responseDto.getId()).isEqualTo(1L); // 저장된 뉴스의 ID 할당확인
107+
assertThat(responseDto.getTitle()).isEqualTo("새 뉴스");
108+
assertThat(responseDto.getContent()).isEqualTo("새 내용");
109+
assertThat(responseDto.getImageUrl()).isEqualTo("새 이미지");
110+
111+
verify(newsRepository, times(1)).save(any(NewsEntity.class)); // save() 호출 검증
112+
}
113+
114+
}

0 commit comments

Comments
 (0)