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

fix: 미션 인증 유효성 검사 추가 #30

Merged
merged 24 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f43517e
feat: 내가 참여중인 미션 조회 API 구현
songyi00 Aug 1, 2024
ef86f0e
refactor: dto 패키지 정리
songyi00 Aug 1, 2024
8d9dae1
fix: 미션 인증 시 요일 검증 추가
kimyu0218 Aug 3, 2024
16d11e0
feat: 장기말 타입 추가
kimyu0218 Aug 3, 2024
449ce1e
feat: 구글을 위한 자체 토큰 생성 추가
kimyu0218 Aug 3, 2024
35c1572
remove: identity token 필드 제거
kimyu0218 Aug 3, 2024
5b5b730
refactor: identity token 자체 생성
kimyu0218 Aug 3, 2024
05c3888
fix: 충돌 해결
kimyu0218 Aug 3, 2024
0ebfad6
fix: 충돌 해결
kimyu0218 Aug 3, 2024
6299ef2
feat: 에러 코드 추가
kimyu0218 Aug 3, 2024
4525237
rename: command -> query
kimyu0218 Aug 3, 2024
1b536de
refactor: 서비스 로직 단순화
kimyu0218 Aug 3, 2024
e7a1871
refactor: 인터페이스 및 구현체 분리
kimyu0218 Aug 3, 2024
14c1687
refactor: 서비스 로직 단순화
kimyu0218 Aug 3, 2024
712cad2
remove: 불필요한 코드 제거
kimyu0218 Aug 3, 2024
0d1b5ef
rename: bean 이름 변경
kimyu0218 Aug 4, 2024
c073ab6
remove: 불필요한 dto 제거
kimyu0218 Aug 4, 2024
f75280c
fix: 서비스 및 레포지토리 에러 픽스
kimyu0218 Aug 4, 2024
7c653de
feat: BadRequest 400 던지도록 변경
kimyu0218 Aug 4, 2024
5c79b2a
fix: java.time.DayOfWeek 호환 문제 해결
kimyu0218 Aug 4, 2024
5c2631b
test: 미션 인증 테스트 작성
kimyu0218 Aug 4, 2024
231dd75
test: 중복 인증 테스트케이스 추가
kimyu0218 Aug 4, 2024
d21be66
refactor: DayOfWeek 검증 방식 변경
kimyu0218 Aug 4, 2024
a4ba8a4
docs: swagger 수정
kimyu0218 Aug 4, 2024
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 @@ -4,6 +4,7 @@
import com.nexters.goalpanzi.application.auth.dto.request.GoogleLoginCommand;
import com.nexters.goalpanzi.application.auth.dto.response.LoginResponse;
import com.nexters.goalpanzi.application.auth.dto.response.TokenResponse;
import com.nexters.goalpanzi.application.auth.google.GoogleIdentityToken;
import com.nexters.goalpanzi.common.jwt.Jwt;
import com.nexters.goalpanzi.common.jwt.JwtProvider;
import com.nexters.goalpanzi.domain.auth.repository.RefreshTokenRepository;
Expand Down Expand Up @@ -32,7 +33,8 @@ public LoginResponse appleOAuthLogin(final AppleLoginCommand command) {
}

