Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ dependencies {
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation 'org.assertj:assertj-core'

// testcontainers
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:mysql:1.21.4'
testImplementation 'org.testcontainers:junit-jupiter:1.21.4'
testImplementation 'com.redis:testcontainers-redis:2.2.4'

// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import umc.cockple.demo.domain.member.domain.MemberExercise;
import umc.cockple.demo.domain.member.enums.MemberStatus;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -35,4 +36,17 @@ List<MemberExercise> findByExerciseIdWithMemberAndProfile(
"where me.member.id = :memberId and me.exercise.id in :exerciseIds")
List<Long> findAllExerciseIdsByMemberAndExerciseIds(@Param("memberId") Long memberId,
@Param("exerciseIds") List<Long> exerciseIds);

@Query("""
SELECT me.member.id, MAX(e.date)
FROM MemberExercise me
JOIN me.exercise e
WHERE me.member.id IN :memberIds
AND e.party.id = :partyId
AND e.date <= CURRENT_DATE
GROUP BY me.member.id
""")
List<Object[]> findLastExerciseDateByMemberIdsAndPartyId(
@Param("memberIds") List<Long> memberIds,
@Param("partyId") Long partyId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import umc.cockple.demo.domain.party.enums.ParticipationType;
import umc.cockple.demo.domain.party.enums.RequestStatus;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Component
Expand Down Expand Up @@ -161,7 +163,7 @@ private List<String> getLevelList(Party party, Gender gender) {
return levelList.isEmpty() ? null : levelList;
}

public PartyMemberDTO.Response toPartyMemberDTO(List<MemberParty> memberParties, Long currentMemberId) {
public PartyMemberDTO.Response toPartyMemberDTO(List<MemberParty> memberParties, Long currentMemberId, Map<Long, LocalDate> lastExerciseDateMap) {
//멤버 리스트 생성
List<PartyMemberDTO.MemberDetail> memberDetails = memberParties.stream()
.map(mp -> {
Expand All @@ -174,6 +176,7 @@ public PartyMemberDTO.Response toPartyMemberDTO(List<MemberParty> memberParties,
.gender(member.getGender().name())
.level(member.getLevel().getKoreanName())
.isMe(member.getId().equals(currentMemberId))
.lastExerciseDate(lastExerciseDateMap.get(member.getId()))
.build();
})
//Role에 따라 정렬 (모임장, 부모임장이 위로 가도록 정렬)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.Builder;

import java.time.LocalDate;
import java.util.List;

public class PartyMemberDTO {
Expand All @@ -26,6 +27,7 @@ public record MemberDetail(
String role,
String gender,
String level,
Boolean isMe
Boolean isMe,
LocalDate lastExerciseDate
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import umc.cockple.demo.domain.member.exception.MemberErrorCode;
import umc.cockple.demo.domain.member.exception.MemberException;
import umc.cockple.demo.domain.member.repository.MemberAddrRepository;
import umc.cockple.demo.domain.member.repository.MemberExerciseRepository;
import umc.cockple.demo.domain.member.repository.MemberPartyRepository;
import umc.cockple.demo.domain.member.repository.MemberRepository;
import umc.cockple.demo.domain.party.converter.PartyConverter;
Expand All @@ -30,6 +31,7 @@
import umc.cockple.demo.domain.party.repository.PartyRepository;
import umc.cockple.demo.global.enums.*;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
Expand All @@ -48,6 +50,7 @@ public class PartyQueryServiceImpl implements PartyQueryService{
private final MemberPartyRepository memberPartyRepository;
private final MemberAddrRepository memberAddrRepository;
private final ExerciseRepository exerciseRepository;
private final MemberExerciseRepository memberExerciseRepository;
private final PartyBookmarkRepository partyBookmarkRepository;
private final ImageService imageService;

Expand Down Expand Up @@ -133,8 +136,11 @@ public PartyMemberDTO.Response getPartyMembers(Long partyId, Long currentMemberI
//모임 멤버 목록 조회
List<MemberParty> memberParties = memberPartyRepository.findAllByPartyIdWithMember(partyId);

//멤버별 마지막 운동일 조회
Map<Long, LocalDate> lastExerciseDateMap = getLastExerciseDateMap(memberParties, partyId);

log.info("모임 멤버 목록 조회 완료 - partyId: {}", partyId);
return partyConverter.toPartyMemberDTO(memberParties, currentMemberId);
return partyConverter.toPartyMemberDTO(memberParties, currentMemberId, lastExerciseDateMap);
}

@Override
Expand Down Expand Up @@ -216,6 +222,15 @@ private Member findMemberOrThrow(Long memberId) {
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
}

//멤버별 마지막 운동일 조회
private Map<Long, LocalDate> getLastExerciseDateMap(List<MemberParty> memberParties, Long partyId) {
List<Long> memberIds = memberParties.stream().map(mp -> mp.getMember().getId()).toList();
return memberExerciseRepository
.findLastExerciseDateByMemberIdsAndPartyId(memberIds, partyId)
.stream()
.collect(Collectors.toMap(row -> (Long) row[0], row -> (LocalDate) row[1]));
}

//운동 정보 조회
private ExerciseInfo getExerciseInfo(List<Long> partyIds) {
//운동 개수 정보 조회
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package umc.cockple.demo.domain.party.integration;

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import umc.cockple.demo.domain.exercise.domain.Exercise;
import umc.cockple.demo.domain.exercise.repository.ExerciseRepository;
import umc.cockple.demo.domain.member.domain.Member;
import umc.cockple.demo.domain.member.repository.MemberExerciseRepository;
import umc.cockple.demo.domain.member.repository.MemberPartyRepository;
import umc.cockple.demo.domain.member.repository.MemberRepository;
import umc.cockple.demo.domain.party.domain.Party;
import umc.cockple.demo.domain.party.domain.PartyAddr;
import umc.cockple.demo.domain.party.exception.PartyErrorCode;
import umc.cockple.demo.domain.party.repository.PartyAddrRepository;
import umc.cockple.demo.domain.party.repository.PartyRepository;
import umc.cockple.demo.global.enums.Gender;
import umc.cockple.demo.global.enums.Level;
import umc.cockple.demo.global.enums.Role;
import umc.cockple.demo.support.IntegrationTestBase;
import umc.cockple.demo.support.SecurityContextHelper;
import umc.cockple.demo.support.fixture.ExerciseFixture;
import umc.cockple.demo.support.fixture.MemberFixture;
import umc.cockple.demo.support.fixture.PartyFixture;

import java.time.LocalDate;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class PartyIntegrationTest extends IntegrationTestBase {

@Autowired MockMvc mockMvc;
@Autowired MemberRepository memberRepository;
@Autowired PartyRepository partyRepository;
@Autowired MemberPartyRepository memberPartyRepository;
@Autowired PartyAddrRepository partyAddrRepository;
@Autowired ExerciseRepository exerciseRepository;
@Autowired MemberExerciseRepository memberExerciseRepository;

private Member manager;
private Member normalMember;
private Party party;

@BeforeEach
void setUp() {
manager = memberRepository.save(MemberFixture.createMember("매니저", Gender.MALE, Level.A, 1001L));
normalMember = memberRepository.save(MemberFixture.createMember("일반멤버", Gender.FEMALE, Level.B, 1002L));

PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울특별시", "강남구"));
party = partyRepository.save(PartyFixture.createParty("테스트 모임", manager.getId(), addr));

memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.party_MANAGER));
memberPartyRepository.save(MemberFixture.createMemberParty(party, normalMember, Role.party_MEMBER));

SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname());
}

@AfterEach
void tearDown() {
memberExerciseRepository.deleteAll();
exerciseRepository.deleteAll();
memberPartyRepository.deleteAll();
partyRepository.deleteAll();
partyAddrRepository.deleteAll();
memberRepository.deleteAll();
}

@Nested
@DisplayName("GET /api/parties/{partyId}/members - 모임 멤버 조회")
class GetPartyMembers {

@Test
@DisplayName("200 - 멤버 목록과 마지막 운동일을 정상 반환한다")
void success_withLastExerciseDate() throws Exception {
Exercise exercise = exerciseRepository.save(
ExerciseFixture.createExercise(party, LocalDate.of(2025, 1, 10)));
memberExerciseRepository.save(MemberFixture.createMemberExercise(normalMember, exercise));

mockMvc.perform(get("/api/parties/{partyId}/members", party.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.summary.totalCount").value(2))
.andExpect(jsonPath("$.data.summary.maleCount").value(1))
.andExpect(jsonPath("$.data.summary.femaleCount").value(1))
// 첫 번째 멤버(매니저) 전체 필드 검증
.andExpect(jsonPath("$.data.members[0].memberId").value(manager.getId()))
.andExpect(jsonPath("$.data.members[0].nickname").value("매니저"))
.andExpect(jsonPath("$.data.members[0].profileImageUrl").doesNotExist())
.andExpect(jsonPath("$.data.members[0].role").value("party_MANAGER"))
.andExpect(jsonPath("$.data.members[0].gender").value("MALE"))
.andExpect(jsonPath("$.data.members[0].level").value("A조"))
.andExpect(jsonPath("$.data.members[0].isMe").value(true))
.andExpect(jsonPath("$.data.members[0].lastExerciseDate").doesNotExist())
// 두 번째 멤버(일반멤버) 마지막 운동일 검증
.andExpect(jsonPath("$.data.members[1].lastExerciseDate").value("2025-01-10"));
}

@Test
@DisplayName("200 - 운동 기록이 없는 멤버의 lastExerciseDate는 null이다")
void success_noExerciseHistory() throws Exception {
mockMvc.perform(get("/api/parties/{partyId}/members", party.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.summary.totalCount").value(2))
.andExpect(jsonPath("$.data.members[0].lastExerciseDate").isEmpty())
.andExpect(jsonPath("$.data.members[1].lastExerciseDate").isEmpty());
}

@Test
@DisplayName("404 - 존재하지 않는 파티면 에러를 반환한다")
void fail_partyNotFound() throws Exception {
mockMvc.perform(get("/api/parties/{partyId}/members", 999L))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode()))
.andExpect(jsonPath("$.message").value(PartyErrorCode.PARTY_NOT_FOUND.getMessage()));
}

@Test
@DisplayName("400 - 비활성화된 파티면 에러를 반환한다")
void fail_partyInactive() throws Exception {
party.delete();
partyRepository.save(party);

mockMvc.perform(get("/api/parties/{partyId}/members", party.getId()))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_IS_DELETED.getCode()))
.andExpect(jsonPath("$.message").value(PartyErrorCode.PARTY_IS_DELETED.getMessage()));
}
}
}
Loading