diff --git a/src/main/java/com/moabam/api/application/MemberService.java b/src/main/java/com/moabam/api/application/MemberService.java index f8e7cc7b..dae4a85f 100644 --- a/src/main/java/com/moabam/api/application/MemberService.java +++ b/src/main/java/com/moabam/api/application/MemberService.java @@ -4,6 +4,7 @@ import static com.moabam.global.error.model.ErrorMessage.*; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import org.apache.commons.lang3.RandomStringUtils; @@ -12,6 +13,7 @@ import com.moabam.api.domain.entity.Member; import com.moabam.api.domain.repository.MemberRepository; +import com.moabam.api.domain.repository.MemberSearchRepository; import com.moabam.api.dto.AuthorizationTokenInfoResponse; import com.moabam.api.dto.LoginResponse; import com.moabam.api.dto.MemberMapper; @@ -25,6 +27,7 @@ public class MemberService { private final MemberRepository memberRepository; + private final MemberSearchRepository memberSearchRepository; public Member getById(Long memberId) { return memberRepository.findById(memberId) @@ -57,4 +60,13 @@ private Member signUp(long socialId) { private String createRandomNickName() { return RandomStringUtils.randomAlphanumeric(RANDOM_NICKNAME_SIZE) + UNDER_BAR + LocalDate.now(); } -} + + public Member getManager(Long roomId) { + return memberSearchRepository.findManager(roomId) + .orElseThrow(() -> new NotFoundException(MEMBER_NOT_FOUND)); + } + + public List getRoomMembers(List memberIds) { + return memberRepository.findAllById(memberIds); + } +} \ No newline at end of file diff --git a/src/main/java/com/moabam/api/application/RoomService.java b/src/main/java/com/moabam/api/application/RoomService.java index 5480ef16..5425f829 100644 --- a/src/main/java/com/moabam/api/application/RoomService.java +++ b/src/main/java/com/moabam/api/application/RoomService.java @@ -3,25 +3,40 @@ import static com.moabam.api.domain.entity.enums.RoomType.*; import static com.moabam.global.error.model.ErrorMessage.*; +import java.time.LocalDate; +import java.time.Period; +import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.moabam.api.domain.entity.Certification; +import com.moabam.api.domain.entity.DailyMemberCertification; +import com.moabam.api.domain.entity.DailyRoomCertification; import com.moabam.api.domain.entity.Member; import com.moabam.api.domain.entity.Participant; import com.moabam.api.domain.entity.Room; import com.moabam.api.domain.entity.Routine; import com.moabam.api.domain.entity.enums.RoomType; +import com.moabam.api.domain.repository.CertificationsMapper; +import com.moabam.api.domain.repository.CertificationsSearchRepository; import com.moabam.api.domain.repository.ParticipantRepository; import com.moabam.api.domain.repository.ParticipantSearchRepository; import com.moabam.api.domain.repository.RoomRepository; import com.moabam.api.domain.repository.RoutineRepository; +import com.moabam.api.domain.repository.RoutineSearchRepository; +import com.moabam.api.dto.CertificationImageResponse; import com.moabam.api.dto.CreateRoomRequest; import com.moabam.api.dto.EnterRoomRequest; import com.moabam.api.dto.ModifyRoomRequest; +import com.moabam.api.dto.RoomDetailsResponse; import com.moabam.api.dto.RoomMapper; +import com.moabam.api.dto.RoutineMapper; +import com.moabam.api.dto.RoutineResponse; +import com.moabam.api.dto.TodayCertificateRankResponse; import com.moabam.global.error.exception.BadRequestException; import com.moabam.global.error.exception.ForbiddenException; import com.moabam.global.error.exception.NotFoundException; @@ -35,14 +50,16 @@ public class RoomService { private final RoomRepository roomRepository; private final RoutineRepository routineRepository; + private final RoutineSearchRepository routineSearchRepository; private final ParticipantRepository participantRepository; private final ParticipantSearchRepository participantSearchRepository; + private final CertificationsSearchRepository certificationsSearchRepository; private final MemberService memberService; @Transactional public void createRoom(Long memberId, CreateRoomRequest createRoomRequest) { Room room = RoomMapper.toRoomEntity(createRoomRequest); - List routines = RoomMapper.toRoutineEntity(room, createRoomRequest.routines()); + List routines = RoutineMapper.toRoutineEntities(room, createRoomRequest.routines()); Participant participant = Participant.builder() .room(room) .memberId(memberId) @@ -62,8 +79,9 @@ public void modifyRoom(Long memberId, Long roomId, ModifyRoomRequest modifyRoomR throw new ForbiddenException(ROOM_MODIFY_UNAUTHORIZED_REQUEST); } - Room room = getRoom(roomId); + Room room = participant.getRoom(); room.changeTitle(modifyRoomRequest.title()); + room.changeAnnouncement(modifyRoomRequest.announcement()); room.changePassword(modifyRoomRequest.password()); room.changeCertifyTime(modifyRoomRequest.certifyTime()); room.changeMaxCount(modifyRoomRequest.maxUserCount()); @@ -71,7 +89,7 @@ public void modifyRoom(Long memberId, Long roomId, ModifyRoomRequest modifyRoomR @Transactional public void enterRoom(Long memberId, Long roomId, EnterRoomRequest enterRoomRequest) { - Room room = getRoom(roomId); + Room room = roomRepository.findById(roomId).orElseThrow(() -> new NotFoundException(ROOM_NOT_FOUND)); validateRoomEnter(memberId, enterRoomRequest.password(), room); room.increaseCurrentUserCount(); @@ -95,7 +113,6 @@ public void exitRoom(Long memberId, Long roomId) { decreaseRoomCount(memberId, room.getRoomType()); participant.removeRoom(); - participantRepository.flush(); participantRepository.delete(participant); if (!participant.isManager()) { @@ -107,6 +124,25 @@ public void exitRoom(Long memberId, Long roomId) { roomRepository.delete(room); } + public RoomDetailsResponse getRoomDetails(Long memberId, Long roomId) { + LocalDate today = LocalDate.now(); + Participant participant = getParticipant(memberId, roomId); + Room room = participant.getRoom(); + + String managerNickname = memberService.getManager(roomId).getNickname(); + List dailyMemberCertifications = + certificationsSearchRepository.findSortedDailyMemberCertifications(roomId, today); + List routineResponses = getRoutineResponses(roomId); + List todayCertificateRankResponses = getTodayCertificateRankResponses(roomId, + routineResponses, dailyMemberCertifications, today); + List certifiedDates = getCertifiedDates(roomId, today); + double completePercentage = calculateCompletePercentage(dailyMemberCertifications.size(), + room.getCurrentUserCount()); + + return RoomMapper.toRoomDetailsResponse(room, managerNickname, routineResponses, certifiedDates, + todayCertificateRankResponses, completePercentage); + } + public void validateRoomById(Long roomId) { if (!roomRepository.existsById(roomId)) { throw new NotFoundException(ROOM_NOT_FOUND); @@ -114,23 +150,17 @@ public void validateRoomById(Long roomId) { } private Participant getParticipant(Long memberId, Long roomId) { - return participantSearchRepository.findParticipant(memberId, roomId) + return participantSearchRepository.findOne(memberId, roomId) .orElseThrow(() -> new NotFoundException(PARTICIPANT_NOT_FOUND)); } - private Room getRoom(Long roomId) { - return roomRepository.findById(roomId).orElseThrow(() -> new NotFoundException(ROOM_NOT_FOUND)); - } - private void validateRoomEnter(Long memberId, String requestPassword, Room room) { if (!isEnterRoomAvailable(memberId, room.getRoomType())) { throw new BadRequestException(MEMBER_ROOM_EXCEED); } - if (!StringUtils.isEmpty(requestPassword) && !room.getPassword().equals(requestPassword)) { throw new BadRequestException(WRONG_ROOM_PASSWORD); } - if (room.getCurrentUserCount() == room.getMaxUserCount()) { throw new BadRequestException(ROOM_MAX_USER_REACHED); } @@ -142,7 +172,6 @@ private boolean isEnterRoomAvailable(Long memberId, RoomType roomType) { if (roomType.equals(MORNING) && member.getCurrentMorningCount() >= 3) { return false; } - if (roomType.equals(NIGHT) && member.getCurrentNightCount() >= 3) { return false; } @@ -171,4 +200,108 @@ private void decreaseRoomCount(Long memberId, RoomType roomType) { member.exitNightRoom(); } + + private List getRoutineResponses(Long roomId) { + List roomRoutines = routineSearchRepository.findByRoomId(roomId); + + return RoutineMapper.toRoutineResponses(roomRoutines); + } + + private List getTodayCertificateRankResponses(Long roomId, + List routines, List dailyMemberCertifications, LocalDate today) { + + List responses = new ArrayList<>(); + List routineIds = routines.stream() + .map(RoutineResponse::routineId) + .toList(); + List certifications = certificationsSearchRepository.findCertifications( + routineIds, + today); + List participants = participantSearchRepository.findParticipants(roomId); + List members = memberService.getRoomMembers(participants.stream() + .map(Participant::getMemberId) + .toList()); + + addCompletedMembers(responses, dailyMemberCertifications, members, certifications, participants, today); + addUncompletedMembers(responses, dailyMemberCertifications, members, participants, today); + + return responses; + } + + private void addCompletedMembers(List responses, + List dailyMemberCertifications, List members, + List certifications, List participants, LocalDate today) { + + int rank = 1; + + for (DailyMemberCertification certification : dailyMemberCertifications) { + Member member = members.stream() + .filter(m -> m.getId().equals(certification.getMemberId())) + .findAny() + .orElseThrow(() -> new NotFoundException(ROOM_DETAILS_ERROR)); + + int contributionPoint = calculateContributionPoint(member.getId(), participants, today); + List certificationImageResponses = + CertificationsMapper.toCertificateImageResponses(member.getId(), certifications); + + TodayCertificateRankResponse response = CertificationsMapper.toTodayCertificateRankResponse( + rank, member, contributionPoint, "https://~awake", "https://~sleep", certificationImageResponses); + + rank += 1; + responses.add(response); + } + } + + private void addUncompletedMembers(List responses, + List dailyMemberCertifications, List members, + List participants, LocalDate today) { + + List allMemberIds = participants.stream() + .map(Participant::getMemberId) + .collect(Collectors.toList()); + + List certifiedMemberIds = dailyMemberCertifications.stream() + .map(DailyMemberCertification::getMemberId) + .toList(); + + allMemberIds.removeAll(certifiedMemberIds); + + for (Long memberId : allMemberIds) { + Member member = members.stream() + .filter(m -> m.getId().equals(memberId)) + .findAny() + .orElseThrow(() -> new NotFoundException(ROOM_DETAILS_ERROR)); + + int contributionPoint = calculateContributionPoint(memberId, participants, today); + + TodayCertificateRankResponse response = CertificationsMapper.toTodayCertificateRankResponse( + 500, member, contributionPoint, "https://~awake", "https://~sleep", null); + + responses.add(response); + } + } + + private int calculateContributionPoint(Long memberId, List participants, LocalDate today) { + Participant participant = participants.stream() + .filter(p -> p.getMemberId().equals(memberId)) + .findAny() + .orElseThrow(() -> new NotFoundException(ROOM_DETAILS_ERROR)); + + int participatedDays = Period.between(participant.getCreatedAt().toLocalDate(), today).getDays() + 1; + + return (int)(((double)participant.getCertifyCount() / participatedDays) * 100); + } + + private List getCertifiedDates(Long roomId, LocalDate today) { + List certifications = certificationsSearchRepository.findDailyRoomCertifications( + roomId, today); + + return certifications.stream().map(DailyRoomCertification::getCertifiedAt).toList(); + } + + private double calculateCompletePercentage(int certifiedMembersCount, int currentsMembersCount) { + double completePercentage = ((double)certifiedMembersCount / currentsMembersCount) * 100; + + return Math.round(completePercentage * 100) / 100.0; + } } diff --git a/src/main/java/com/moabam/api/domain/entity/DailyMemberCertification.java b/src/main/java/com/moabam/api/domain/entity/DailyMemberCertification.java new file mode 100644 index 00000000..1b9f6d19 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/DailyMemberCertification.java @@ -0,0 +1,49 @@ +package com.moabam.api.domain.entity; + +import static java.util.Objects.*; + +import com.moabam.global.common.entity.BaseTimeEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "daily_member_certification") // 매일 사용자가 방에 인증을 완료했는지 -> createdAt으로 인증 시각 확인 +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DailyMemberCertification extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "member_id", nullable = false, updatable = false) + private Long memberId; + + @Column(name = "room_id", nullable = false, updatable = false) + private Long roomId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "participant_id") // Participant createdAt으로 방 참여 시작 날짜 확인, certifyCount 가져다가 쓰기 + private Participant participant; + + @Builder + private DailyMemberCertification(Long id, Long memberId, Long roomId, Participant participant) { + this.id = id; + this.memberId = requireNonNull(memberId); + this.roomId = requireNonNull(roomId); + this.participant = requireNonNull(participant); + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/DailyRoomCertification.java b/src/main/java/com/moabam/api/domain/entity/DailyRoomCertification.java new file mode 100644 index 00000000..2e345b8a --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/DailyRoomCertification.java @@ -0,0 +1,41 @@ +package com.moabam.api.domain.entity; + +import static java.util.Objects.*; + +import java.time.LocalDate; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "daily_room_certification") // 매일 방이 인증을 완료했는지 -> certifiedAt으로 인증 날짜 확인 +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DailyRoomCertification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "room_id", nullable = false, updatable = false) + private Long roomId; + + @Column(name = "certified_at", nullable = false, updatable = false) + private LocalDate certifiedAt; + + @Builder + private DailyRoomCertification(Long id, Long roomId, LocalDate certifiedAt) { + this.id = id; + this.roomId = requireNonNull(roomId); + this.certifiedAt = requireNonNull(certifiedAt); + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/Participant.java b/src/main/java/com/moabam/api/domain/entity/Participant.java index 7c4442a8..09762b85 100644 --- a/src/main/java/com/moabam/api/domain/entity/Participant.java +++ b/src/main/java/com/moabam/api/domain/entity/Participant.java @@ -6,6 +6,8 @@ import org.hibernate.annotations.SQLDelete; +import com.moabam.global.common.entity.BaseTimeEntity; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -25,7 +27,7 @@ @Table(name = "participant") @SQLDelete(sql = "UPDATE participant SET deleted_at = CURRENT_TIMESTAMP where id = ?") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Participant { +public class Participant extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -33,7 +35,7 @@ public class Participant { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", updatable = false) + @JoinColumn(name = "room_id") private Room room; @Column(name = "member_id", updatable = false, nullable = false) diff --git a/src/main/java/com/moabam/api/domain/entity/Routine.java b/src/main/java/com/moabam/api/domain/entity/Routine.java index 26015bb2..23dd4773 100644 --- a/src/main/java/com/moabam/api/domain/entity/Routine.java +++ b/src/main/java/com/moabam/api/domain/entity/Routine.java @@ -30,7 +30,7 @@ public class Routine extends BaseTimeEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false, updatable = false) + @JoinColumn(name = "room_id", updatable = false) private Room room; @Column(name = "content", nullable = false, length = 60) diff --git a/src/main/java/com/moabam/api/domain/repository/CertificationsMapper.java b/src/main/java/com/moabam/api/domain/repository/CertificationsMapper.java new file mode 100644 index 00000000..0bba80ec --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/CertificationsMapper.java @@ -0,0 +1,50 @@ +package com.moabam.api.domain.repository; + +import java.util.ArrayList; +import java.util.List; + +import com.moabam.api.domain.entity.Certification; +import com.moabam.api.domain.entity.Member; +import com.moabam.api.dto.CertificationImageResponse; +import com.moabam.api.dto.TodayCertificateRankResponse; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class CertificationsMapper { + + public static List toCertificateImageResponses(Long memberId, + List certifications) { + List cftImageResponses = new ArrayList<>(); + List filteredCertifications = certifications.stream() + .filter(certification -> certification.getMemberId().equals(memberId)) + .toList(); + + for (Certification certification : filteredCertifications) { + CertificationImageResponse cftImageResponse = CertificationImageResponse.builder() + .routineId(certification.getRoutine().getId()) + .image(certification.getImage()) + .build(); + + cftImageResponses.add(cftImageResponse); + } + + return cftImageResponses; + } + + public static TodayCertificateRankResponse toTodayCertificateRankResponse(int rank, Member member, + int contributionPoint, String awakeImage, String sleepImage, + List certificationImageResponses) { + return TodayCertificateRankResponse.builder() + .rank(rank) + .memberId(member.getId()) + .nickname(member.getNickname()) + .profileImage(member.getProfileImage()) + .contributionPoint(contributionPoint) + .awakeImage(awakeImage) + .sleepImage(sleepImage) + .certificationImage(certificationImageResponses) + .build(); + } +} diff --git a/src/main/java/com/moabam/api/domain/repository/CertificationsSearchRepository.java b/src/main/java/com/moabam/api/domain/repository/CertificationsSearchRepository.java new file mode 100644 index 00000000..70c8a8cf --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/CertificationsSearchRepository.java @@ -0,0 +1,68 @@ +package com.moabam.api.domain.repository; + +import static com.moabam.api.domain.entity.QCertification.*; +import static com.moabam.api.domain.entity.QDailyMemberCertification.*; +import static com.moabam.api.domain.entity.QDailyRoomCertification.*; +import static com.moabam.api.domain.entity.QParticipant.*; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.moabam.api.domain.entity.Certification; +import com.moabam.api.domain.entity.DailyMemberCertification; +import com.moabam.api.domain.entity.DailyRoomCertification; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class CertificationsSearchRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public List findCertifications(List routineIds, LocalDate date) { + BooleanExpression expression = null; + + for (Long routineId : routineIds) { + BooleanExpression routineExpression = certification.routine.id.eq(routineId); + expression = expression == null ? routineExpression : expression.or(routineExpression); + } + + return jpaQueryFactory + .selectFrom(certification) + .where( + expression, + certification.createdAt.between(date.atStartOfDay(), date.atTime(LocalTime.MAX)) + ) + .fetch(); + } + + public List findSortedDailyMemberCertifications(Long roomId, LocalDate date) { + return jpaQueryFactory + .selectFrom(dailyMemberCertification) + .join(dailyMemberCertification.participant, participant).fetchJoin() + .where( + dailyMemberCertification.roomId.eq(roomId), + dailyMemberCertification.createdAt.between(date.atStartOfDay(), date.atTime(LocalTime.MAX)) + ) + .orderBy( + dailyMemberCertification.createdAt.asc() + ) + .fetch(); + } + + public List findDailyRoomCertifications(Long roomId, LocalDate date) { + return jpaQueryFactory + .selectFrom(dailyRoomCertification) + .where( + dailyRoomCertification.roomId.eq(roomId), + dailyRoomCertification.certifiedAt.eq(date) + ) + .fetch(); + } +} diff --git a/src/main/java/com/moabam/api/domain/repository/DailyMemberCertificationRepository.java b/src/main/java/com/moabam/api/domain/repository/DailyMemberCertificationRepository.java new file mode 100644 index 00000000..d5c98951 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/DailyMemberCertificationRepository.java @@ -0,0 +1,9 @@ +package com.moabam.api.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.entity.DailyMemberCertification; + +public interface DailyMemberCertificationRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moabam/api/domain/repository/DailyRoomCertificationRepository.java b/src/main/java/com/moabam/api/domain/repository/DailyRoomCertificationRepository.java new file mode 100644 index 00000000..baf84d7e --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/DailyRoomCertificationRepository.java @@ -0,0 +1,9 @@ +package com.moabam.api.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.entity.DailyRoomCertification; + +public interface DailyRoomCertificationRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moabam/api/domain/repository/MemberSearchRepository.java b/src/main/java/com/moabam/api/domain/repository/MemberSearchRepository.java new file mode 100644 index 00000000..9970db48 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/MemberSearchRepository.java @@ -0,0 +1,33 @@ +package com.moabam.api.domain.repository; + +import static com.moabam.api.domain.entity.QMember.*; +import static com.moabam.api.domain.entity.QParticipant.*; + +import java.util.Optional; + +import org.springframework.stereotype.Repository; + +import com.moabam.api.domain.entity.Member; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class MemberSearchRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Optional findManager(Long roomId) { + return Optional.ofNullable( + jpaQueryFactory + .selectFrom(member) + .innerJoin(participant).on(member.id.eq(participant.memberId)) + .where( + participant.isManager.eq(true), + participant.room.id.eq(roomId) + ) + .fetchOne() + ); + } +} diff --git a/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java b/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java index 9e3a01b3..91830965 100644 --- a/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java +++ b/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java @@ -1,7 +1,9 @@ package com.moabam.api.domain.repository; import static com.moabam.api.domain.entity.QParticipant.*; +import static com.moabam.api.domain.entity.QRoom.*; +import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; @@ -18,13 +20,25 @@ public class ParticipantSearchRepository { private final JPAQueryFactory jpaQueryFactory; - public Optional findParticipant(Long memberId, Long roomId) { + public Optional findOne(Long memberId, Long roomId) { return Optional.ofNullable( - jpaQueryFactory.selectFrom(participant) + jpaQueryFactory + .selectFrom(participant) + .join(participant.room, room).fetchJoin() .where( DynamicQuery.generateEq(roomId, participant.room.id::eq), DynamicQuery.generateEq(memberId, participant.memberId::eq) - ).fetchOne() + ) + .fetchOne() ); } + + public List findParticipants(Long roomId) { + return jpaQueryFactory + .selectFrom(participant) + .where( + participant.room.id.eq(roomId) + ) + .fetch(); + } } diff --git a/src/main/java/com/moabam/api/domain/repository/RoutineSearchRepository.java b/src/main/java/com/moabam/api/domain/repository/RoutineSearchRepository.java new file mode 100644 index 00000000..ee18d348 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/RoutineSearchRepository.java @@ -0,0 +1,28 @@ +package com.moabam.api.domain.repository; + +import static com.moabam.api.domain.entity.QRoutine.*; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.moabam.api.domain.entity.Routine; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class RoutineSearchRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public List findByRoomId(Long roomId) { + return jpaQueryFactory + .selectFrom(routine) + .where( + routine.room.id.eq(roomId) + ) + .fetch(); + } +} diff --git a/src/main/java/com/moabam/api/dto/CertificationImageResponse.java b/src/main/java/com/moabam/api/dto/CertificationImageResponse.java new file mode 100644 index 00000000..33d96acb --- /dev/null +++ b/src/main/java/com/moabam/api/dto/CertificationImageResponse.java @@ -0,0 +1,11 @@ +package com.moabam.api.dto; + +import lombok.Builder; + +@Builder +public record CertificationImageResponse( + Long routineId, + String image +) { + +} diff --git a/src/main/java/com/moabam/api/dto/NotificationMapper.java b/src/main/java/com/moabam/api/dto/NotificationMapper.java index f51016be..79795e15 100644 --- a/src/main/java/com/moabam/api/dto/NotificationMapper.java +++ b/src/main/java/com/moabam/api/dto/NotificationMapper.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) -public class NotificationMapper { +public final class NotificationMapper { private static final String TITLE = "모아밤"; private static final String KNOCK_BODY = "님이 콕 찔렀습니다."; diff --git a/src/main/java/com/moabam/api/dto/OAuthMapper.java b/src/main/java/com/moabam/api/dto/OAuthMapper.java index 6550f32f..a637ff4e 100644 --- a/src/main/java/com/moabam/api/dto/OAuthMapper.java +++ b/src/main/java/com/moabam/api/dto/OAuthMapper.java @@ -6,7 +6,7 @@ import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) -public class OAuthMapper { +public final class OAuthMapper { public static AuthorizationCodeRequest toAuthorizationCodeRequest(OAuthConfig oAuthConfig) { return AuthorizationCodeRequest.builder() diff --git a/src/main/java/com/moabam/api/dto/RoomDetailsResponse.java b/src/main/java/com/moabam/api/dto/RoomDetailsResponse.java new file mode 100644 index 00000000..b90f8c70 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/RoomDetailsResponse.java @@ -0,0 +1,26 @@ +package com.moabam.api.dto; + +import java.time.LocalDate; +import java.util.List; + +import lombok.Builder; + +@Builder +public record RoomDetailsResponse( + Long roomId, + String title, + String managerNickName, + String roomImage, + int level, + String roomType, + int certifyTime, + int currentUserCount, + int maxUserCount, + String announcement, + double completePercentage, + List certifiedDates, + List routine, + List todayCertificateRank +) { + +} diff --git a/src/main/java/com/moabam/api/dto/RoomMapper.java b/src/main/java/com/moabam/api/dto/RoomMapper.java index 9ac332c2..0a97ae0c 100644 --- a/src/main/java/com/moabam/api/dto/RoomMapper.java +++ b/src/main/java/com/moabam/api/dto/RoomMapper.java @@ -1,15 +1,15 @@ package com.moabam.api.dto; +import java.time.LocalDate; import java.util.List; import com.moabam.api.domain.entity.Room; -import com.moabam.api.domain.entity.Routine; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) -public abstract class RoomMapper { +public final class RoomMapper { public static Room toRoomEntity(CreateRoomRequest createRoomRequest) { return Room.builder() @@ -21,12 +21,24 @@ public static Room toRoomEntity(CreateRoomRequest createRoomRequest) { .build(); } - public static List toRoutineEntity(Room room, List routinesRequest) { - return routinesRequest.stream() - .map(routine -> Routine.builder() - .room(room) - .content(routine) - .build()) - .toList(); + public static RoomDetailsResponse toRoomDetailsResponse(Room room, String managerNickname, + List routineResponses, List certifiedDates, + List todayCertificateRankResponses, double completePercentage) { + return RoomDetailsResponse.builder() + .roomId(room.getId()) + .title(room.getTitle()) + .managerNickName(managerNickname) + .roomImage(room.getRoomImage()) + .level(room.getLevel()) + .roomType(room.getRoomType().name()) + .certifyTime(room.getCertifyTime()) + .currentUserCount(room.getCurrentUserCount()) + .maxUserCount(room.getMaxUserCount()) + .announcement(room.getAnnouncement()) + .completePercentage(completePercentage) + .certifiedDates(certifiedDates) + .routine(routineResponses) + .todayCertificateRank(todayCertificateRankResponses) + .build(); } } diff --git a/src/main/java/com/moabam/api/dto/RoutineMapper.java b/src/main/java/com/moabam/api/dto/RoutineMapper.java new file mode 100644 index 00000000..40e941a4 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/RoutineMapper.java @@ -0,0 +1,31 @@ +package com.moabam.api.dto; + +import java.util.List; + +import com.moabam.api.domain.entity.Room; +import com.moabam.api.domain.entity.Routine; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class RoutineMapper { + + public static List toRoutineEntities(Room room, List routinesRequest) { + return routinesRequest.stream() + .map(routine -> Routine.builder() + .room(room) + .content(routine) + .build()) + .toList(); + } + + public static List toRoutineResponses(List routines) { + return routines.stream() + .map(routine -> RoutineResponse.builder() + .routineId(routine.getId()) + .content(routine.getContent()) + .build()) + .toList(); + } +} diff --git a/src/main/java/com/moabam/api/dto/RoutineResponse.java b/src/main/java/com/moabam/api/dto/RoutineResponse.java new file mode 100644 index 00000000..8f02990b --- /dev/null +++ b/src/main/java/com/moabam/api/dto/RoutineResponse.java @@ -0,0 +1,11 @@ +package com.moabam.api.dto; + +import lombok.Builder; + +@Builder +public record RoutineResponse( + Long routineId, + String content +) { + +} diff --git a/src/main/java/com/moabam/api/dto/TodayCertificateRankResponse.java b/src/main/java/com/moabam/api/dto/TodayCertificateRankResponse.java new file mode 100644 index 00000000..970f6877 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/TodayCertificateRankResponse.java @@ -0,0 +1,19 @@ +package com.moabam.api.dto; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record TodayCertificateRankResponse( + int rank, + Long memberId, + String nickname, + String profileImage, + int contributionPoint, + String awakeImage, + String sleepImage, + List certificationImage +) { + +} diff --git a/src/main/java/com/moabam/api/presentation/RoomController.java b/src/main/java/com/moabam/api/presentation/RoomController.java index 077f94f1..8eba8cc3 100644 --- a/src/main/java/com/moabam/api/presentation/RoomController.java +++ b/src/main/java/com/moabam/api/presentation/RoomController.java @@ -2,6 +2,7 @@ import org.springframework.http.HttpStatus; 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; @@ -14,6 +15,7 @@ import com.moabam.api.dto.CreateRoomRequest; import com.moabam.api.dto.EnterRoomRequest; import com.moabam.api.dto.ModifyRoomRequest; +import com.moabam.api.dto.RoomDetailsResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -49,4 +51,10 @@ public void enterRoom(@Valid @RequestBody EnterRoomRequest enterRoomRequest, @Pa public void exitRoom(@PathVariable("roomId") Long roomId) { roomService.exitRoom(1L, roomId); } + + @GetMapping("/{roomId}") + @ResponseStatus(HttpStatus.OK) + public RoomDetailsResponse getRoomDetails(@PathVariable("roomId") Long roomId) { + return roomService.getRoomDetails(1L, roomId); + } } diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java index a9eafa3d..088b89ff 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -20,6 +20,7 @@ public enum ErrorMessage { PARTICIPANT_NOT_FOUND("방에 대한 참여자의 정보가 없습니다."), WRONG_ROOM_PASSWORD("방의 비밀번호가 일치하지 않습니다."), ROOM_MAX_USER_REACHED("방의 인원수가 찼습니다."), + ROOM_DETAILS_ERROR("방 정보를 불러오는데 실패했습니다."), LOGIN_FAILED("로그인에 실패했습니다."), REQUEST_FAILED("네트워크 접근 실패입니다."), diff --git a/src/test/java/com/moabam/api/presentation/RoomControllerTest.java b/src/test/java/com/moabam/api/presentation/RoomControllerTest.java index b4d24f56..7477725d 100644 --- a/src/test/java/com/moabam/api/presentation/RoomControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/RoomControllerTest.java @@ -7,6 +7,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -24,9 +25,16 @@ import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.databind.ObjectMapper; +import com.moabam.api.domain.entity.Certification; +import com.moabam.api.domain.entity.DailyMemberCertification; +import com.moabam.api.domain.entity.DailyRoomCertification; import com.moabam.api.domain.entity.Member; import com.moabam.api.domain.entity.Participant; import com.moabam.api.domain.entity.Room; +import com.moabam.api.domain.entity.Routine; +import com.moabam.api.domain.repository.CertificationRepository; +import com.moabam.api.domain.repository.DailyMemberCertificationRepository; +import com.moabam.api.domain.repository.DailyRoomCertificationRepository; import com.moabam.api.domain.repository.MemberRepository; import com.moabam.api.domain.repository.ParticipantRepository; import com.moabam.api.domain.repository.RoomRepository; @@ -61,6 +69,15 @@ class RoomControllerTest { @Autowired private MemberRepository memberRepository; + @Autowired + private CertificationRepository certificationRepository; + + @Autowired + private DailyMemberCertificationRepository dailyMemberCertificationRepository; + + @Autowired + private DailyRoomCertificationRepository dailyRoomCertificationRepository; + Member member; @BeforeAll @@ -586,10 +603,10 @@ void no_manager_exit_room_success() throws Exception { .andDo(print()); participantRepository.flush(); + Room findRoom = roomRepository.findById(room.getId()).orElseThrow(); Participant deletedParticipant = participantRepository.findById(participant.getId()).orElseThrow(); - assertThat(room.getCurrentUserCount()).isEqualTo(4); + assertThat(findRoom.getCurrentUserCount()).isEqualTo(4); assertThat(deletedParticipant.getDeletedAt()).isNotNull(); - assertThat(deletedParticipant.getDeletedRoomTitle()).isEqualTo("5명이 있는 방~"); } @DisplayName("방장의 방 나가기 - 방 삭제 성공") @@ -614,6 +631,7 @@ void manager_delete_room_success() throws Exception { mockMvc.perform(delete("/rooms/" + room.getId())) .andExpect(status().isOk()) .andDo(print()); + Participant deletedParticipant = participantRepository.findById(participant.getId()).orElseThrow(); assertThat(roomRepository.findById(room.getId())).isEmpty(); assertThat(deletedParticipant.getDeletedAt()).isNotNull(); @@ -719,4 +737,105 @@ void exit_and_decrease_night_room_count() throws Exception { // then assertThat(getMember.getCurrentNightCount()).isEqualTo(2); } + + @DisplayName("방 상세 정보 조회 성공 테스트") + @Test + void get_room_details_test() throws Exception { + // given + Room room = Room.builder() + .title("방 제목") + .password("1234") + .roomType(NIGHT) + .certifyTime(23) + .maxUserCount(5) + .build(); + + room.increaseCurrentUserCount(); + room.increaseCurrentUserCount(); + + Routine routine1 = Routine.builder() + .room(room) + .content("물 마시기") + .build(); + + Routine routine2 = Routine.builder() + .room(room) + .content("코테 풀기") + .build(); + + Participant participant1 = Participant.builder() + .room(room) + .memberId(1L) + .build(); + participant1.enableManager(); + + Member member2 = Member.builder() + .socialId("SOCIAL_2") + .nickname("NICKNAME_2") + .profileImage("PROFILE_IMAGE_2") + .bug(BugFixture.bug()) + .build(); + + Member member3 = Member.builder() + .socialId("SOCIAL_3") + .nickname("NICKNAME_3") + .profileImage("PROFILE_IMAGE_3") + .bug(BugFixture.bug()) + .build(); + + roomRepository.save(room); + routineRepository.save(routine1); + routineRepository.save(routine2); + memberRepository.save(member2); + memberRepository.save(member3); + + Participant participant2 = Participant.builder() + .room(room) + .memberId(member2.getId()) + .build(); + + Participant participant3 = Participant.builder() + .room(room) + .memberId(member3.getId()) + .build(); + + participantRepository.save(participant1); + participantRepository.save(participant2); + participantRepository.save(participant3); + + Certification certification1 = Certification.builder() + .routine(routine1) + .memberId(member.getId()) + .image("member1Image") + .build(); + + Certification certification2 = Certification.builder() + .routine(routine2) + .memberId(member.getId()) + .image("member2Image") + .build(); + + certificationRepository.save(certification1); + certificationRepository.save(certification2); + + DailyMemberCertification dailyMemberCertification = DailyMemberCertification.builder() + .memberId(member.getId()) + .roomId(room.getId()) + .participant(participant1) + .build(); + + dailyMemberCertificationRepository.save(dailyMemberCertification); + + DailyRoomCertification dailyRoomCertification = DailyRoomCertification.builder() + .roomId(room.getId()) + .certifiedAt(LocalDate.now()) + .build(); + + dailyRoomCertificationRepository.save(dailyRoomCertification); + + // expected + mockMvc.perform(get("/rooms/" + room.getId())) + .andExpect(status().isOk()) + .andDo(print()); + } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index c73dc6c7..723cc57c 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -12,7 +12,7 @@ spring: properties: hibernate: format_sql: true - + highlight_sql: true # Redis data: redis: