Skip to content

Commit 5b0d708

Browse files
authored
Merge pull request #1328 from Moadong/feature/#1326-add-patch-promotion-controller-MOA-742
[feature] 홍보 게시글 수정 API 추가
2 parents b636156 + 0a430e6 commit 5b0d708

File tree

8 files changed

+214
-9
lines changed

8 files changed

+214
-9
lines changed

backend/src/main/java/moadong/club/controller/PromotionArticleController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
import io.swagger.v3.oas.annotations.tags.Tag;
66
import lombok.RequiredArgsConstructor;
77
import moadong.club.payload.request.PromotionArticleCreateRequest;
8+
import moadong.club.payload.request.PromotionArticleUpdateRequest;
89
import moadong.club.payload.response.PromotionArticleResponse;
910
import moadong.club.service.PromotionArticleService;
1011
import moadong.global.payload.Response;
1112
import org.springframework.http.ResponseEntity;
1213
import org.springframework.security.access.prepost.PreAuthorize;
1314
import org.springframework.validation.annotation.Validated;
1415
import org.springframework.web.bind.annotation.GetMapping;
16+
import org.springframework.web.bind.annotation.PathVariable;
1517
import org.springframework.web.bind.annotation.PostMapping;
18+
import org.springframework.web.bind.annotation.PutMapping;
1619
import org.springframework.web.bind.annotation.RequestBody;
1720
import org.springframework.web.bind.annotation.RequestMapping;
1821
import org.springframework.web.bind.annotation.RestController;
@@ -41,4 +44,15 @@ public ResponseEntity<?> createPromotionArticle(
4144
promotionArticleService.createPromotionArticle(request);
4245
return Response.ok("홍보 게시글이 생성되었습니다.");
4346
}
47+
48+
@PutMapping("/{articleId}")
49+
@Operation(summary = "홍보 게시글 수정", description = "기존 홍보 게시글을 수정합니다.")
50+
@PreAuthorize("hasRole('DEVELOPER')")
51+
@SecurityRequirement(name = "BearerAuth")
52+
public ResponseEntity<?> updatePromotionArticle(
53+
@PathVariable String articleId,
54+
@RequestBody @Validated PromotionArticleUpdateRequest request) {
55+
promotionArticleService.updatePromotionArticle(articleId, request);
56+
return Response.ok("홍보 게시글이 수정되었습니다.");
57+
}
4458
}

backend/src/main/java/moadong/club/entity/PromotionArticle.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.Builder;
55
import lombok.Getter;
66
import lombok.NoArgsConstructor;
7+
import moadong.club.payload.request.PromotionArticleUpdateRequest;
78
import org.springframework.data.annotation.Id;
89
import org.springframework.data.mongodb.core.mapping.Document;
910

@@ -38,4 +39,15 @@ public class PromotionArticle {
3839

3940
@Builder.Default
4041
private Instant createdAt = Instant.now();
42+
43+
public void update(PromotionArticleUpdateRequest request, String clubName) {
44+
this.clubId = request.clubId();
45+
this.clubName = clubName;
46+
this.title = request.title();
47+
this.location = request.location();
48+
this.eventStartDate = request.eventStartDate();
49+
this.eventEndDate = request.eventEndDate();
50+
this.description = request.description();
51+
this.images = request.images();
52+
}
4153
}

backend/src/main/java/moadong/club/payload/dto/PromotionArticleDto.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package moadong.club.payload.dto;
22

3+
import moadong.club.entity.PromotionArticle;
4+
35
import java.time.Instant;
46
import java.util.List;
57

