Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
583f7e7
feat: News에 siteUserId FK 추가
Gyuhyeok99 Jun 29, 2025
aef7987
refactor: 권한 검사 로직을 역할 기반으로 유연하게 개선
Gyuhyeok99 Jun 29, 2025
5563ef9
feat: 소식지 생성 api 추가
Gyuhyeok99 Jun 29, 2025
e002fcc
test: 소식지 생성 테스트 추가
Gyuhyeok99 Jun 29, 2025
a968baf
feat: 소식지 수정 api 추가
Gyuhyeok99 Jun 29, 2025
9b246fb
feat: 소식지 삭제 api 추가
Gyuhyeok99 Jun 29, 2025
1e8461f
feat: 특정 유저의 소식지 목록 조회 api 추가
Gyuhyeok99 Jun 29, 2025
4eaf083
style: news_id에서 news-id로 변경
Gyuhyeok99 Jun 29, 2025
37a0bb9
style: 테스트 클래스에서 불필요한 public 제거
Gyuhyeok99 Jun 29, 2025
627c691
refactor: @JsonProperty 제거
Gyuhyeok99 Jun 29, 2025
7d92804
refactor: defaultThumbnailUrl application-varaible.yml에서 가져오도록 변경
Gyuhyeok99 Jun 30, 2025
df0253a
refactor: RequireRoleAccess 어노테이션 roles 기본값 제거
Gyuhyeok99 Jun 30, 2025
954ef78
refactor: 권한 체크 로직 contains로 단순화하여 final 제거
Gyuhyeok99 Jun 30, 2025
8214db7
test: 권한별 나열식 테스트 제거, 요구 역할 유무에 따른 핵심 시나리오 중심으로 개선
Gyuhyeok99 Jun 30, 2025
18a9121
refactor: 소식지 수정 PATCH -> PUT으로 변경
Gyuhyeok99 Jun 30, 2025
389f3ee
test: 테스트 메서드명을 멘토 → 사용자로 일반화
Gyuhyeok99 Jun 30, 2025
36be2ab
style: NewsRepository 메서드 선언 개행 및 파라미터 타입 Long → long 변경
Gyuhyeok99 Jun 30, 2025
b275ede
refactor: 단일/목록 응답 클래스 네이밍 통일
Gyuhyeok99 Jun 30, 2025
a425050
chore: 서브모듈 커밋 반영
Gyuhyeok99 Jun 30, 2025
cb77894
refactor: siteUserId 타입을 Long에서 long으로 통일
Gyuhyeok99 Jul 1, 2025
cef3ff5
test: assertAll로 테스트 그룹화
Gyuhyeok99 Jul 1, 2025
a170b6b
refactor: private 메서드 위치를 호출부 아래로 이동
Gyuhyeok99 Jul 1, 2025
5eb35a7
chore: 서브모듈 커밋 반영
Gyuhyeok99 Jul 2, 2025
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
Expand Up @@ -6,7 +6,8 @@
import com.example.solidconnection.application.service.ApplicationQueryService;
import com.example.solidconnection.application.service.ApplicationSubmissionService;
import com.example.solidconnection.common.resolver.AuthorizedUser;
import com.example.solidconnection.security.annotation.RequireAdminAccess;
import com.example.solidconnection.security.annotation.RequireRoleAccess;
import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -39,7 +40,7 @@ public ResponseEntity<ApplicationSubmissionResponse> apply(
.body(applicationSubmissionResponse);
}

@RequireAdminAccess
@RequireRoleAccess(roles = {Role.ADMIN})
@GetMapping
public ResponseEntity<ApplicationsResponse> getApplicants(
@AuthorizedUser SiteUser siteUser,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public enum ErrorCode {
COUNTRY_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 국가를 찾을 수 없습니다."),
GPA_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 학점입니다."),
LANGUAGE_TEST_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 어학성적입니다."),
NEWS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 소식지입니다."),

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

// news
INVALID_NEWS_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 소식지만 제어할 수 있습니다."),


// database
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.solidconnection.news.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "news")
public record NewsProperties(
String defaultThumbnailUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.example.solidconnection.news.controller;

import com.example.solidconnection.common.resolver.AuthorizedUser;
import com.example.solidconnection.news.dto.NewsCommandResponse;
import com.example.solidconnection.news.dto.NewsCreateRequest;
import com.example.solidconnection.news.dto.NewsListResponse;
import com.example.solidconnection.news.dto.NewsUpdateRequest;
import com.example.solidconnection.news.service.NewsCommandService;
import com.example.solidconnection.news.service.NewsQueryService;
import com.example.solidconnection.security.annotation.RequireRoleAccess;
import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
@RequestMapping("/news")
public class NewsController {

private final NewsQueryService newsQueryService;
private final NewsCommandService newsCommandService;

// todo: 추후 Slice 적용
@GetMapping
public ResponseEntity<NewsListResponse> findNewsBySiteUserId(
@RequestParam(value = "site-user-id") Long siteUserId
) {
NewsListResponse newsListResponse = newsQueryService.findNewsBySiteUserId(siteUserId);
return ResponseEntity.ok(newsListResponse);
}

@RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR})
@PostMapping
public ResponseEntity<NewsCommandResponse> createNews(
@AuthorizedUser SiteUser siteUser,
@Valid @RequestPart("newsCreateRequest") NewsCreateRequest newsCreateRequest,
@RequestParam(value = "file", required = false) MultipartFile imageFile
) {
NewsCommandResponse newsCommandResponse = newsCommandService.createNews(siteUser.getId(), newsCreateRequest, imageFile);
return ResponseEntity.ok(newsCommandResponse);
}

@RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR})
@PutMapping("/{news-id}")
public ResponseEntity<NewsCommandResponse> updateNews(
@AuthorizedUser SiteUser siteUser,
@PathVariable("news-id") Long newsId,
@Valid @RequestPart(value = "newsUpdateRequest") NewsUpdateRequest newsUpdateRequest,
@RequestParam(value = "file", required = false) MultipartFile imageFile
) {
NewsCommandResponse newsCommandResponse = newsCommandService.updateNews(
siteUser.getId(),
newsId,
newsUpdateRequest,
imageFile);
return ResponseEntity.ok(newsCommandResponse);
}

@RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR})
@DeleteMapping("/{news-id}")
public ResponseEntity<NewsCommandResponse> deleteNewsById(
@AuthorizedUser SiteUser siteUser,
@PathVariable("news-id") Long newsId
) {
NewsCommandResponse newsCommandResponse = newsCommandService.deleteNewsById(siteUser, newsId);
return ResponseEntity.ok(newsCommandResponse);
}
}
32 changes: 30 additions & 2 deletions src/main/java/com/example/solidconnection/news/domain/News.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
public class News extends BaseEntity {

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

@Column(length = 500)
private String url;

private long siteUserId;

public News(
String title,
String description,
String thumbnailUrl,
String url,
long siteUserId) {
this.title = title;
this.description = description;
this.thumbnailUrl = thumbnailUrl;
this.url = url;
this.siteUserId = siteUserId;
}

public void updateNews(String title, String description, String url) {
this.title = title;
this.description = description;
this.url = url;
}

public void updateThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.solidconnection.news.dto;

import com.example.solidconnection.news.domain.News;

public record NewsCommandResponse(
long id
) {
public static NewsCommandResponse from(News news) {
return new NewsCommandResponse(
news.getId()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.solidconnection.news.dto;

import com.example.solidconnection.news.domain.News;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.hibernate.validator.constraints.URL;

public record NewsCreateRequest(
@NotBlank(message = "소식지 제목을 입력해주세요.")
@Size(max = 20, message = "소식지 제목은 20자 이하여야 합니다.")
String title,

@NotBlank(message = "소식지 내용을 입력해주세요.")
@Size(max = 30, message = "소식지 내용은 30자 이하여야 합니다.")
String description,

@NotBlank(message = "소식지 URL을 입력해주세요.")
@Size(max = 500, message = "소식지 URL은 500자 이하여야 합니다.")
@URL(message = "올바른 URL 형식이 아닙니다.")
String url
) {
public News toEntity(String thumbnailUrl, long siteUserId) {
return new News(
title,
description,
thumbnailUrl,
url,
siteUserId
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.news.dto;

import java.util.List;

public record NewsListResponse(
List<NewsResponse> newsResponseList
) {
public static NewsListResponse from(List<NewsResponse> newsResponseList) {
return new NewsListResponse(newsResponseList);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.solidconnection.news.dto;

import com.example.solidconnection.news.domain.News;

import java.time.ZonedDateTime;

public record NewsResponse(
long id,
String title,
String description,
String thumbnailUrl,
String url,
ZonedDateTime updatedAt
) {
public static NewsResponse from(News news) {
return new NewsResponse(
news.getId(),
news.getTitle(),
news.getDescription(),
news.getThumbnailUrl(),
news.getUrl(),
news.getUpdatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.solidconnection.news.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.hibernate.validator.constraints.URL;

public record NewsUpdateRequest(
@NotBlank(message = "소식지 제목을 입력해주세요.")
@Size(max = 20, message = "소식지 제목은 20자 이하여야 합니다.")
String title,

@NotBlank(message = "소식지 내용을 입력해주세요.")
@Size(max = 30, message = "소식지 내용은 30자 이하여야 합니다.")
String description,

@NotBlank(message = "소식지 URL을 입력해주세요.")
@Size(max = 500, message = "소식지 URL은 500자 이하여야 합니다.")
@URL(message = "올바른 URL 형식이 아닙니다.")
String url,

Boolean resetToDefaultImage
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.news.repository;

import com.example.solidconnection.news.domain.News;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface NewsRepository extends JpaRepository<News, Long> {

List<News> findAllBySiteUserIdOrderByUpdatedAtDesc(long siteUserId);
}
Loading
Loading