Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 미션 인증 피드 API 틀 구현 #74

Merged
merged 16 commits into from
Aug 21, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void validateMaxPersonnel(final Mission mission) {
}

public void validateMissionPeriod(final Mission mission) {
if (mission.isMissionPeriod()) {
if (mission.isMissionPeriod() || mission.isExpired()) {
throw new BadRequestException(ErrorCode.CAN_NOT_JOIN_MISSION.toString());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
import com.nexters.goalpanzi.application.mission.dto.request.CreateMissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.request.MissionVerificationQuery;
import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationQuery;
import com.nexters.goalpanzi.application.mission.dto.request.ViewMissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse;
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationsResponse;
import com.nexters.goalpanzi.application.upload.ObjectStorageClient;
import com.nexters.goalpanzi.domain.common.BaseEntity;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
import com.nexters.goalpanzi.domain.mission.Mission;
import com.nexters.goalpanzi.domain.mission.MissionMember;
import com.nexters.goalpanzi.domain.mission.MissionMembers;
import com.nexters.goalpanzi.domain.mission.MissionVerification;
import com.nexters.goalpanzi.domain.mission.*;
import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionVerificationRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionVerificationViewRepository;
import com.nexters.goalpanzi.exception.BadRequestException;
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
Expand All @@ -33,6 +33,7 @@ public class MissionVerificationService {

private final MissionVerificationRepository missionVerificationRepository;
private final MissionMemberRepository missionMemberRepository;
private final MissionVerificationViewRepository missionVerificationViewRepository;
private final MemberRepository memberRepository;

private final ObjectStorageClient objectStorageClient;
Expand All @@ -43,8 +44,7 @@ public MissionVerificationsResponse getVerifications(final MissionVerificationQu

Member member = memberRepository.getMember(query.memberId());
MissionMembers missionMembers = new MissionMembers(missionMemberRepository.findAllByMissionId(query.missionId()));
// TODO 추후 활성화
// missionMembers.verifyMissionMember(member);
missionMembers.verifyMissionMember(member);
List<MissionVerification> missionVerifications = missionVerificationRepository.findAllByMissionIdAndDate(query.missionId(), date);

return new MissionVerificationsResponse(sortMissionVerifications(member, query.sortType(), query.direction(), missionVerifications, missionMembers.getMissionMembers()));
Expand All @@ -57,7 +57,9 @@ private List<MissionVerificationResponse> sortMissionVerifications(final Member

missionMembers.forEach(missionMember -> {
Member member1 = missionMember.getMember();
MissionVerificationResponse missionVerificationResponse = MissionVerificationResponse.of(member1, Optional.ofNullable(map.get(member1.getId())));
MissionVerification missionVerification = map.get(member1.getId());
MissionVerificationView missionVerificationView = missionVerificationViewRepository.getMissionVerificationView(missionVerification.getId(), member1.getId());
MissionVerificationResponse missionVerificationResponse = MissionVerificationResponse.of(member1, Optional.of(missionVerification), Optional.of(missionVerificationView));
response.add(missionVerificationResponse);
});

Expand All @@ -67,6 +69,7 @@ private List<MissionVerificationResponse> sortMissionVerifications(final Member

private static Comparator<MissionVerificationResponse> compareMissionVerificationResponses(final String nickname, final MissionVerificationQuery.SortType sortType, final Sort.Direction direction) {
return Comparator.comparing((MissionVerificationResponse missionVerificationResponse) -> missionVerificationResponse.nickname().equals(nickname)).reversed()
.thenComparing((MissionVerificationResponse missionVerificationResponse) -> missionVerificationResponse.viewedAt() == null, Comparator.reverseOrder())
.thenComparing(compareMissionVerificationResponsesByOrder(sortType, direction));
}

Expand All @@ -84,7 +87,7 @@ private static Comparator<MissionVerificationResponse> compareMissionVerificatio
public MissionVerificationResponse getMyVerification(final MyMissionVerificationQuery query) {
MissionVerification verification = missionVerificationRepository.getMyVerification(query.memberId(), query.missionId(), query.number());

return MissionVerificationResponse.verified(verification.getMember(), verification);
return MissionVerificationResponse.verified(verification.getMember(), verification, null);
}

@Transactional
Expand Down Expand Up @@ -136,4 +139,13 @@ public void deleteAllByMissionId(final Long missionId) {
missionVerificationRepository.findAllByMissionId(missionId)
.forEach(BaseEntity::delete);
}

@Transactional
public void viewMissionVerification(final ViewMissionVerificationCommand command) {
Member member = memberRepository.getMember(command.memberId());
MissionVerification missionVerification = missionVerificationRepository.findById(command.missionVerificationId())
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_VERIFICATION));

missionVerificationViewRepository.save(new MissionVerificationView(missionVerification, member));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.nexters.goalpanzi.application.mission.dto.request;

public record ViewMissionVerificationCommand(
Long missionVerificationId,
Long memberId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.nexters.goalpanzi.domain.member.CharacterType;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.mission.MissionVerification;
import com.nexters.goalpanzi.domain.mission.MissionVerificationView;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;

Expand All @@ -14,23 +15,32 @@ public record MissionVerificationResponse(
@NotEmpty String nickname,
@Schema(description = "장기말 타입", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty CharacterType characterType,
@Schema(description = "미션 인증 아이디", requiredMode = Schema.RequiredMode.REQUIRED)
Long missionVerificationId,
@Schema(description = "인증 이미지 URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
String imageUrl,
@Schema(description = "인증 시간", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
LocalDateTime verifiedAt
LocalDateTime verifiedAt,
@Schema(description = "조회 시간", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
LocalDateTime viewedAt
) {

public static MissionVerificationResponse of(final Member member, final Optional<MissionVerification> verification) {
return verification
.map(v -> verified(member, v))
.orElseGet(() -> notVerified(member));
public static MissionVerificationResponse of(final Member member, final Optional<MissionVerification> missionVerification, final Optional<MissionVerificationView> missionVerificationView) {
LocalDateTime viewedAt = getViewedAt(missionVerificationView);
return missionVerification
.map(v -> verified(member, v, viewedAt))
.orElseGet(() -> notVerified(member, viewedAt));
}

public static MissionVerificationResponse verified(Member member, MissionVerification verification) {
return new MissionVerificationResponse(member.getNickname(), member.getCharacterType(), verification.getImageUrl(), verification.getCreatedAt());
public static MissionVerificationResponse verified(final Member member, final MissionVerification missionVerification, final LocalDateTime viewedAt) {
return new MissionVerificationResponse(member.getNickname(), member.getCharacterType(), missionVerification.getId(), missionVerification.getImageUrl(), missionVerification.getCreatedAt(), viewedAt);
}

public static MissionVerificationResponse notVerified(Member member) {
return new MissionVerificationResponse(member.getNickname(), member.getCharacterType(), "", null);
public static MissionVerificationResponse notVerified(final Member member, final LocalDateTime viewedAt) {
return new MissionVerificationResponse(member.getNickname(), member.getCharacterType(), null, "", null, viewedAt);
}

private static LocalDateTime getViewedAt(final Optional<MissionVerificationView> missionVerificationView) {
return missionVerificationView.isPresent() ? missionVerificationView.get().getCreatedAt() : null;
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
package com.nexters.goalpanzi.application.mission.dto.response;

import com.nexters.goalpanzi.domain.mission.MissionVerification;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;
import java.util.Optional;

public record MissionVerificationsResponse(
@Schema(description = "미션 인증 정보 리스트", requiredMode = Schema.RequiredMode.REQUIRED)
List<MissionVerificationResponse> missionVerifications
) {

public static MissionVerificationsResponse from(final List<MissionVerification> verifications) {
return new MissionVerificationsResponse(
verifications.stream()
.map(verification -> MissionVerificationResponse.of(verification.getMember(), Optional.of(verification)))
.toList()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ public boolean isMissionTime() {
return now.compareTo(uploadStartTime) >= 0 && now.compareTo(uploadEndTime) <= 0;
}

public boolean isExpired() {
LocalDate today = LocalDate.now();
return today.isAfter(missionEndDate.toLocalDate());
}

@Override
public boolean equals(final Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.nexters.goalpanzi.domain.mission;

import com.nexters.goalpanzi.domain.common.BaseEntity;
import com.nexters.goalpanzi.domain.member.Member;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "mission_verification_view")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class MissionVerificationView extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "mission_verification_view_id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "mission_verification_id", nullable = false)
private MissionVerification missionVerification;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

public MissionVerificationView(final MissionVerification missionVerification, final Member member) {
this.missionVerification = missionVerification;
this.member = member;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.util.List;
import java.util.Optional;

@Repository
public interface MissionMemberRepository extends JpaRepository<MissionMember, Long> {
Optional<MissionMember> findByMemberIdAndMissionId(final Long memberId, final Long missionId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
import com.nexters.goalpanzi.exception.NotFoundException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

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

@Repository
public interface MissionVerificationRepository extends JpaRepository<MissionVerification, Long> {

List<MissionVerification> findAllByMemberId(final Long memberId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.nexters.goalpanzi.domain.mission.repository;

import com.nexters.goalpanzi.domain.mission.MissionVerificationView;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MissionVerificationViewRepository extends JpaRepository<MissionVerificationView, Long> {

Optional<MissionVerificationView> findByMissionVerificationIdAndMemberId(final Long missionVerificationId, final Long memberId);

default MissionVerificationView getMissionVerificationView(final Long missionVerificationId, final Long memberId) {
return findByMissionVerificationIdAndMemberId(missionVerificationId, memberId).orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import com.nexters.goalpanzi.application.mission.dto.request.CreateMissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.request.MissionVerificationQuery;
import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationQuery;
import com.nexters.goalpanzi.application.mission.dto.request.ViewMissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse;
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationsResponse;
import com.nexters.goalpanzi.common.argumentresolver.LoginMemberId;
import com.nexters.goalpanzi.presentation.mission.dto.ViewMissionVerificationRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.format.annotation.DateTimeFormat;
Expand Down Expand Up @@ -56,4 +59,16 @@ public ResponseEntity<Void> createVerification(

return ResponseEntity.ok().build();
}

@Override
@PostMapping(value = "/verifications/view")
public ResponseEntity<MissionVerificationResponse> viewMissionVerification(
@Valid @RequestBody final ViewMissionVerificationRequest request,
@LoginMemberId final Long memberId
) {
missionVerificationService.viewMissionVerification(
new ViewMissionVerificationCommand(request.missionVerificationId(), memberId));

return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse;
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationsResponse;
import com.nexters.goalpanzi.common.argumentresolver.LoginMemberId;
import com.nexters.goalpanzi.presentation.mission.dto.ViewMissionVerificationRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand Down Expand Up @@ -40,6 +42,8 @@ public interface MissionVerificationControllerDocs {

date, sortType, sortDirection 생략 시, **[오늘 기준, 인증 최신순]**으로 조회합니다.

내가 조회하지 않은 인증 현황은 viewedAt을 null로 전달합니다. (주황색 테두리 표시 용도)

미션을 인증하지 않은 멤버는 프로필 정보만 포함하여 마지막에 배치됩니다.
"""
)
Expand All @@ -50,7 +54,7 @@ public interface MissionVerificationControllerDocs {
})
@GetMapping("/{missionId}/verifications")
ResponseEntity<MissionVerificationsResponse> getVerifications(
@Parameter(hidden = true) @LoginMemberId final Long memberId,
@Parameter(in = ParameterIn.HEADER, hidden = true) @LoginMemberId final Long memberId,
@Schema(description = "미션 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED)
@PathVariable(name = "missionId") final Long missionId,
@Schema(description = "미션 인증 일자 (생략 시 오늘로 간주)", type = "string", format = "date", pattern = "^\\d{4}-\\d{2}-\\d{2}$", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
Expand All @@ -69,7 +73,7 @@ ResponseEntity<MissionVerificationsResponse> getVerifications(
})
@GetMapping("/{missionId}/verifications/me/{number}")
ResponseEntity<MissionVerificationResponse> getMyVerification(
@Parameter(hidden = true) @LoginMemberId final Long memberId,
@Parameter(in = ParameterIn.HEADER, hidden = true) @LoginMemberId final Long memberId,
@Schema(description = "미션 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED)
@PathVariable(name = "missionId") final Long missionId,
@Schema(description = "보드칸 번호", type = "integer", format = "int32", requiredMode = Schema.RequiredMode.REQUIRED)
Expand All @@ -95,9 +99,16 @@ ResponseEntity<MissionVerificationResponse> getMyVerification(
})
@PostMapping(value = "/{missionId}/verifications/me")
ResponseEntity<Void> createVerification(
@Parameter(hidden = true) @LoginMemberId final Long memberId,
@Parameter(in = ParameterIn.HEADER, hidden = true) @LoginMemberId final Long memberId,
@Schema(description = "미션 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED)
@PathVariable(name = "missionId") final Long missionId,
@Schema(description = "인증 이미지 파일", requiredMode = Schema.RequiredMode.REQUIRED)
@RequestPart(name = "imageFile") final MultipartFile imageFile);

@Operation(summary = "미션 인증 피드 확인", description = "사용자의 인증 피드를 확인합니다.")
@PostMapping(value = "/verifications/view")
ResponseEntity<MissionVerificationResponse> viewMissionVerification(
@RequestBody final ViewMissionVerificationRequest request,
@Parameter(in = ParameterIn.HEADER, hidden = true) @LoginMemberId final Long memberId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.nexters.goalpanzi.presentation.mission.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record ViewMissionVerificationRequest(
@NotNull
@Schema(description = "미션 인증 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED)
Long missionVerificationId
) {
}
11 changes: 11 additions & 0 deletions src/main/resources/db/migration/V1__init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,14 @@ create table if not exists mission_member
mission_id bigint not null,
verification_count int
);

create table if not exists mission_verification_view
(
mission_verification_view_id bigint auto_increment
primary key,
created_at datetime(6),
deleted_at datetime(6),
updated_at datetime(6),
mission_verification_id bigint not null,
member_id bigint not null
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
create index idx_mission_verification_view__mission_verification_id__member_id on mission_verification_view (mission_verification_id, member_id);
Loading
Loading