68
public record PromotionArticleDto(
9+
String id,
710
String clubName,
811
String clubId,
912
String title,
@@ -13,4 +16,17 @@ public record PromotionArticleDto(
1316
String description,
1417
List<String> images
1518
) {
19+
public static PromotionArticleDto from(PromotionArticle article) {
20+
return new PromotionArticleDto(
21+
article.getId(),
22+
article.getClubName(),
23+
article.getClubId(),
24+
article.getTitle(),
25+
article.getLocation(),
26+
article.getEventStartDate(),
27+
article.getEventEndDate(),
28+
article.getDescription(),
29+
article.getImages()
30+
);
31+
}
1632
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package moadong.club.payload.request;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotEmpty;
5+
import jakarta.validation.constraints.NotNull;
6+
7+
import java.time.Instant;
8+
import java.util.List;
9+
10+
public record PromotionArticleUpdateRequest(
11+
@NotBlank String clubId,
12+
@NotBlank String title,
13+
@NotBlank String location,
14+
@NotNull Instant eventStartDate,
15+
@NotNull Instant eventEndDate,
16+
@NotBlank String description,
17+
@NotEmpty List<String> images
18+
) {
19+
}
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
package moadong.club.repository;
22

33
import moadong.club.entity.PromotionArticle;
4-
import moadong.club.payload.dto.PromotionArticleDto;
54
import org.springframework.data.mongodb.repository.MongoRepository;
6-
import org.springframework.data.mongodb.repository.Query;
75
import org.springframework.stereotype.Repository;
86

97
import java.util.List;
108

119
@Repository
1210
public interface PromotionArticleRepository extends MongoRepository<PromotionArticle, String> {
1311

14-
@Query(value = "{}", sort = "{ 'createdAt': -1 }")
15-
List<PromotionArticleDto> findAllProjectedBy();
12+
List<PromotionArticle> findAllByOrderByCreatedAtDesc();
1613

1714
List<PromotionArticle> findByClubIdOrderByCreatedAtDesc(String clubId);
1815
}

backend/src/main/java/moadong/club/service/PromotionArticleService.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import moadong.club.entity.PromotionArticle;
66
import moadong.club.payload.dto.PromotionArticleDto;
77
import moadong.club.payload.request.PromotionArticleCreateRequest;
8+
import moadong.club.payload.request.PromotionArticleUpdateRequest;
89
import moadong.club.payload.response.PromotionArticleResponse;
910
import moadong.club.repository.ClubRepository;
1011
import moadong.club.repository.PromotionArticleRepository;
@@ -25,15 +26,16 @@ public class PromotionArticleService {
2526
private final ClubRepository clubRepository;
2627

2728
public PromotionArticleResponse getPromotionArticles() {
28-
List<PromotionArticleDto> articles = promotionArticleRepository.findAllProjectedBy();
29+
List<PromotionArticleDto> articles = promotionArticleRepository.findAllByOrderByCreatedAtDesc()
30+
.stream()
31+
.map(PromotionArticleDto::from)
32+
.toList();
2933
return new PromotionArticleResponse(articles);
3034
}
3135

3236
@Transactional
3337
public void createPromotionArticle(PromotionArticleCreateRequest request) {
34-
ObjectId clubObjectId = ObjectIdConverter.convertString(request.clubId());
35-
Club club = clubRepository.findClubById(clubObjectId)
36-
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
38+
Club club = getClub(request.clubId());
3739

3840
PromotionArticle article = PromotionArticle.builder()
3941
.clubId(request.clubId())
@@ -48,4 +50,20 @@ public void createPromotionArticle(PromotionArticleCreateRequest request) {
4850

4951
promotionArticleRepository.save(article);
5052
}
53+
54+
@Transactional
55+
public void updatePromotionArticle(String articleId, PromotionArticleUpdateRequest request) {
56+
PromotionArticle article = promotionArticleRepository.findById(articleId)
57+
.orElseThrow(() -> new RestApiException(ErrorCode.PROMOTION_ARTICLE_NOT_FOUND));
58+
Club club = getClub(request.clubId());
59+
60+
article.update(request, club.getName());
61+
promotionArticleRepository.save(article);
62+
}
63+
64+
private Club getClub(String clubId) {
65+
ObjectId clubObjectId = ObjectIdConverter.convertString(clubId);
66+
return clubRepository.findClubById(clubObjectId)
67+
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
68+
}
5169
}

backend/src/main/java/moadong/global/exception/ErrorCode.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ public enum ErrorCode {
6868
// 901xx: FCM 관련 오류
6969
FCMTOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "901-1", "존재하지 않는 토큰입니다."),
7070
FCMTOKEN_SUBSCRIBE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "901-2", "동아리 구독중에 오류가 발생 하였습니다."),
71-
FCMMESSAGE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "901-3", "FCM 메시지 전송 중 오류가 발생했습니다.");
71+
FCMMESSAGE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "901-3", "FCM 메시지 전송 중 오류가 발생했습니다."),
72+
73+
// 902xx: 홍보게시판 오류
74+
PROMOTION_ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "902-1", "홍보 게시글이 존재하지 않습니다."),
75+
;
7276

