Skip to content

Commit 91f9021

Browse files
dohy-eonysw789
andauthored
refactor: 다솜소식API 내부 로직 수정
* refactor: News 예외처리 수정 * refactor: 속성 관련 검증로직 수정 * fix: ErrorCode 세미콜론추가 * feat: 수정삭제 테스트케이스 작성 * refactor: 생성 트랜잭션추가 * refactor: 404응답처리 400번으로 수정 * refactor: 코드컨벤션 공백수정 * feat: News 이미지 관련 파일 수정 * refactor: 이미지 저장 로직 수정 * refactor: 파일저장 로직수정 * fix: 테스트케이스 수정 * fix: 파일저장 인코딩 수정 * refactor: 응답 객체 추가 및 반환요청 수정 * refactor: NewsEntity-FileEntity 양방향 관계설정 * fix: 테스트코드 수정 * fix: 테스트케이스 수정 * infra: 파일 업로드 용량 제한 설정 * refactor: 파일 업로드, 조회 로직 리팩토링 - 연관 엔티티와 직접 연관관계를 갖지 않고, 내부적으로 fileType(엔티티), targetId(엔티티 Id)로 구분함 - 파일 저장 요청 시, 어느 엔티티를 위한 파일인지 fileType을 명시해야함 * feat: 파일 업로드 관련 에러코드 추가 * feat: 뉴스 CRUD 리팩토링 - 등록, 전체 조회(사진 제외), 상세 조회, 수정(사진 제외), 삭제 구현 완료 - 썸네일 포함한 전체 조회, 사진 관련 수정 기능 구현 예정 * chore: 테스트 케이스 임시 주석 처리 * feat: fileType, targetId로 파일 조회 구현 * feat: 뉴스 목록 썸네일 포함 응답 구현 * feat: 뉴스 수정 요청 이미지 삭제 요청 처리 구현 - 파일 업로드 API를 통해 추가할 이미지 별도 업로드 * chore: 미사용 파일 제거, 공백 수정 --------- Co-authored-by: Seungwan Yoo <[email protected]>
1 parent 4a5c6c2 commit 91f9021

File tree

18 files changed

+657
-205
lines changed

18 files changed

+657
-205
lines changed