public LoginResponse googleOAuthLogin(final GoogleLoginCommand command) {
SocialUserInfo socialUserInfo = new SocialUserInfo(command.identityToken(), command.email());
SocialUserInfo socialUserInfo = new SocialUserInfo(
GoogleIdentityToken.generate(command.email()), command.email());

return socialLogin(socialUserInfo, SocialType.GOOGLE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import jakarta.validation.constraints.NotEmpty;

public record GoogleLoginCommand(
@NotEmpty String identityToken,
@NotEmpty String email
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.nexters.goalpanzi.application.auth.google;

import com.nexters.goalpanzi.domain.member.SocialType;
import com.nexters.goalpanzi.exception.BaseException;
import com.nexters.goalpanzi.exception.ErrorCode;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class GoogleIdentityToken {

private static String SHA_256 = "SHA-256";

public static String generate(final String email) {
return generateHash(email + "__" + SocialType.GOOGLE);
}

private static String generateHash(final String input) {
try {
return Base64.getUrlEncoder().withoutPadding().encodeToString(
MessageDigest.getInstance(SHA_256).digest(input.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) {
throw new BaseException(ErrorCode.FAILED_TO_GENERATE_HASH);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
package com.nexters.goalpanzi.application.mission;

import com.nexters.goalpanzi.application.mission.dto.request.CreateMissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.request.MissionVerificationCommand;
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.response.MissionVerificationResponse;
import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse;
import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationCommand;
import com.nexters.goalpanzi.application.ncp.ObjectStorageClient;
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.MissionVerification;
import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionVerificationRepository;
import com.nexters.goalpanzi.exception.BadRequestException;
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.NotFoundException;
import com.nexters.goalpanzi.infrastructure.ncp.ObjectStorageManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
Expand All @@ -33,82 +29,70 @@
@Service
public class MissionVerificationService {

private final MissionRepository missionRepository;
private final MissionVerificationRepository missionVerificationRepository;
private final MissionMemberRepository missionMemberRepository;
private final MemberRepository memberRepository;

private final ObjectStorageManager objectStorageManager;
private final ObjectStorageClient objectStorageClient;

@Transactional(readOnly = true)
public List<MissionVerificationResponse> getVerifications(final MissionVerificationCommand command) {
LocalDate date = command.date() != null ? command.date() : LocalDate.now();
Member member =
memberRepository.findById(command.memberId())
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
List<MissionMember> missionMembers = missionMemberRepository.findAllByMissionId(command.missionId());
List<MissionVerification> verifications = missionVerificationRepository.findAllByMissionIdAndDate(command.missionId(), date);
public List<MissionVerificationResponse> getVerifications(final MissionVerificationQuery query) {
LocalDate date = query.date() != null ? query.date() : LocalDate.now();
Member member = memberRepository.getMember(query.memberId());
List<MissionVerification> verifications = missionVerificationRepository.findAllByMissionIdAndDate(query.missionId(), date);

Map<Long, MissionVerification> verificationMap = verifications.stream()
.collect(Collectors.toMap(v -> v.getMember().getId(), v -> v));

return missionMembers.stream()
.map(m -> {
MissionVerification v = verificationMap.get(m.getMember().getId());
return v != null
? MissionVerificationResponse.verified(m.getMember(), v)
: MissionVerificationResponse.notVerified(m.getMember());
})
return missionMemberRepository.findAllByMissionId(query.missionId()).stream()
.map(m -> convertToVerificationResponse(m, verificationMap.get(m.getMember().getId())))
.sorted(Comparator.comparing((MissionVerificationResponse r) -> r.nickname().equals(member.getNickname())).reversed()
.thenComparing(MissionVerificationResponse::verifiedAt, Comparator.nullsLast(Comparator.reverseOrder())))
.collect(Collectors.toList());
}

public MissionVerificationResponse getMyVerification(final MyMissionVerificationCommand command) {
Member member =
memberRepository.findById(command.memberId())
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
MissionVerification verification =
missionVerificationRepository.findByMemberIdAndMissionIdAndBoardNumber(command.memberId(), command.missionId(), command.number())
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_VERIFICATION));
public MissionVerificationResponse getMyVerification(final MyMissionVerificationQuery query) {
MissionVerification verification = missionVerificationRepository.getMyVerification(query.memberId(), query.missionId(), query.number());

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

@Transactional
public void createVerification(final CreateMissionVerificationCommand command) {
Member member =
memberRepository.findById(command.memberId())
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
Mission mission =
missionRepository.findById(command.missionId())
.orElseThrow(() -> new NotFoundException("TODO"));
MissionMember missionMember =
missionMemberRepository.findByMemberIdAndMissionId(command.memberId(), command.missionId())
.orElseThrow(() -> new NotFoundException("TODO"));

checkVerificationValidation(command.memberId(), command.missionId(), mission, missionMember);

String imageUrl = objectStorageManager.uploadFile(command.imageFile());
missionVerificationRepository.save(new MissionVerification(member, mission, imageUrl));
MissionMember missionMember = missionMemberRepository.getMissionMember(command.memberId(), command.missionId());
Mission mission = missionMember.getMission();

checkVerificationValidation(command.memberId(), mission, missionMember);

String imageUrl = objectStorageClient.uploadFile(command.imageFile());
missionMember.verify();
missionVerificationRepository.save(new MissionVerification(missionMember.getMember(), mission, imageUrl, missionMember.getVerificationCount()));
}

private void checkVerificationValidation(final Long memberId, final Long missionId, final Mission mission, final MissionMember missionMember) {
private MissionVerificationResponse convertToVerificationResponse(final MissionMember missionMember, final MissionVerification verification) {
return verification != null
? MissionVerificationResponse.verified(missionMember.getMember(), verification)
: MissionVerificationResponse.notVerified(missionMember.getMember());
}

private void checkVerificationValidation(final Long memberId, final Mission mission, final MissionMember missionMember) {
if (isCompletedMission(mission, missionMember)) {
throw new BadRequestException(ErrorCode.ALREADY_COMPLETED_MISSION);
}
if (isDuplicatedVerification(memberId, missionId)) {
LocalDate today = LocalDate.now();
if (isDuplicatedVerification(memberId, mission.getId(), today)) {
throw new BadRequestException(ErrorCode.DUPLICATE_VERIFICATION);
}
if (!mission.isMissionDay(today)) {
throw new BadRequestException(ErrorCode.NOT_VERIFICATION_DAY);
}
}

private boolean isCompletedMission(final Mission mission, final MissionMember missionMember) {
return missionMember.getVerificationCount() >= mission.getBoardCount();
}

private boolean isDuplicatedVerification(final Long memberId, final Long missionId) {
LocalDate today = LocalDateTime.now().toLocalDate();
private boolean isDuplicatedVerification(final Long memberId, final Long missionId, final LocalDate today) {
return missionVerificationRepository.findByMemberIdAndMissionIdAndDate(memberId, missionId, today).isPresent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.time.LocalDate;

public record MissionVerificationCommand(
public record MissionVerificationQuery(
Long memberId,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 필수값 여부 스키마 주석 추가해주면 좋을 것 같아
코드보니까 date 가 nullable인 것 같아서!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아! 이 부분 null 허용해줬어

date 안 보내면 그냥 오늘 기준으로 인증 정보 조회하도록 만들었는데 그냥 무조건 보내도록 만들까?? (딱히 이유가 있는 건 아니야!! 기본값 느낌으로..)

@Transactional(readOnly = true)
public List<MissionVerificationResponse> getVerifications(final MissionVerificationQuery query) {
    LocalDate date = query.date() != null ? query.date() : LocalDate.now();
    ...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

응응 기본값 오늘 기준으로 인증해줘도 괜찮을 것 같아!
쿼리 dto에 스웨거 스키마 필수값 여부만 추가해줘 ㅎㅎ

Long missionId,
LocalDate date
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.nexters.goalpanzi.application.mission.dto.request;

public record MyMissionVerificationCommand(
public record MyMissionVerificationQuery(
Long memberId,
Long missionId,
Integer number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ public record MissionResponse(
@Schema(description = "목표 행동", requiredMode = Schema.RequiredMode.REQUIRED)
String description
) {
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.nexters.goalpanzi.application.mission.dto.response;

import com.nexters.goalpanzi.domain.member.CharacterType;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.mission.MissionVerification;
import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -10,21 +11,19 @@
public record MissionVerificationResponse(
@Schema(description = "닉네임", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty String nickname,

// TODO 캐릭터 이미지

@Schema(description = "장기말 타입", requiredMode = Schema.RequiredMode.REQUIRED)
CharacterType characterType,
@Schema(description = "인증 이미지 URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
String imageUrl,

@Schema(description = "인증 시간", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
LocalDateTime verifiedAt
) {

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

public static MissionVerificationResponse notVerified(Member member) {
return new MissionVerificationResponse(member.getNickname(), "", null);
return new MissionVerificationResponse(member.getNickname(), member.getCharacterType(), "", null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.nexters.goalpanzi.application.member.dto.response.ProfileResponse;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.mission.MissionMember;
import com.nexters.goalpanzi.domain.mission.MissionVerification;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.nexters.goalpanzi.application.ncp;

import org.springframework.stereotype.Repository;
import org.springframework.web.multipart.MultipartFile;

@Repository
public interface ObjectStorageClient {

String uploadFile(final MultipartFile file);

void deleteFile(final String uploadedFileUrl);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class NcpConfig {
private String endPoint;

@Bean
public AmazonS3 s3Client() {
public AmazonS3 amazonS3() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

return AmazonS3ClientBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ public enum DayOfWeek {
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
;
SUNDAY;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@

import com.nexters.goalpanzi.common.jpa.DaysOfWeekConverter;
import com.nexters.goalpanzi.domain.common.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

Expand Down Expand Up @@ -111,4 +105,8 @@ private void validateMission() {
throw new IllegalArgumentException("보드 칸 개수는 최소 1이어야 합니다.");
}
}

public boolean isMissionDay(final LocalDate date) {
return this.missionDays.contains(DayOfWeek.valueOf(date.getDayOfWeek().name()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ public class MissionVerification extends BaseEntity {
@Column(name = "board_number")
private Integer boardNumber;

public MissionVerification(final Member member, final Mission mission, final String imageUrl) {
public MissionVerification(final Member member, final Mission mission, final String imageUrl, final Integer boardNumber) {
this.member = member;
this.mission = mission;
this.imageUrl = imageUrl;
this.boardNumber = boardNumber;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.nexters.goalpanzi.domain.mission.repository;

import com.nexters.goalpanzi.domain.mission.MissionMember;
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.NotFoundException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

Expand All @@ -14,4 +16,9 @@ public interface MissionMemberRepository extends JpaRepository<MissionMember, Lo
List<MissionMember> findAllByMissionId(final Long MissionId);

List<MissionMember> findAllByMemberId(final Long memberId);

default MissionMember getMissionMember(final Long memberId, final Long missionId) {
return findByMemberIdAndMissionId(memberId, missionId)
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_JOINED_MISSION_MEMBER));
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.nexters.goalpanzi.domain.mission.repository;

import com.nexters.goalpanzi.domain.mission.Mission;
import com.nexters.goalpanzi.domain.mission.MissionVerification;
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.NotFoundException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
Expand All @@ -17,9 +19,14 @@ public interface MissionVerificationRepository extends JpaRepository<MissionVeri

Optional<MissionVerification> findByMemberIdAndMissionIdAndBoardNumber(final Long memberId, final Long missionId, final Integer boardNumber);

@Query(value = "SELECT * FROM mission_verification mv WHERE mv.mission_id = :missionId AND DATE(mv.created_at) = :date", nativeQuery = true)
List<MissionVerification> findAllByMissionIdAndDate(final Long missionId, final LocalDate date);
@Query("SELECT mv FROM MissionVerification mv WHERE mv.mission.id = :missionId AND Date(mv.createdAt) = :date")
List<MissionVerification> findAllByMissionIdAndDate(@Param("missionId") final Long missionId, @Param("date") final LocalDate date);

@Query(value = "SELECT * FROM mission_verification mv WHERE mv.member_id = :memberId AND mv.mission_id = :missionId AND DATE(mv.created_at) = :date", nativeQuery = true)
Optional<MissionVerification> findByMemberIdAndMissionIdAndDate(final Long memberId, final Long missionId, final LocalDate date);
@Query("SELECT mv FROM MissionVerification mv WHERE mv.member.id = :memberId AND mv.mission.id = :missionId AND Date(mv.createdAt) = :date")
Optional<MissionVerification> findByMemberIdAndMissionIdAndDate(@Param("memberId") Long memberId, @Param("missionId") Long missionId, @Param("date") LocalDate date);

default MissionVerification getMyVerification(final Long memberId, final Long missionId, final Integer boardNumber) {
return findByMemberIdAndMissionIdAndBoardNumber(memberId, missionId, boardNumber)
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_VERIFICATION));
}
}
Loading
Loading