diff --git a/backend/build.gradle b/backend/build.gradle index 724b0111a..d807dc00b 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -63,6 +63,7 @@ dependencies { implementation 'com.google.firebase:firebase-admin:9.7.0' + implementation 'org.javers:javers-spring-boot-starter-mongo:7.10.0' } //전체 테스트 diff --git a/backend/src/main/java/moadong/club/entity/Club.java b/backend/src/main/java/moadong/club/entity/Club.java index 4e81a5333..8e01e4174 100644 --- a/backend/src/main/java/moadong/club/entity/Club.java +++ b/backend/src/main/java/moadong/club/entity/Club.java @@ -13,6 +13,7 @@ import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest; import moadong.global.exception.ErrorCode; import moadong.global.exception.RestApiException; +import org.javers.core.metamodel.annotation.DiffIgnore; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; import org.springframework.data.domain.Persistable; @@ -41,6 +42,7 @@ public class Club implements Persistable { private String userId; + @DiffIgnore private Map socialLinks; @Field("recruitmentInformation") @@ -146,9 +148,11 @@ public void updateRecruitmentStatus(ClubRecruitmentStatus clubRecruitmentStatus) public void sendPushNotification(Message message) { try { - FirebaseMessaging.getInstance().send(message); + log.info("FCM 알림 전송 시작 - clubId: {}, clubName: {}", this.id, this.name); + String messageId = FirebaseMessaging.getInstance().send(message); + log.info("FCM 알림 전송 성공 - clubId: {}, messageId: {}", this.id, messageId); } catch (FirebaseMessagingException e) { - log.error("FirebaseSendNotificationError: {}", e.getMessage()); + log.error("FCM 알림 전송 실패 - clubId: {}, error: {}", this.id, e.getMessage()); } } diff --git a/backend/src/main/java/moadong/club/entity/ClubRecruitmentInformation.java b/backend/src/main/java/moadong/club/entity/ClubRecruitmentInformation.java index 819df38e2..104fe8ba2 100644 --- a/backend/src/main/java/moadong/club/entity/ClubRecruitmentInformation.java +++ b/backend/src/main/java/moadong/club/entity/ClubRecruitmentInformation.java @@ -14,6 +14,7 @@ import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest; import moadong.global.RegexConstants; import org.checkerframework.common.aliasing.qual.Unique; +import org.javers.core.metamodel.annotation.DiffIgnore; import java.time.Instant; import java.time.LocalDateTime; @@ -26,15 +27,17 @@ @Builder(toBuilder = true) public class ClubRecruitmentInformation { - @Id - private String id; +// @Id +// private String id; @Column(length = 1024) @Unique + @DiffIgnore private String logo; @Column(length = 1024) @Unique + @DiffIgnore private String cover; @Column(length = 30) @@ -45,6 +48,7 @@ public class ClubRecruitmentInformation { @Pattern(regexp = RegexConstants.PHONE_NUMBER, message = "전화번호 형식이 올바르지 않습니다.") @Column(length = 13) + @DiffIgnore private String presidentTelephoneNumber; private Instant recruitmentStart; @@ -53,16 +57,20 @@ public class ClubRecruitmentInformation { private String recruitmentTarget; + @DiffIgnore String externalApplicationUrl; + @DiffIgnore private List feedImages; private List tags; @Enumerated(EnumType.STRING) @NotNull + @DiffIgnore private ClubRecruitmentStatus clubRecruitmentStatus; + @DiffIgnore private LocalDateTime lastModifiedDate; public void updateLogo(String logo) { diff --git a/backend/src/main/java/moadong/club/repository/ClubRepository.java b/backend/src/main/java/moadong/club/repository/ClubRepository.java index ab40067bf..2b961dd45 100644 --- a/backend/src/main/java/moadong/club/repository/ClubRepository.java +++ b/backend/src/main/java/moadong/club/repository/ClubRepository.java @@ -4,6 +4,7 @@ import java.util.Optional; import moadong.club.entity.Club; import org.bson.types.ObjectId; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; diff --git a/backend/src/main/java/moadong/club/service/ClubProfileService.java b/backend/src/main/java/moadong/club/service/ClubProfileService.java index 5116ecba9..5fa2b54b7 100644 --- a/backend/src/main/java/moadong/club/service/ClubProfileService.java +++ b/backend/src/main/java/moadong/club/service/ClubProfileService.java @@ -14,6 +14,7 @@ import moadong.global.util.ObjectIdConverter; import moadong.user.payload.CustomUserDetails; import org.bson.types.ObjectId; +import org.javers.core.Javers; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,13 +24,16 @@ public class ClubProfileService { private final ClubRepository clubRepository; private final ClubSearchRepository clubSearchRepository; + private final RecruitmentStateCalculator recruitmentStateCalculator; + private final Javers javers; @Transactional public void updateClubInfo(ClubInfoRequest request, CustomUserDetails user) { Club club = clubRepository.findClubByUserId(user.getId()) .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); club.update(request); - clubRepository.save(club); + Club saved = clubRepository.save(club); + javers.commit(user.getUsername(), saved); } public void updateClubRecruitmentInfo(ClubRecruitmentInfoUpdateRequest request, @@ -37,13 +41,14 @@ public void updateClubRecruitmentInfo(ClubRecruitmentInfoUpdateRequest request, Club club = clubRepository.findClubByUserId(user.getId()) .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); club.update(request); - RecruitmentStateCalculator.calculate( + recruitmentStateCalculator.calculate( club, club.getClubRecruitmentInformation().getRecruitmentStart(), club.getClubRecruitmentInformation().getRecruitmentEnd() ); club.getClubRecruitmentInformation().updateLastModifiedDate(); - clubRepository.save(club); + Club saved = clubRepository.save(club); + javers.commit(user.getUsername(), saved); } public ClubDetailedResponse getClubDetail(String clubId) { @@ -57,3 +62,4 @@ public ClubDetailedResponse getClubDetail(String clubId) { return new ClubDetailedResponse(clubDetailedResult); } } + diff --git a/backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java b/backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java index 151616fbb..9f53a0690 100644 --- a/backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java +++ b/backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java @@ -21,6 +21,7 @@ public class RecruitmentStateChecker { private final ClubRepository clubRepository; + private final RecruitmentStateCalculator recruitmentStateCalculator; @Scheduled(fixedRate = 60 * 60 * 1000) // 1시간마다 실행 public void performTask() { @@ -32,9 +33,10 @@ public void performTask() { if (recruitInfo.getClubRecruitmentStatus() == ClubRecruitmentStatus.ALWAYS) { continue; } - RecruitmentStateCalculator.calculate(club, recruitmentStartDate, recruitmentEndDate); + recruitmentStateCalculator.calculate(club, recruitmentStartDate, recruitmentEndDate); clubRepository.save(club); } } } + diff --git a/backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java b/backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java index 8e4b89210..d8f295060 100644 --- a/backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java +++ b/backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java @@ -8,14 +8,21 @@ import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; +import lombok.RequiredArgsConstructor; import moadong.club.entity.Club; import moadong.club.entity.ClubRecruitmentInformation; import moadong.club.enums.ClubRecruitmentStatus; +import moadong.fcm.util.FcmTopicResolver; +import org.springframework.stereotype.Component; +@Component +@RequiredArgsConstructor public class RecruitmentStateCalculator { public static final int ALWAYS_RECRUIT_YEAR = 2999; - public static void calculate(Club club, ZonedDateTime recruitmentStartDate, ZonedDateTime recruitmentEndDate) { + private final FcmTopicResolver fcmTopicResolver; + + public void calculate(Club club, ZonedDateTime recruitmentStartDate, ZonedDateTime recruitmentEndDate) { ClubRecruitmentStatus oldStatus = club.getClubRecruitmentInformation().getClubRecruitmentStatus(); ClubRecruitmentStatus newStatus = calculateRecruitmentStatus(recruitmentStartDate, recruitmentEndDate); club.updateRecruitmentStatus(newStatus); @@ -50,7 +57,7 @@ public static ClubRecruitmentStatus calculateRecruitmentStatus(ZonedDateTime rec return ClubRecruitmentStatus.CLOSED; } - public static Message buildRecruitmentMessage(Club club, ClubRecruitmentStatus status) { + public Message buildRecruitmentMessage(Club club, ClubRecruitmentStatus status) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M월 d일 a h시 m분", Locale.KOREAN); ClubRecruitmentInformation info = club.getClubRecruitmentInformation(); @@ -72,7 +79,8 @@ public static Message buildRecruitmentMessage(Club club, ClubRecruitmentStatus s .setTitle(club.getName()) .setBody(bodyMessage) .build()) - .setTopic(club.getId()) + .setTopic(fcmTopicResolver.resolveTopic(club.getId())) .build(); } } + diff --git a/backend/src/main/java/moadong/fcm/service/FcmAsyncService.java b/backend/src/main/java/moadong/fcm/service/FcmAsyncService.java index 064fa51c7..404e6562a 100644 --- a/backend/src/main/java/moadong/fcm/service/FcmAsyncService.java +++ b/backend/src/main/java/moadong/fcm/service/FcmAsyncService.java @@ -6,6 +6,7 @@ import com.google.firebase.messaging.TopicManagementResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import moadong.fcm.util.FcmTopicResolver; import moadong.global.exception.ErrorCode; import moadong.global.exception.RestApiException; import org.springframework.beans.factory.annotation.Value; @@ -30,6 +31,8 @@ public class FcmAsyncService { private final FirebaseMessaging firebaseMessaging; + private final FcmTopicResolver fcmTopicResolver; + @Value("${fcm.topic.timeout-seconds:5}") private int timeoutSeconds; @@ -40,14 +43,16 @@ public CompletableFuture updateSubscriptions(String token, Set new // 새로운 동아리 구독 if (!clubsToSubscribe.isEmpty()) { for (String clubId : clubsToSubscribe) { - futures.add(firebaseMessaging.subscribeToTopicAsync(Collections.singletonList(token), clubId)); + String topic = fcmTopicResolver.resolveTopic(clubId); + futures.add(firebaseMessaging.subscribeToTopicAsync(Collections.singletonList(token), topic)); } } // 더 이상 구독하지 않는 동아리 구독 해제 if (!clubsToUnsubscribe.isEmpty()) { for (String clubId : clubsToUnsubscribe) { - futures.add(firebaseMessaging.unsubscribeFromTopicAsync(Collections.singletonList(token), clubId)); + String topic = fcmTopicResolver.resolveTopic(clubId); + futures.add(firebaseMessaging.unsubscribeFromTopicAsync(Collections.singletonList(token), topic)); } } @@ -84,3 +89,4 @@ public CompletableFuture updateSubscriptions(String token, Set new return CompletableFuture.completedFuture(null); } } + diff --git a/backend/src/main/java/moadong/fcm/util/FcmTopicResolver.java b/backend/src/main/java/moadong/fcm/util/FcmTopicResolver.java new file mode 100644 index 000000000..81781913e --- /dev/null +++ b/backend/src/main/java/moadong/fcm/util/FcmTopicResolver.java @@ -0,0 +1,18 @@ +package moadong.fcm.util; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class FcmTopicResolver { + + @Value("${spring.profiles.active:prod}") + private String activeProfile; + + public String resolveTopic(String clubId) { + if ("prod".equals(activeProfile)) { + return clubId; + } + return activeProfile + "_" + clubId; + } +} diff --git a/backend/src/main/java/moadong/global/config/JaversConfig.java b/backend/src/main/java/moadong/global/config/JaversConfig.java new file mode 100644 index 000000000..32c7e918a --- /dev/null +++ b/backend/src/main/java/moadong/global/config/JaversConfig.java @@ -0,0 +1,27 @@ +package moadong.global.config; + +import org.javers.core.Javers; +import org.javers.core.JaversBuilder; +import org.javers.repository.mongo.MongoRepository; +import org.javers.spring.auditable.AuthorProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class JaversConfig { + @Bean + public AuthorProvider authorProvider() { + return () -> { + return SecurityContextHolder.getContext().getAuthentication().getName(); + }; + } + + @Bean + public MongoRepository javersMongoRepository(MongoDatabaseFactory dbFactory) { + // MongoDatabaseFactory에서 database 객체를 꺼내서 Javers에 넘김 + return new MongoRepository(dbFactory.getMongoDatabase()); + } +} diff --git a/backend/src/main/java/moadong/log/club/controller/ClubHistoryController.java b/backend/src/main/java/moadong/log/club/controller/ClubHistoryController.java new file mode 100644 index 000000000..bbebe2862 --- /dev/null +++ b/backend/src/main/java/moadong/log/club/controller/ClubHistoryController.java @@ -0,0 +1,25 @@ +package moadong.log.club.controller; + +import lombok.RequiredArgsConstructor; +import moadong.log.club.payload.response.ClubHistoryResponse; +import moadong.log.club.service.ClubHistoryService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/clubs") +@RequiredArgsConstructor +public class ClubHistoryController { + private final ClubHistoryService clubHistoryService; + + @GetMapping("/{clubId}/histories") + public ResponseEntity> getClubHistories(@PathVariable String clubId) { + List histories = clubHistoryService.getClubHistories(clubId); + return ResponseEntity.ok(histories); + } +} diff --git a/backend/src/main/java/moadong/log/club/payload/response/ClubHistoryResponse.java b/backend/src/main/java/moadong/log/club/payload/response/ClubHistoryResponse.java new file mode 100644 index 000000000..ff9d9d548 --- /dev/null +++ b/backend/src/main/java/moadong/log/club/payload/response/ClubHistoryResponse.java @@ -0,0 +1,79 @@ +package moadong.log.club.payload.response; + +import lombok.Builder; +import lombok.Getter; +import moadong.club.entity.Club; +import moadong.club.enums.ClubRecruitmentStatus; +import org.javers.shadow.Shadow; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +@Getter +@Builder +public class ClubHistoryResponse { + // 1. 메타데이터 (버전 정보) + private long version; + private LocalDateTime modifiedAt; + private String modifiedBy; + + // 2. Club 엔티티 주요 데이터 + private String name; + private String division; + private String category; + private String state; // Enum -> String 변환 권장 + + // 3. ClubRecruitmentInformation 주요 데이터 (DiffIgnore 제외된 것들) + private String introduction; + private String presidentName; + private String recruitmentTarget; + private ClubRecruitmentStatus recruitmentStatus; + private LocalDateTime recruitmentStart; + private LocalDateTime recruitmentEnd; + + // tags는 DiffIgnore가 없으므로 포함 + private List tags; + + // 4. 안전한 변환 메서드 (Factory Method) + public static ClubHistoryResponse from(Shadow shadow) { + Club club = shadow.get(); + var metadata = shadow.getCommitMetadata(); + + // 중첩 객체 Null 방어 로직 + var recruitmentInfo = club.getClubRecruitmentInformation(); + boolean hasInfo = recruitmentInfo != null; + + return ClubHistoryResponse.builder() + .version(metadata.getId().getMajorId()) + .modifiedAt(metadata.getCommitDate()) + .modifiedBy(metadata.getAuthor()) + + // Club 필드 매핑 + .name(club.getName()) + .division(club.getDivision()) + .category(club.getCategory()) + .state(club.getState() != null ? club.getState().name() : null) + + // RecruitmentInfo 필드 매핑 (Null Safe) + .introduction(hasInfo ? recruitmentInfo.getIntroduction() : null) + .presidentName(hasInfo ? recruitmentInfo.getPresidentName() : null) + .recruitmentTarget(hasInfo ? recruitmentInfo.getRecruitmentTarget() : null) + .recruitmentStatus(hasInfo ? recruitmentInfo.getClubRecruitmentStatus() : null) + + // 시간 타입 변환 (Instant -> LocalDateTime 등 필요시) + .recruitmentStart(hasInfo && recruitmentInfo.getRecruitmentStart() != null + ? recruitmentInfo.getRecruitmentStart().toLocalDateTime() + : null) + .recruitmentEnd(hasInfo && recruitmentInfo.getRecruitmentEnd() != null + ? recruitmentInfo.getRecruitmentEnd().toLocalDateTime() + : null) + + // 리스트 필드 Null 방어 + .tags(hasInfo && recruitmentInfo.getTags() != null + ? recruitmentInfo.getTags() + : Collections.emptyList()) + + .build(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/moadong/log/club/service/ClubHistoryService.java b/backend/src/main/java/moadong/log/club/service/ClubHistoryService.java new file mode 100644 index 000000000..56e2c1060 --- /dev/null +++ b/backend/src/main/java/moadong/log/club/service/ClubHistoryService.java @@ -0,0 +1,40 @@ +package moadong.log.club.service; + +import lombok.RequiredArgsConstructor; +import moadong.club.entity.Club; +import moadong.log.club.payload.response.ClubHistoryResponse; +import org.javers.core.Javers; +import org.javers.repository.jql.JqlQuery; +import org.javers.repository.jql.QueryBuilder; +import org.javers.shadow.Shadow; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ClubHistoryService { + + private final Javers javers; + + public List getClubHistories(String clubId) { + JqlQuery query = QueryBuilder.byInstanceId(clubId, Club.class) + .withChildValueObjects() + .limit(20) + .build(); + + List> shadows = findClubShadows(query); + + return shadows.stream() + .map(ClubHistoryResponse::from) + .toList(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private List> findClubShadows(JqlQuery query) { + return (List) javers.findShadows(query); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/moadong/club/service/ClubProfileServiceDateTest.java b/backend/src/test/java/moadong/club/service/ClubProfileServiceDateTest.java index 5650b77bd..4e6faf57b 100644 --- a/backend/src/test/java/moadong/club/service/ClubProfileServiceDateTest.java +++ b/backend/src/test/java/moadong/club/service/ClubProfileServiceDateTest.java @@ -9,13 +9,12 @@ import moadong.fixture.UserFixture; import moadong.user.payload.CustomUserDetails; import moadong.util.annotations.UnitTest; +import org.javers.core.Javers; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; @@ -26,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -41,6 +41,12 @@ public class ClubProfileServiceDateTest { @Mock ClubSearchRepository clubSearchRepository; + @Mock + RecruitmentStateCalculator recruitmentStateCalculator; + + @Mock + Javers javers; + @DisplayName("모집글 수정 시 최근 업데이트 일자를 보여준다") @Test void 모집글_수정_시_최근_업데이트_일자를_보여줘야한다(){ @@ -49,27 +55,19 @@ public class ClubProfileServiceDateTest { CustomUserDetails customUserDetails = UserFixture.createUserDetails("test"); Club club = new Club(); when(clubRepository.findClubByUserId(any())).thenReturn(Optional.of(club)); - //updateClubRecruitmentInfo의 RecruitmentStateCalculator 무시 - try (var mocked = Mockito.mockStatic(RecruitmentStateCalculator.class)) { - mocked.when(() -> - RecruitmentStateCalculator.calculate( - Mockito.any(moadong.club.entity.Club.class), - Mockito.any(java.time.ZonedDateTime.class), - Mockito.any(java.time.ZonedDateTime.class) - ) - ).thenAnswer(inv -> null); + doNothing().when(recruitmentStateCalculator).calculate(any(), any(), any()); - //WHEN - clubProfileService.updateClubRecruitmentInfo(request, customUserDetails); + //WHEN + clubProfileService.updateClubRecruitmentInfo(request, customUserDetails); - //THEN - assertNotNull(club.getClubRecruitmentInformation().getLastModifiedDate()); - //1초 전후 차이로 살펴보기 - LocalDateTime now = LocalDateTime.now(); - assertTrue(club.getClubRecruitmentInformation(). - getLastModifiedDate().isAfter(now.minusSeconds(1))); - assertTrue(club.getClubRecruitmentInformation(). - getLastModifiedDate().isBefore(now.plusSeconds(1))); - } + //THEN + assertNotNull(club.getClubRecruitmentInformation().getLastModifiedDate()); + //1초 전후 차이로 살펴보기 + LocalDateTime now = LocalDateTime.now(); + assertTrue(club.getClubRecruitmentInformation(). + getLastModifiedDate().isAfter(now.minusSeconds(1))); + assertTrue(club.getClubRecruitmentInformation(). + getLastModifiedDate().isBefore(now.plusSeconds(1))); } } + diff --git a/backend/src/test/java/moadong/club/service/RecruitmentStateCheckerTest.java b/backend/src/test/java/moadong/club/service/RecruitmentStateCheckerTest.java index 6af038e24..9db389915 100644 --- a/backend/src/test/java/moadong/club/service/RecruitmentStateCheckerTest.java +++ b/backend/src/test/java/moadong/club/service/RecruitmentStateCheckerTest.java @@ -1,6 +1,7 @@ package moadong.club.service; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -13,6 +14,7 @@ import moadong.club.entity.ClubRecruitmentInformation; import moadong.club.enums.ClubRecruitmentStatus; import moadong.club.repository.ClubRepository; +import moadong.club.util.RecruitmentStateCalculator; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -28,6 +30,9 @@ public class RecruitmentStateCheckerTest { @Mock private ClubRepository clubRepository; + @Mock + private RecruitmentStateCalculator recruitmentStateCalculator; + static final ZonedDateTime NOW = ZonedDateTime.now(ZoneId.of("Asia/Seoul")); @Test @@ -42,19 +47,18 @@ public class RecruitmentStateCheckerTest { recruitmentStateChecker.performTask(); - verify(club, never()).updateRecruitmentStatus(any()); + verify(recruitmentStateCalculator, never()).calculate(any(), any(), any()); verify(clubRepository, never()).save(club); } @Test - void 모집시작전_14일이내면_UPCOMING() { + void 모집시작전_14일이내면_calculate호출() { Club club = mock(Club.class); ClubRecruitmentInformation info = mock(ClubRecruitmentInformation.class); ZonedDateTime start = NOW.plusDays(10); ZonedDateTime end = NOW.plusDays(20); - when(club.getId()).thenReturn("1"); when(club.getClubRecruitmentInformation()).thenReturn(info); when(info.getClubRecruitmentStatus()).thenReturn(ClubRecruitmentStatus.CLOSED); when(info.getRecruitmentStart()).thenReturn(start); @@ -63,19 +67,18 @@ public class RecruitmentStateCheckerTest { recruitmentStateChecker.performTask(); - verify(club).updateRecruitmentStatus(ClubRecruitmentStatus.UPCOMING); + verify(recruitmentStateCalculator).calculate(eq(club), eq(start), eq(end)); verify(clubRepository).save(club); } @Test - void 모집기간중이면_OPEN() { + void 모집기간중이면_calculate호출() { Club club = mock(Club.class); ClubRecruitmentInformation info = mock(ClubRecruitmentInformation.class); ZonedDateTime start = NOW.minusDays(1); ZonedDateTime end = NOW.plusDays(5); - when(club.getId()).thenReturn("1"); when(club.getClubRecruitmentInformation()).thenReturn(info); when(info.getClubRecruitmentStatus()).thenReturn(ClubRecruitmentStatus.CLOSED); when(info.getRecruitmentStart()).thenReturn(start); @@ -84,19 +87,18 @@ public class RecruitmentStateCheckerTest { recruitmentStateChecker.performTask(); - verify(club).updateRecruitmentStatus(ClubRecruitmentStatus.OPEN); + verify(recruitmentStateCalculator).calculate(eq(club), eq(start), eq(end)); verify(clubRepository).save(club); } @Test - void 모집마감_이후면_CLOSED() { + void 모집마감_이후면_calculate호출() { Club club = mock(Club.class); ClubRecruitmentInformation info = mock(ClubRecruitmentInformation.class); ZonedDateTime start = NOW.minusDays(10); ZonedDateTime end = NOW.minusDays(1); - when(club.getId()).thenReturn("1"); when(club.getClubRecruitmentInformation()).thenReturn(info); when(info.getClubRecruitmentStatus()).thenReturn(ClubRecruitmentStatus.OPEN); when(info.getRecruitmentStart()).thenReturn(start); @@ -105,16 +107,15 @@ public class RecruitmentStateCheckerTest { recruitmentStateChecker.performTask(); - verify(club).updateRecruitmentStatus(ClubRecruitmentStatus.CLOSED); + verify(recruitmentStateCalculator).calculate(eq(club), eq(start), eq(end)); verify(clubRepository).save(club); } @Test - void 시작_또는_종료날짜_null이면_CLOSED() { + void 시작_또는_종료날짜_null이면_calculate호출() { Club club = mock(Club.class); ClubRecruitmentInformation info = mock(ClubRecruitmentInformation.class); - when(club.getId()).thenReturn("1"); when(club.getClubRecruitmentInformation()).thenReturn(info); when(info.getClubRecruitmentStatus()).thenReturn(ClubRecruitmentStatus.OPEN); when(info.getRecruitmentStart()).thenReturn(null); @@ -123,7 +124,8 @@ public class RecruitmentStateCheckerTest { recruitmentStateChecker.performTask(); - verify(club).updateRecruitmentStatus(ClubRecruitmentStatus.CLOSED); + verify(recruitmentStateCalculator).calculate(eq(club), eq(null), eq(null)); verify(clubRepository).save(club); } } + diff --git a/backend/src/test/java/moadong/fixture/ClubFixture.java b/backend/src/test/java/moadong/fixture/ClubFixture.java index 0049a3bba..2c828dffc 100644 --- a/backend/src/test/java/moadong/fixture/ClubFixture.java +++ b/backend/src/test/java/moadong/fixture/ClubFixture.java @@ -30,7 +30,7 @@ public static ClubRecruitmentInformation createRecruitmentInfo( List feedImages, ClubRecruitmentStatus clubRecruitmentStatus) { ClubRecruitmentInformation clubRecruitmentInfo = mock(ClubRecruitmentInformation.class); - when(clubRecruitmentInfo.getId()).thenReturn(id); +// when(clubRecruitmentInfo.getId()).thenReturn(id); when(clubRecruitmentInfo.getLogo()).thenReturn(logo); when(clubRecruitmentInfo.getIntroduction()).thenReturn(introduction); when(clubRecruitmentInfo.getPresidentName()).thenReturn(presidentName); diff --git a/backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java b/backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java index 0d253c0c9..c90a6f134 100644 --- a/backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java +++ b/backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java @@ -16,6 +16,7 @@ import moadong.global.exception.RestApiException; import moadong.user.payload.CustomUserDetails; import moadong.util.annotations.UnitTest; +import org.javers.core.Javers; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -29,6 +30,9 @@ public class ClubProfileServiceTest { @InjectMocks private ClubProfileService clubProfileService; + @Mock + Javers javers; + @Test void 정상적으로_클럽_약력을_업데이트한다() { // Given