Skip to content

Commit b4a5d09

Browse files
authored
feat: 소식지 서비스 구현 (#353)
* feat: News에 siteUserId FK 추가 * refactor: 권한 검사 로직을 역할 기반으로 유연하게 개선 - 기존 어드민 전용 어노테이션을 다중 역할 지원하도록 확장 - @RequireRoleAccess 어노테이션으로 여러 역할 조합 가능하게 변경 * feat: 소식지 생성 api 추가 * test: 소식지 생성 테스트 추가 - 소식지 관련 fixture 추가 * feat: 소식지 수정 api 추가 - default 이미지 URL 설정 어떻게 할지 논의 필요 * feat: 소식지 삭제 api 추가 - 어드민은 그냥 삭제 가능한지 논의 필요 * feat: 특정 유저의 소식지 목록 조회 api 추가 - 추후 Slice 적용 필요 * style: news_id에서 news-id로 변경 * style: 테스트 클래스에서 불필요한 public 제거 * refactor: @JsonProperty 제거 * refactor: defaultThumbnailUrl application-varaible.yml에서 가져오도록 변경 * refactor: RequireRoleAccess 어노테이션 roles 기본값 제거 * refactor: 권한 체크 로직 contains로 단순화하여 final 제거 * test: 권한별 나열식 테스트 제거, 요구 역할 유무에 따른 핵심 시나리오 중심으로 개선 * refactor: 소식지 수정 PATCH -> PUT으로 변경 * test: 테스트 메서드명을 멘토 → 사용자로 일반화 * style: NewsRepository 메서드 선언 개행 및 파라미터 타입 Long → long 변경 * refactor: 단일/목록 응답 클래스 네이밍 통일 * chore: 서브모듈 커밋 반영 * refactor: siteUserId 타입을 Long에서 long으로 통일 * test: assertAll로 테스트 그룹화 * refactor: private 메서드 위치를 호출부 아래로 이동 * chore: 서브모듈 커밋 반영
1 parent bacdbbb commit b4a5d09

File tree

26 files changed

+962
-102
lines changed

26 files changed

+962
-102
lines changed

src/main/java/com/example/solidconnection/application/controller/ApplicationController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import com.example.solidconnection.application.service.ApplicationQueryService;
77
import com.example.solidconnection.application.service.ApplicationSubmissionService;
88
import com.example.solidconnection.common.resolver.AuthorizedUser;
9-
import com.example.solidconnection.security.annotation.RequireAdminAccess;
9+
import com.example.solidconnection.security.annotation.RequireRoleAccess;
10+
import com.example.solidconnection.siteuser.domain.Role;
1011
import com.example.solidconnection.siteuser.domain.SiteUser;
1112
import jakarta.validation.Valid;
1213
import lombok.RequiredArgsConstructor;
@@ -39,7 +40,7 @@ public ResponseEntity<ApplicationSubmissionResponse> apply(
3940
.body(applicationSubmissionResponse);
4041
}
4142

42-
@RequireAdminAccess
43+
@RequireRoleAccess(roles = {Role.ADMIN})
4344
@GetMapping
4445
public ResponseEntity<ApplicationsResponse> getApplicants(
4546
@AuthorizedUser SiteUser siteUser,

src/main/java/com/example/solidconnection/common/exception/ErrorCode.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public enum ErrorCode {
4242
COUNTRY_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 국가를 찾을 수 없습니다."),
4343
GPA_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 학점입니다."),
4444
LANGUAGE_TEST_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 어학성적입니다."),
45+
NEWS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 소식지입니다."),
4546

4647
// auth
4748
USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."),
@@ -96,6 +97,10 @@ public enum ErrorCode {
9697
USER_DO_NOT_HAVE_GPA(HttpStatus.BAD_REQUEST.value(), "해당 유저의 학점을 찾을 수 없음"),
9798
REJECTED_REASON_REQUIRED(HttpStatus.BAD_REQUEST.value(), "거절 사유가 필요합니다."),
9899

100+
// news
101+
INVALID_NEWS_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 소식지만 제어할 수 있습니다."),
102+
103+
99104
// database
100105
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."),
101106

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.solidconnection.news.config;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
@ConfigurationProperties(prefix = "news")
6+
public record NewsProperties(
7+
String defaultThumbnailUrl
8+
) {
9+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.example.solidconnection.news.controller;
2+
3+
import com.example.solidconnection.common.resolver.AuthorizedUser;
4+
import com.example.solidconnection.news.dto.NewsCommandResponse;
5+
import com.example.solidconnection.news.dto.NewsCreateRequest;
6+
import com.example.solidconnection.news.dto.NewsListResponse;
7+
import com.example.solidconnection.news.dto.NewsUpdateRequest;
8+
import com.example.solidconnection.news.service.NewsCommandService;
9+
import com.example.solidconnection.news.service.NewsQueryService;
10+
import com.example.solidconnection.security.annotation.RequireRoleAccess;
11+
import com.example.solidconnection.siteuser.domain.Role;
12+
import com.example.solidconnection.siteuser.domain.SiteUser;
13+
import jakarta.validation.Valid;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.DeleteMapping;
17+
import org.springframework.web.bind.annotation.GetMapping;
18+
import org.springframework.web.bind.annotation.PathVariable;
19+
import org.springframework.web.bind.annotation.PostMapping;
20+
import org.springframework.web.bind.annotation.PutMapping;
21+
import org.springframework.web.bind.annotation.RequestMapping;
22+
import org.springframework.web.bind.annotation.RequestParam;
23+
import org.springframework.web.bind.annotation.RequestPart;
24+
import org.springframework.web.bind.annotation.RestController;
25+
import org.springframework.web.multipart.MultipartFile;
26+
27+
@RestController
28+
@RequiredArgsConstructor
29+
@RequestMapping("/news")
30+
public class NewsController {
31+
32+
private final NewsQueryService newsQueryService;
33+
private final NewsCommandService newsCommandService;
34+
35+
// todo: 추후 Slice 적용
36+
@GetMapping
37+
public ResponseEntity<NewsListResponse> findNewsBySiteUserId(
38+
@RequestParam(value = "site-user-id") Long siteUserId
39+
) {
40+
NewsListResponse newsListResponse = newsQueryService.findNewsBySiteUserId(siteUserId);
41+
return ResponseEntity.ok(newsListResponse);
42+
}
43+
44+
@RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR})
45+
@PostMapping
46+
public ResponseEntity<NewsCommandResponse> createNews(
47+
@AuthorizedUser SiteUser siteUser,
48+
@Valid @RequestPart("newsCreateRequest") NewsCreateRequest newsCreateRequest,
49+
@RequestParam(value = "file", required = false) MultipartFile imageFile
50+
) {
51+
NewsCommandResponse newsCommandResponse = newsCommandService.createNews(siteUser.getId(), newsCreateRequest, imageFile);
52+
return ResponseEntity.ok(newsCommandResponse);
53+
}
54+
55+
@RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR})
56+
@PutMapping("/{news-id}")
57+
public ResponseEntity<NewsCommandResponse> updateNews(
58+
@AuthorizedUser SiteUser siteUser,
59+
@PathVariable("news-id") Long newsId,
60+
@Valid @RequestPart(value = "newsUpdateRequest") NewsUpdateRequest newsUpdateRequest,
61+
@RequestParam(value = "file", required = false) MultipartFile imageFile
62+
) {
63+
NewsCommandResponse newsCommandResponse = newsCommandService.updateNews(
64+
siteUser.getId(),
65+
newsId,
66+
newsUpdateRequest,
67+
imageFile);
68+
return ResponseEntity.ok(newsCommandResponse);
69+
}
70+
71+
@RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR})
72+
@DeleteMapping("/{news-id}")
73+
public ResponseEntity<NewsCommandResponse> deleteNewsById(
74+
@AuthorizedUser SiteUser siteUser,
75+
@PathVariable("news-id") Long newsId
76+
) {
77+
NewsCommandResponse newsCommandResponse = newsCommandService.deleteNewsById(siteUser, newsId);
78+
return ResponseEntity.ok(newsCommandResponse);
79+
}
80+
}