7377
private final HttpStatus httpStatus;
7478
private final String code;
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package moadong.club.service;
2+
3+
import moadong.club.entity.Club;
4+
import moadong.club.entity.PromotionArticle;
5+
import moadong.club.payload.request.PromotionArticleUpdateRequest;
6+
import moadong.club.payload.response.PromotionArticleResponse;
7+
import moadong.club.repository.ClubRepository;
8+
import moadong.club.repository.PromotionArticleRepository;
9+
import moadong.global.exception.ErrorCode;
10+
import moadong.global.exception.RestApiException;
11+
import org.bson.types.ObjectId;
12+
import org.junit.jupiter.api.Test;
13+
import org.junit.jupiter.api.extension.ExtendWith;
14+
import org.mockito.InjectMocks;
15+
import org.mockito.Mock;
16+
import org.mockito.junit.jupiter.MockitoExtension;
17+
18+
import java.time.Instant;
19+
import java.util.List;
20+
import java.util.Optional;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertThrows;
24+
import static org.mockito.Mockito.never;
25+
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
27+
28+
@ExtendWith(MockitoExtension.class)
29+
class PromotionArticleServiceTest {
30+
31+
@Mock
32+
private PromotionArticleRepository promotionArticleRepository;
33+
34+
@Mock
35+
private ClubRepository clubRepository;
36+
37+
@InjectMocks
38+
private PromotionArticleService promotionArticleService;
39+
40+
@Test
41+
void 홍보게시글_목록조회시_id를_포함해_반환한다() {
42+
PromotionArticle article = PromotionArticle.builder()
43+
.id("article-1")
44+
.clubId("65f0c1d5a4b1c92f57d51111")
45+
.clubName("모아동")
46+
.title("봄 모집")
47+
.location("서울")
48+
.eventStartDate(Instant.parse("2026-03-01T00:00:00Z"))
49+
.eventEndDate(Instant.parse("2026-03-31T00:00:00Z"))
50+
.description("설명")
51+
.images(List.of("image-1"))
52+
.build();
53+
when(promotionArticleRepository.findAllByOrderByCreatedAtDesc()).thenReturn(List.of(article));
54+
55+
PromotionArticleResponse response = promotionArticleService.getPromotionArticles();
56+
57+
assertEquals(1, response.articles().size());
58+
assertEquals("article-1", response.articles().get(0).id());
59+
assertEquals("모아동", response.articles().get(0).clubName());
60+
}
61+
62+
@Test
63+
void 홍보게시글을_수정한다() {
64+
String clubId = new ObjectId().toHexString();
65+
PromotionArticle article = PromotionArticle.builder()
66+
.id("article-1")
67+
.clubId("old-club-id")
68+
.clubName("이전 동아리")
69+
.title("이전 제목")
70+
.location("이전 장소")
71+
.eventStartDate(Instant.parse("2026-03-01T00:00:00Z"))
72+
.eventEndDate(Instant.parse("2026-03-02T00:00:00Z"))
73+
.description("이전 설명")
74+
.images(List.of("old-image"))
75+
.build();
76+
PromotionArticleUpdateRequest request = new PromotionArticleUpdateRequest(
77+
clubId,
78+
"수정 제목",
79+
"수정 장소",
80+
Instant.parse("2026-04-01T00:00:00Z"),
81+
Instant.parse("2026-04-10T00:00:00Z"),
82+
"수정 설명",
83+
List.of("new-image-1", "new-image-2")
84+
);
85+
when(promotionArticleRepository.findById("article-1")).thenReturn(Optional.of(article));
86+
when(clubRepository.findClubById(new ObjectId(clubId))).thenReturn(Optional.of(Club.builder()
87+
.name("수정 동아리")
88+
.category("category")
89+
.division("division")
90+
.userId("user-1")
91+
.build()));
92+
93+
promotionArticleService.updatePromotionArticle("article-1", request);
94+
95+
assertEquals(clubId, article.getClubId());
96+
assertEquals("수정 동아리", article.getClubName());
97+
assertEquals("수정 제목", article.getTitle());
98+
assertEquals("수정 장소", article.getLocation());
99+
assertEquals(Instant.parse("2026-04-01T00:00:00Z"), article.getEventStartDate());
100+
assertEquals(Instant.parse("2026-04-10T00:00:00Z"), article.getEventEndDate());
101+
assertEquals("수정 설명", article.getDescription());
102+
assertEquals(List.of("new-image-1", "new-image-2"), article.getImages());
103+
verify(promotionArticleRepository).save(article);
104+
}
105+
106+
@Test
107+
void 존재하지_않는_홍보게시글이면_예외를_던진다() {
108+
PromotionArticleUpdateRequest request = new PromotionArticleUpdateRequest(
109+
new ObjectId().toHexString(),
110+
"수정 제목",
111+
"수정 장소",
112+
Instant.parse("2026-04-01T00:00:00Z"),
113+
Instant.parse("2026-04-10T00:00:00Z"),
114+
"수정 설명",
115+
List.of("new-image")
116+
);
117+
when(promotionArticleRepository.findById("missing-article")).thenReturn(Optional.empty());
118+
119+
RestApiException exception = assertThrows(RestApiException.class,
120+
() -> promotionArticleService.updatePromotionArticle("missing-article", request));
121+
122+
assertEquals(ErrorCode.PROMOTION_ARTICLE_NOT_FOUND, exception.getErrorCode());
123+
verify(clubRepository, never()).findClubById(org.mockito.ArgumentMatchers.any());
124+
}
125+
}

0 commit comments

Comments
 (0)