build.gradle

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ dependencies {
5454
implementation 'com.google.apis:google-api-services-sheets:v4-rev516-1.23.0'
5555
implementation 'com.google.auth:google-auth-library-oauth2-http:0.20.0'
5656

57-
58-
59-
60-
6157
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
6258
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
6359
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public enum ErrorCode {
2727
INVALID_INQUIRY_PERIOD(400, "C018", "조회 기간이 아닙니다."),
2828
SHEET_WRITE_FAIL(400, "C019", "시트에 데이터를 쓰는데 실패하였습니다."),
2929
SHEET_READ_FAIL(400, "C200", "시트에 데이터를 쓰는데 실패하였습니다."),
30+
FILE_ENCODE_FAIL(400, "C028", "파일 인코딩에 실패하였습니다.")
3031
;
3132

3233
private final int status;
Lines changed: 22 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,55 @@
11
package dmu.dasom.api.domain.news.controller;
22

3-
import dmu.dasom.api.domain.news.dto.NewsRequestDto;
4-
import dmu.dasom.api.domain.news.dto.NewsResponseDto;
3+
import dmu.dasom.api.domain.news.dto.*;
54
import dmu.dasom.api.domain.news.service.NewsService;
65
import io.swagger.v3.oas.annotations.tags.Tag;
76
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;
7+
import jakarta.validation.constraints.Min;
8+
import lombok.RequiredArgsConstructor;
129
import org.springframework.http.ResponseEntity;
1310
import org.springframework.web.bind.annotation.*;
1411

1512
import jakarta.validation.Valid;
1613
import java.util.List;
1714

18-
@Tag(name = "NEWS API", description = "다솜소식 API")
15+
@Tag(name = "NEWS API", description = "뉴스 API")
1916
@RestController
17+
@RequiredArgsConstructor
2018
@RequestMapping("/api/news")
2119
public class NewsController {
2220

2321
private final NewsService newsService;
2422

25-
public NewsController(NewsService newsService) {
26-
this.newsService = newsService;
27-
}
28-
29-
// 전체 조회
30-
@Operation(summary = "소식 조회", description = "리스트로 조회")
31-
@ApiResponse(responseCode = "200", description = "정상 응답",
32-
content = @Content(mediaType = "application/json"))
23+
@Operation(summary = "전체 뉴스 조회 (썸네일 포함)")
3324
@GetMapping
34-
public ResponseEntity<List<NewsResponseDto>> getAllNews() {
35-
List<NewsResponseDto> newsList = newsService.getAllNews();
36-
return ResponseEntity.ok(newsList);
25+
public ResponseEntity<List<NewsListResponseDto>> getAllNews() {
26+
return ResponseEntity.ok(newsService.getAllNews());
3727
}
3828

39-
// 개별 조회
40-
@Operation(summary = "소식 상세 조회", description = "ID로 특정 소식을 조회")
41-
@ApiResponses({
42-
@ApiResponse(responseCode = "200", description = "조회 성공"),
43-
@ApiResponse(responseCode = "404", description = "소식을 찾을 수 없음")
44-
})
29+
@Operation(summary = "뉴스 상세 조회")
4530
@GetMapping("/{id}")
46-
public ResponseEntity<NewsResponseDto> getNewsById(@PathVariable Long id) {
47-
NewsResponseDto responseDto = newsService.getNewsById(id);
48-
return ResponseEntity.ok(responseDto);
31+
public ResponseEntity<NewsResponseDto> getNewsById(@PathVariable @Min(1) Long id) {
32+
return ResponseEntity.ok(newsService.getNewsById(id));
4933
}
5034

51-
// 생성
52-
@Operation(summary = "소식 등록", description = "새로운 소식을 등록")
53-
@ApiResponses(value = {
54-
@ApiResponse(responseCode = "201", description = "생성 완료"),
55-
@ApiResponse(responseCode = "400", description = "유효하지 않은 요청 데이터",
56-
content = @Content(mediaType = "application/json",
57-
examples = @ExampleObject(value = "{ \"errorCode\": \"E003\", \"errorMessage\": \"유효하지 않은 입력입니다.\" }")))
58-
})
35+
@Operation(summary = "뉴스 등록")
5936
@PostMapping
60-
public ResponseEntity<NewsResponseDto> createNews(@Valid @RequestBody NewsRequestDto requestDto) {
61-
NewsResponseDto responseDto = newsService.createNews(requestDto);
62-
return ResponseEntity.status(201).body(responseDto);
37+
public ResponseEntity<NewsCreationResponseDto> createNews(@Valid @RequestBody NewsRequestDto requestDto) {
38+
return ResponseEntity.status(201).body(newsService.createNews(requestDto));
6339
}
6440

65-
// 수정
66-
@Operation(summary = "소식 수정", description = "ID로 특정 소식을 수정")
67-
@ApiResponses({
68-
@ApiResponse(responseCode = "200", description = "수정 성공"),
69-
@ApiResponse(responseCode = "404", description = "소식을 찾을 수 없음")
70-
})
41+
@Operation(summary = "뉴스 수정")
7142
@PutMapping("/{id}")
72-
public ResponseEntity<NewsResponseDto> updateNews(@PathVariable Long id, @Valid @RequestBody NewsRequestDto requestDto) {
73-
NewsResponseDto updatedNews = newsService.updateNews(id, requestDto);
74-
return ResponseEntity.ok(updatedNews);
43+
public ResponseEntity<NewsResponseDto> updateNews(@PathVariable @Min(1) Long id,
44+
@Valid @RequestBody NewsUpdateRequestDto requestDto) {
45+
return ResponseEntity.ok(newsService.updateNews(id, requestDto));
7546
}
7647

77-
// 삭제
78-
@Operation(summary = "소식 삭제", description = "ID로 특정 소식을 삭제")
79-
@ApiResponses({
80-
@ApiResponse(responseCode = "204", description = "삭제 성공"),
81-
@ApiResponse(responseCode = "404", description = "소식을 찾을 수 없음")
82-
})
48+
@Operation(summary = "뉴스 삭제")
8349
@DeleteMapping("/{id}")
8450
public ResponseEntity<Void> deleteNews(@PathVariable Long id) {
8551
newsService.deleteNews(id);
86-
return ResponseEntity.noContent().build();
52+
return ResponseEntity.ok().build();
8753
}
88-
89-
}
54+
55+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package dmu.dasom.api.domain.news.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotNull;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Getter
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
@Schema(name = "NewsCreationResponseDto", description = "뉴스 생성 응답 DTO")
13+
public class NewsCreationResponseDto {
14+
15+
@NotNull
16+
@Schema(description = "뉴스 ID", example = "1")
17+
private Long id;
18+
19+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dmu.dasom.api.domain.news.dto;
2+
3+
import dmu.dasom.api.global.file.dto.FileResponseDto;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
import java.time.LocalDateTime;
11+
12+
@Getter
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Builder
16+
@Schema(name = "NewsListResponseDto", description = "뉴스 리스트 응답 DTO")
17+
public class NewsListResponseDto {
18+
19+
@Schema(description = "소식 ID", example = "1")
20+
private Long id;
21+
22+
@Schema(description = "뉴스 제목", example = "제목")
23+
private String title;
24+
25+
@Schema(description = "작성일", example = "2025-02-14T12:00:00")
26+
private LocalDateTime createdAt;
27+
28+
@Schema(description = "인코딩된 이미지", example = "asdf", nullable = true)
29+
private FileResponseDto image;
30+
31+
}
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dmu.dasom.api.domain.news.dto;
22

3+
import dmu.dasom.api.domain.news.entity.NewsEntity;
34
import io.swagger.v3.oas.annotations.media.Schema;
45
import jakarta.validation.constraints.NotBlank;
56
import jakarta.validation.constraints.Size;
@@ -13,16 +14,20 @@
1314
@Schema(name = "NewsRequestDto", description = "뉴스 생성 요청 DTO")
1415
public class NewsRequestDto {
1516

16-
@NotBlank
17-
@Size(max = 100)
17+
@NotBlank(message = "뉴스 제목은 필수입니다.")
18+
@Size(max = 100, message = "뉴스 제목은 최대 100자입니다.")
1819
@Schema(description = "뉴스 제목", example = "새로운 뉴스 제목")
1920
private String title;
2021

21-
@NotBlank
22+
@NotBlank(message = "뉴스 내용은 필수입니다.")
2223
@Schema(description = "뉴스 내용", example = "새로운 뉴스 내용")
2324
private String content;
2425

25-
@Size(max = 255)
26-
@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg", nullable = true)
27-
private String imageUrl;
28-
}
26+
public NewsEntity toEntity() {
27+
return NewsEntity.builder()
28+
.title(this.title)
29+
.content(this.content)
30+
.build();
31+
}
32+
33+
}
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package dmu.dasom.api.domain.news.dto;
22

3+
import dmu.dasom.api.global.file.dto.FileResponseDto;
34
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
47
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
59

610
import java.time.LocalDateTime;
11+
import java.util.List;
712

813
@Getter
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
@Builder
917
@Schema(name = "NewsResponseDto", description = "뉴스 응답 DTO")
1018
public class NewsResponseDto {
1119

@@ -21,14 +29,7 @@ public class NewsResponseDto {
2129
@Schema(description = "작성일", example = "2025-02-14T12:00:00")
2230
private LocalDateTime createdAt;
2331

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-
}
32+
@Schema(description = "인코딩된 이미지", example = "asdf", nullable = true)
33+
private List<FileResponseDto> images;
34+
35+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dmu.dasom.api.domain.news.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.Size;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.util.List;
10+
11+
@Getter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Schema(name = "NewsUpdateRequestDto", description = "뉴스 수정 요청 DTO")
15+
public class NewsUpdateRequestDto {
16+
17+
@Size(max = 100, message = "뉴스 제목은 최대 100자입니다.")
18+
@Schema(description = "수정할 뉴스 제목", example = "뉴스 제목", nullable = true)
19+
private String title;
20+
21+
@Schema(description = "수정할 뉴스 내용", example = "뉴스 내용", nullable = true)
22+
private String content;
23+
24+
@Schema(description = "삭제할 이미지 ID 목록", example = "[1, 2, 3]", nullable = true)
25+
private List<Long> deleteImageIds;
26+
27+
}
Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package dmu.dasom.api.domain.news.entity;
22

33
import dmu.dasom.api.domain.common.BaseEntity;
4-
import dmu.dasom.api.domain.common.Status;
4+
import dmu.dasom.api.domain.news.dto.NewsListResponseDto;
55
import dmu.dasom.api.domain.news.dto.NewsResponseDto;
6+
import dmu.dasom.api.global.file.dto.FileResponseDto;
67
import io.swagger.v3.oas.annotations.media.Schema;
78
import jakarta.persistence.*;
8-
import jakarta.validation.constraints.NotBlank;
9-
import jakarta.validation.constraints.NotNull;
10-
import jakarta.validation.constraints.Size;
119
import lombok.*;
10+
import org.apache.commons.lang3.ObjectUtils;
11+
import org.hibernate.annotations.DynamicUpdate;
1212

13+
import java.util.List;
14+
15+
@DynamicUpdate
1316
@Getter
1417
@Entity
1518
@Table(name = "news")
@@ -21,53 +24,37 @@ public class NewsEntity extends BaseEntity {
2124

2225
@Id
2326
@GeneratedValue(strategy = GenerationType.IDENTITY)
24-
@Schema(description = "뉴스 ID", example = "1")
2527
private Long id;
2628

27-
@NotNull
28-
@Schema(description = "뉴스 제목", example = "뉴스 예제 제목")
2929
@Column(nullable = false, length = 100)
3030
private String title;
3131

3232
@Lob
33-
@NotNull
34-
@Schema(description = "뉴스 내용", example = "뉴스 예제 내용")
3533
@Column(nullable = false)
3634
private String content;
3735

38-
39-
@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg")
40-
@Column(length = 255)
41-
private String imageUrl;
42-
43-
// 뉴스 상태 업데이트
44-
public void updateStatus(Status status) {
45-
super.updateStatus(status);
36+
public void update(String title, String content) {
37+
this.title = title;
38+
this.content = content;
4639
}
4740

48-
// NewsEntity → NewsResponseDto 변환
49-
public NewsResponseDto toResponseDto() {
50-
return new NewsResponseDto(id, title, content, getCreatedAt(), imageUrl);
41+
public NewsResponseDto toResponseDto(List<FileResponseDto> images) {
42+
return NewsResponseDto.builder()
43+
.id(this.id)
44+
.title(this.title)
45+
.content(this.content)
46+
.createdAt(getCreatedAt())
47+
.images(ObjectUtils.isEmpty(images) ? null : images)
48+
.build();
5149
}
5250

53-
//수정기능
54-
public void update(String title, String content, String imageUrl) {
55-
if (title == null || title.isBlank()) {
56-
throw new IllegalArgumentException("제목은 필수입니다");
57-
}
58-
if (title.length() > 100) {
59-
throw new IllegalArgumentException("제목은 100자까지");
60-
}
61-
if (content == null || content.isBlank()) {
62-
throw new IllegalArgumentException("내용은 필수입니다");
63-
}
64-
if (imageUrl != null && imageUrl.length() > 255) {
65-
throw new IllegalArgumentException("이미지 URL은 255자까지");
66-
}
67-
68-
this.title = title;
69-
this.content = content;
70-
this.imageUrl = imageUrl;
51+
public NewsListResponseDto toListResponseDto(FileResponseDto file) {
52+
return NewsListResponseDto.builder()
53+
.id(this.id)
54+
.title(this.title)
55+
.createdAt(getCreatedAt())
56+
.image(file)
57+
.build();
7158
}
7259

7360
}

0 commit comments

Comments
 (0)