src/main/java/com/example/solidconnection/news/domain/News.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
import jakarta.persistence.GeneratedValue;
77
import jakarta.persistence.GenerationType;
88
import jakarta.persistence.Id;
9+
import lombok.AccessLevel;
10+
import lombok.AllArgsConstructor;
911
import lombok.EqualsAndHashCode;
1012
import lombok.Getter;
1113
import lombok.NoArgsConstructor;
1214

13-
@Getter
1415
@Entity
15-
@NoArgsConstructor
16+
@Getter
17+
@AllArgsConstructor
18+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1619
@EqualsAndHashCode
1720
public class News extends BaseEntity {
1821

@@ -29,4 +32,29 @@ public class News extends BaseEntity {
2932

3033
@Column(length = 500)
3134
private String url;
35+
36+
private long siteUserId;
37+
38+
public News(
39+
String title,
40+
String description,
41+
String thumbnailUrl,
42+
String url,
43+
long siteUserId) {
44+
this.title = title;
45+
this.description = description;
46+
this.thumbnailUrl = thumbnailUrl;
47+
this.url = url;
48+
this.siteUserId = siteUserId;
49+
}
50+
51+
public void updateNews(String title, String description, String url) {
52+
this.title = title;
53+
this.description = description;
54+
this.url = url;
55+
}
56+
57+
public void updateThumbnailUrl(String thumbnailUrl) {
58+
this.thumbnailUrl = thumbnailUrl;
59+
}
3260
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.example.solidconnection.news.dto;
2+
3+
import com.example.solidconnection.news.domain.News;
4+
5+
public record NewsCommandResponse(
6+
long id
7+
) {
8+
public static NewsCommandResponse from(News news) {
9+
return new NewsCommandResponse(
10+
news.getId()
11+
);
12+
}
13+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.example.solidconnection.news.dto;
2+
3+
import com.example.solidconnection.news.domain.News;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Size;
6+
import org.hibernate.validator.constraints.URL;
7+
8+
public record NewsCreateRequest(
9+
@NotBlank(message = "소식지 제목을 입력해주세요.")
10+
@Size(max = 20, message = "소식지 제목은 20자 이하여야 합니다.")
11+
String title,
12+
13+
@NotBlank(message = "소식지 내용을 입력해주세요.")
14+
@Size(max = 30, message = "소식지 내용은 30자 이하여야 합니다.")
15+
String description,
16+
17+
@NotBlank(message = "소식지 URL을 입력해주세요.")
18+
@Size(max = 500, message = "소식지 URL은 500자 이하여야 합니다.")
19+
@URL(message = "올바른 URL 형식이 아닙니다.")
20+
String url
21+
) {
22+
public News toEntity(String thumbnailUrl, long siteUserId) {
23+
return new News(
24+
title,
25+
description,
26+
thumbnailUrl,
27+
url,
28+
siteUserId
29+
);
30+
}
31+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.solidconnection.news.dto;
2+
3+
import java.util.List;
4+
5+
public record NewsListResponse(
6+
List<NewsResponse> newsResponseList
7+
) {
8+
public static NewsListResponse from(List<NewsResponse> newsResponseList) {
9+
return new NewsListResponse(newsResponseList);
10+
}
11+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.example.solidconnection.news.dto;
2+
3+
import com.example.solidconnection.news.domain.News;
4+
5+
import java.time.ZonedDateTime;
6+
7+
public record NewsResponse(
8+
long id,
9+
String title,
10+
String description,
11+
String thumbnailUrl,
12+
String url,
13+
ZonedDateTime updatedAt
14+
) {
15+
public static NewsResponse from(News news) {
16+
return new NewsResponse(
17+
news.getId(),
18+
news.getTitle(),
19+
news.getDescription(),
20+
news.getThumbnailUrl(),
21+
news.getUrl(),
22+
news.getUpdatedAt()
23+
);
24+
}
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.example.solidconnection.news.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
5+
import org.hibernate.validator.constraints.URL;
6+
7+
public record NewsUpdateRequest(
8+
@NotBlank(message = "소식지 제목을 입력해주세요.")
9+
@Size(max = 20, message = "소식지 제목은 20자 이하여야 합니다.")
10+
String title,
11+
12+
@NotBlank(message = "소식지 내용을 입력해주세요.")
13+
@Size(max = 30, message = "소식지 내용은 30자 이하여야 합니다.")
14+
String description,
15+
16+
@NotBlank(message = "소식지 URL을 입력해주세요.")
17+
@Size(max = 500, message = "소식지 URL은 500자 이하여야 합니다.")
18+
@URL(message = "올바른 URL 형식이 아닙니다.")
19+
String url,
20+
21+
Boolean resetToDefaultImage
22+
) {
23+
}

0 commit comments

Comments
 (0)