diff --git a/src/main/java/com/nexters/goalpanzi/application/auth/AuthService.java b/src/main/java/com/nexters/goalpanzi/application/auth/AuthService.java index cb5fe54b..aa0705a5 100644 --- a/src/main/java/com/nexters/goalpanzi/application/auth/AuthService.java +++ b/src/main/java/com/nexters/goalpanzi/application/auth/AuthService.java @@ -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; @@ -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); } diff --git a/src/main/java/com/nexters/goalpanzi/application/auth/dto/request/GoogleLoginCommand.java b/src/main/java/com/nexters/goalpanzi/application/auth/dto/request/GoogleLoginCommand.java index cb3ea85a..f2a60150 100644 --- a/src/main/java/com/nexters/goalpanzi/application/auth/dto/request/GoogleLoginCommand.java +++ b/src/main/java/com/nexters/goalpanzi/application/auth/dto/request/GoogleLoginCommand.java @@ -3,7 +3,6 @@ import jakarta.validation.constraints.NotEmpty; public record GoogleLoginCommand( - @NotEmpty String identityToken, @NotEmpty String email ) { } \ No newline at end of file diff --git a/src/main/java/com/nexters/goalpanzi/application/auth/google/GoogleIdentityToken.java b/src/main/java/com/nexters/goalpanzi/application/auth/google/GoogleIdentityToken.java new file mode 100644 index 00000000..45da44d4 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/auth/google/GoogleIdentityToken.java @@ -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); + } + } +} diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java index c5656132..a2fbeece 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java @@ -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; @@ -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 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 missionMembers = missionMemberRepository.findAllByMissionId(command.missionId()); - List verifications = missionVerificationRepository.findAllByMissionIdAndDate(command.missionId(), date); + public List getVerifications(final MissionVerificationQuery query) { + LocalDate date = query.date() != null ? query.date() : LocalDate.now(); + Member member = memberRepository.getMember(query.memberId()); + List verifications = missionVerificationRepository.findAllByMissionIdAndDate(query.missionId(), date); Map 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(); } } diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionVerificationCommand.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionVerificationQuery.java similarity index 80% rename from src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionVerificationCommand.java rename to src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionVerificationQuery.java index cd2270a7..ec791f97 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionVerificationCommand.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionVerificationQuery.java @@ -2,7 +2,7 @@ import java.time.LocalDate; -public record MissionVerificationCommand( +public record MissionVerificationQuery( Long memberId, Long missionId, LocalDate date diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MyMissionVerificationCommand.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MyMissionVerificationQuery.java similarity index 76% rename from src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MyMissionVerificationCommand.java rename to src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MyMissionVerificationQuery.java index afdbfc3b..c536fb4d 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MyMissionVerificationCommand.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MyMissionVerificationQuery.java @@ -1,6 +1,6 @@ package com.nexters.goalpanzi.application.mission.dto.request; -public record MyMissionVerificationCommand( +public record MyMissionVerificationQuery( Long memberId, Long missionId, Integer number diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionResponse.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionResponse.java index d95780b0..64d81ffe 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionResponse.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionResponse.java @@ -8,4 +8,4 @@ public record MissionResponse( @Schema(description = "목표 행동", requiredMode = Schema.RequiredMode.REQUIRED) String description ) { -} +} \ No newline at end of file diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionVerificationResponse.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionVerificationResponse.java index 8b71f39d..ef4dd327 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionVerificationResponse.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionVerificationResponse.java @@ -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; @@ -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); } } diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionsResponse.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionsResponse.java index 8eb1c5c9..797077e7 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionsResponse.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionsResponse.java @@ -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; diff --git a/src/main/java/com/nexters/goalpanzi/application/ncp/ObjectStorageClient.java b/src/main/java/com/nexters/goalpanzi/application/ncp/ObjectStorageClient.java new file mode 100644 index 00000000..bb554193 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/ncp/ObjectStorageClient.java @@ -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); +} diff --git a/src/main/java/com/nexters/goalpanzi/config/NcpConfig.java b/src/main/java/com/nexters/goalpanzi/config/NcpConfig.java index 0b73c197..510d0f5a 100644 --- a/src/main/java/com/nexters/goalpanzi/config/NcpConfig.java +++ b/src/main/java/com/nexters/goalpanzi/config/NcpConfig.java @@ -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 diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/DayOfWeek.java b/src/main/java/com/nexters/goalpanzi/domain/mission/DayOfWeek.java index 1584736d..56822eca 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/DayOfWeek.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/DayOfWeek.java @@ -7,6 +7,5 @@ public enum DayOfWeek { THURSDAY, FRIDAY, SATURDAY, - SUNDAY - ; + SUNDAY; } diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java index 575401f4..f7528f23 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java @@ -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; @@ -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())); + } } diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionVerification.java b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionVerification.java index 89263305..5d661af2 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionVerification.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionVerification.java @@ -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; } } diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionMemberRepository.java b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionMemberRepository.java index 47af4f79..377dce7a 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionMemberRepository.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionMemberRepository.java @@ -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; @@ -14,4 +16,9 @@ public interface MissionMemberRepository extends JpaRepository findAllByMissionId(final Long MissionId); List 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)); + } } diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionVerificationRepository.java b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionVerificationRepository.java index e8a850e4..c06d8e11 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionVerificationRepository.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionVerificationRepository.java @@ -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; @@ -17,9 +19,14 @@ public interface MissionVerificationRepository extends JpaRepository 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 findAllByMissionIdAndDate(final Long missionId, final LocalDate date); + @Query("SELECT mv FROM MissionVerification mv WHERE mv.mission.id = :missionId AND Date(mv.createdAt) = :date") + List 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 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 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)); + } } \ No newline at end of file diff --git a/src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java b/src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java index fb409422..a8be5a19 100644 --- a/src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java +++ b/src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java @@ -25,15 +25,20 @@ public enum ErrorCode { // MISSION MEMBER ALREADY_EXISTS_MISSION_MEMBER("이미 참여한 미션입니다. [%s]"), + NOT_JOINED_MISSION_MEMBER("해당 미션에 참여하지 않았습니다."), // MISSION VERIFICATION NOT_FOUND_VERIFICATION("존재하지 않는 미션 인증입니다."), DUPLICATE_VERIFICATION("이미 인증한 미션이므로 더 이상 인증할 수 없습니다."), ALREADY_COMPLETED_MISSION("이미 완료된 미션이므로 더 이상 인증할 수 없습니다."), + NOT_VERIFICATION_DAY("인증 일자가 아니므로 인증할 수 없습니다."), // FILE UPLOAD INVALID_FILE("유효하지 않은 파일입니다."), - FILE_UPLOAD_FAILED("파일 업로드에 실패했습니다."); + FILE_UPLOAD_FAILED("파일 업로드에 실패하였습니다. [%s]"), + + // ETC + FAILED_TO_GENERATE_HASH("해시값을 생성하는 데 실패하였습니다."); private String message; diff --git a/src/main/java/com/nexters/goalpanzi/exception/GlobalExceptionHandler.java b/src/main/java/com/nexters/goalpanzi/exception/GlobalExceptionHandler.java index 84a6aba8..8fba6225 100644 --- a/src/main/java/com/nexters/goalpanzi/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/nexters/goalpanzi/exception/GlobalExceptionHandler.java @@ -73,7 +73,7 @@ public ResponseEntity handleUnauthorizedException(final RuntimeEx .body(new ErrorResponse(HttpStatus.UNAUTHORIZED.value(), exception.getMessage())); } - @ExceptionHandler({IllegalArgumentException.class}) + @ExceptionHandler({BadRequestException.class, IllegalArgumentException.class}) public ResponseEntity handleBadRequestException(final RuntimeException exception) { logger.error("message", exception); diff --git a/src/main/java/com/nexters/goalpanzi/infrastructure/ncp/ObjectStorageManager.java b/src/main/java/com/nexters/goalpanzi/infrastructure/aws/S3Client.java similarity index 80% rename from src/main/java/com/nexters/goalpanzi/infrastructure/ncp/ObjectStorageManager.java rename to src/main/java/com/nexters/goalpanzi/infrastructure/aws/S3Client.java index dfc248d1..6b04eed4 100644 --- a/src/main/java/com/nexters/goalpanzi/infrastructure/ncp/ObjectStorageManager.java +++ b/src/main/java/com/nexters/goalpanzi/infrastructure/aws/S3Client.java @@ -1,10 +1,11 @@ -package com.nexters.goalpanzi.infrastructure.ncp; +package com.nexters.goalpanzi.infrastructure.aws; import com.amazonaws.SdkClientException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.transfer.TransferManager; import com.amazonaws.services.s3.transfer.TransferManagerBuilder; import com.amazonaws.services.s3.transfer.Upload; +import com.nexters.goalpanzi.application.ncp.ObjectStorageClient; import com.nexters.goalpanzi.exception.BadRequestException; import com.nexters.goalpanzi.exception.BaseException; import jakarta.annotation.PostConstruct; @@ -22,18 +23,18 @@ @RequiredArgsConstructor @Component -public class ObjectStorageManager { +public class S3Client implements ObjectStorageClient { @Value("${cloud.aws.credentials.bucket}") private String bucketName; - private final AmazonS3 s3Client; + private final AmazonS3 amazonS3; private TransferManager tm; @PostConstruct private void buildTransferManager() { - tm = TransferManagerBuilder.standard().withS3Client(s3Client).build(); + tm = TransferManagerBuilder.standard().withS3Client(amazonS3).build(); } public String uploadFile(final MultipartFile file) { @@ -42,16 +43,16 @@ public String uploadFile(final MultipartFile file) { Upload upload = tm.upload(bucketName, fileObjKeyName, convert(file)); upload.waitForCompletion(); - return s3Client.getUrl(bucketName, fileObjKeyName).toString(); + return amazonS3.getUrl(bucketName, fileObjKeyName).toString(); } catch (SdkClientException | InterruptedException e) { - throw new BaseException(FILE_UPLOAD_FAILED); + throw new BaseException(FILE_UPLOAD_FAILED, e); } } public void deleteFile(final String uploadedFileUrl) { if (uploadedFileUrl != null) { String fileObjKeyName = uploadedFileUrl.substring(uploadedFileUrl.lastIndexOf("/") + 1); - s3Client.deleteObject(bucketName, fileObjKeyName); + amazonS3.deleteObject(bucketName, fileObjKeyName); } } diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationController.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationController.java index 635f2ce3..a136fbdf 100644 --- a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationController.java +++ b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationController.java @@ -1,17 +1,16 @@ package com.nexters.goalpanzi.presentation.mission; import com.nexters.goalpanzi.application.mission.MissionVerificationService; -import com.nexters.goalpanzi.application.mission.dto.request.MissionVerificationCommand; +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.response.MissionVerificationResponse; -import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse; -import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationCommand; import com.nexters.goalpanzi.common.argumentresolver.LoginMemberId; -import com.nexters.goalpanzi.presentation.mission.dto.CreateMissionVerificationRequest; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; import java.util.List; @@ -23,13 +22,13 @@ public class MissionVerificationController implements MissionVerificationControl private final MissionVerificationService missionVerificationService; - @GetMapping + @GetMapping("/{missionId}/verifications") public ResponseEntity> getVerifications( @LoginMemberId final Long memberId, @PathVariable(name = "missionId") final Long missionId, @RequestParam(name = "date", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) final LocalDate date ) { - List response = missionVerificationService.getVerifications(new MissionVerificationCommand(memberId, missionId, date)); + List response = missionVerificationService.getVerifications(new MissionVerificationQuery(memberId, missionId, date)); return ResponseEntity.ok(response); } @@ -40,17 +39,17 @@ public ResponseEntity getMyVerification( @PathVariable(name = "missionId") final Long missionId, @PathVariable(name = "number") final Integer number) { MissionVerificationResponse response = missionVerificationService.getMyVerification( - new MyMissionVerificationCommand(memberId, missionId, number)); + new MyMissionVerificationQuery(memberId, missionId, number)); return ResponseEntity.ok(response); } - @PostMapping("/{missionId}/verifications/me") + @PostMapping(value = "/{missionId}/verifications/me") public ResponseEntity createVerification( @LoginMemberId final Long memberId, @PathVariable(name = "missionId") final Long missionId, - @RequestBody @Valid final CreateMissionVerificationRequest request) { - missionVerificationService.createVerification(request.toServiceDto(memberId, missionId)); + @RequestPart(name = "imageFile") final MultipartFile imageFile) { + missionVerificationService.createVerification(new CreateMissionVerificationCommand(memberId, missionId, imageFile)); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationControllerDocs.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationControllerDocs.java index 301c5585..da8ed7bd 100644 --- a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationControllerDocs.java +++ b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationControllerDocs.java @@ -1,9 +1,7 @@ package com.nexters.goalpanzi.presentation.mission; import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse; -import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse; import com.nexters.goalpanzi.common.argumentresolver.LoginMemberId; -import com.nexters.goalpanzi.presentation.mission.dto.CreateMissionVerificationRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -11,10 +9,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; import java.util.List; @@ -34,12 +32,12 @@ public interface MissionVerificationControllerDocs { @ApiResponse(responseCode = "200"), @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), }) - @GetMapping + @GetMapping("/{missionId}/verifications") ResponseEntity> getVerifications( @Parameter(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", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @Schema(description = "미션 인증 일자", type = "string", format = "date", requiredMode = Schema.RequiredMode.REQUIRED) @RequestParam(name = "date", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) final LocalDate date ); @@ -49,12 +47,12 @@ ResponseEntity> getVerifications( @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), @ApiResponse(responseCode = "404", description = "Not Found - 정보에 해당하는 이미지가 존재하지 않음", content = @Content(schema = @Schema(hidden = true))), }) - @GetMapping("/me/{number}") + @GetMapping("/{missionId}/verifications/me/{number}") ResponseEntity getMyVerification( @Parameter(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) + @Schema(description = "보드칸 번호", type = "integer", format = "int32", requiredMode = Schema.RequiredMode.REQUIRED) @PathVariable(name = "number") final Integer number); @Operation(summary = "미션 인증", description = "미션 인증을 위해 이미지를 업로드합니다.") @@ -64,10 +62,11 @@ ResponseEntity getMyVerification( @ApiResponse(responseCode = "401"), @ApiResponse(responseCode = "404", description = "Not Found - 정보에 해당하는 미션이 존재하지 않음"), }) - @PostMapping("/me") + @PostMapping(value = "/{missionId}/verifications/me") ResponseEntity createVerification( @Parameter(hidden = true) @LoginMemberId final Long memberId, @Schema(description = "미션 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED) @PathVariable(name = "missionId") final Long missionId, - @RequestBody @Valid final CreateMissionVerificationRequest request); + @Schema(description = "인증 이미지 파일", requiredMode = Schema.RequiredMode.REQUIRED) + @RequestPart(name = "imageFile") final MultipartFile imageFile); } \ No newline at end of file diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/dto/CreateMissionVerificationRequest.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/dto/CreateMissionVerificationRequest.java deleted file mode 100644 index a2bd3276..00000000 --- a/src/main/java/com/nexters/goalpanzi/presentation/mission/dto/CreateMissionVerificationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.nexters.goalpanzi.presentation.mission.dto; - -import com.nexters.goalpanzi.application.mission.dto.request.CreateMissionVerificationCommand; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import org.springframework.web.multipart.MultipartFile; - -public record CreateMissionVerificationRequest( - @Schema(description = "인증 이미지 파일", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull MultipartFile imageFile -) { - - public CreateMissionVerificationCommand toServiceDto(final Long memberId, final Long missionId) { - return new CreateMissionVerificationCommand(memberId, missionId, imageFile); - } -} diff --git a/src/test/java/com/nexters/goalpanzi/acceptance/AcceptanceStep.java b/src/test/java/com/nexters/goalpanzi/acceptance/AcceptanceStep.java index d829f589..5981b30d 100644 --- a/src/test/java/com/nexters/goalpanzi/acceptance/AcceptanceStep.java +++ b/src/test/java/com/nexters/goalpanzi/acceptance/AcceptanceStep.java @@ -11,7 +11,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -74,4 +77,39 @@ public class AcceptanceStep { .statusCode(HttpStatus.OK.value()) .extract(); } + + public static ExtractableResponse 미션_인증(MultipartFile imageFile, Long missionId, String accessToken) { + try { + return RestAssured.given().log().all() + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .header(HttpHeaders.AUTHORIZATION, BEARER + accessToken) + .multiPart("imageFile", imageFile.getOriginalFilename(), imageFile.getInputStream(), imageFile.getContentType()) + .when().post("/api/missions/" + missionId + "/verifications/me") + .then().log().all() + .extract(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static ExtractableResponse 일자별_미션_인증_조회(Long missionId, LocalDate date, String accessToken) { + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.AUTHORIZATION, BEARER + accessToken) + .queryParam("date", date.toString()) + .when().get("/api/missions/" + missionId + "/verifications") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + } + + public static ExtractableResponse 내_미션_인증_조회(Integer number, Long missionId, String accessToken) { + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.AUTHORIZATION, BEARER + accessToken) + .when().get("/api/missions/" + missionId + "/verifications/me/" + number) + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + } } diff --git a/src/test/java/com/nexters/goalpanzi/acceptance/LoginAcceptanceTest.java b/src/test/java/com/nexters/goalpanzi/acceptance/LoginAcceptanceTest.java index 042db9b2..765fea24 100644 --- a/src/test/java/com/nexters/goalpanzi/acceptance/LoginAcceptanceTest.java +++ b/src/test/java/com/nexters/goalpanzi/acceptance/LoginAcceptanceTest.java @@ -17,7 +17,6 @@ import java.security.NoSuchAlgorithmException; import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL; -import static com.nexters.goalpanzi.fixture.MemberFixture.ID_TOKEN; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; @@ -58,8 +57,8 @@ public class LoginAcceptanceTest extends AcceptanceTest { } @Test - void 사용자가_구글_로그인을_정상적으로_한다() { - GoogleLoginCommand request = new GoogleLoginCommand(ID_TOKEN, EMAIL); + void 사용자가_구글_로그인을_정상적으로_한다() { + GoogleLoginCommand request = new GoogleLoginCommand(EMAIL); LoginResponse actual = RestAssured.given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) diff --git a/src/test/java/com/nexters/goalpanzi/acceptance/MissionAcceptanceTest.java b/src/test/java/com/nexters/goalpanzi/acceptance/MissionAcceptanceTest.java index d5e06612..61558523 100644 --- a/src/test/java/com/nexters/goalpanzi/acceptance/MissionAcceptanceTest.java +++ b/src/test/java/com/nexters/goalpanzi/acceptance/MissionAcceptanceTest.java @@ -13,7 +13,6 @@ import static com.nexters.goalpanzi.acceptance.AcceptanceStep.*; import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL; -import static com.nexters.goalpanzi.fixture.MemberFixture.ID_TOKEN; import static com.nexters.goalpanzi.fixture.MissionFixture.DESCRIPTION; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -22,7 +21,7 @@ public class MissionAcceptanceTest extends AcceptanceTest { @Test void 미션을_생성한다() { - LoginResponse loginResponse = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class); + LoginResponse loginResponse = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); CreateMissionRequest request = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(5), TimeOfDay.EVERYDAY, List.of(DayOfWeek.FRIDAY), 5); @@ -36,7 +35,7 @@ public class MissionAcceptanceTest extends AcceptanceTest { @Test void 미션을_조회한다() { - LoginResponse login = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class); + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); CreateMissionRequest request = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(5), TimeOfDay.EVERYDAY, diff --git a/src/test/java/com/nexters/goalpanzi/acceptance/MissionMemberAcceptanceTest.java b/src/test/java/com/nexters/goalpanzi/acceptance/MissionMemberAcceptanceTest.java index 6a941d98..54bdf8f9 100644 --- a/src/test/java/com/nexters/goalpanzi/acceptance/MissionMemberAcceptanceTest.java +++ b/src/test/java/com/nexters/goalpanzi/acceptance/MissionMemberAcceptanceTest.java @@ -4,29 +4,23 @@ import com.nexters.goalpanzi.application.auth.dto.response.LoginResponse; import com.nexters.goalpanzi.application.mission.dto.response.MissionDetailResponse; import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse; -import com.nexters.goalpanzi.infrastructure.ncp.ObjectStorageManager; import com.nexters.goalpanzi.presentation.mission.dto.JoinMissionRequest; import io.restassured.RestAssured; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import static com.nexters.goalpanzi.acceptance.AcceptanceStep.*; import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL; -import static com.nexters.goalpanzi.fixture.MemberFixture.ID_TOKEN; import static com.nexters.goalpanzi.fixture.TokenFixture.BEARER; import static org.assertj.core.api.Assertions.assertThat; public class MissionMemberAcceptanceTest extends AcceptanceTest { - @MockBean - ObjectStorageManager objectStorageManager; - @Test void 초대코드로_미션에_참여한다() { - LoginResponse login = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class); + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); MissionDetailResponse mission = 미션_생성(login.accessToken()).as(MissionDetailResponse.class); JoinMissionRequest joinRequest = new JoinMissionRequest(mission.invitationCode()); @@ -40,8 +34,8 @@ public class MissionMemberAcceptanceTest extends AcceptanceTest { } @Test - void 참여하고있는_미션을_조회한다() { - LoginResponse login = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class); + void 참여하고_있는_미션을_조회한다() { + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); MissionDetailResponse mission = 미션_생성(login.accessToken()).as(MissionDetailResponse.class); 미션_참여(mission.invitationCode(), login.accessToken()); diff --git a/src/test/java/com/nexters/goalpanzi/acceptance/MissionVerificationAcceptanceTest.java b/src/test/java/com/nexters/goalpanzi/acceptance/MissionVerificationAcceptanceTest.java new file mode 100644 index 00000000..6a559a95 --- /dev/null +++ b/src/test/java/com/nexters/goalpanzi/acceptance/MissionVerificationAcceptanceTest.java @@ -0,0 +1,146 @@ +package com.nexters.goalpanzi.acceptance; + +import com.nexters.goalpanzi.application.auth.dto.request.GoogleLoginCommand; +import com.nexters.goalpanzi.application.auth.dto.response.LoginResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionDetailResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse; +import com.nexters.goalpanzi.application.ncp.ObjectStorageClient; +import com.nexters.goalpanzi.domain.mission.DayOfWeek; +import com.nexters.goalpanzi.domain.mission.TimeOfDay; +import com.nexters.goalpanzi.exception.ErrorCode; +import com.nexters.goalpanzi.presentation.mission.dto.CreateMissionRequest; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static com.nexters.goalpanzi.acceptance.AcceptanceStep.*; +import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL; +import static com.nexters.goalpanzi.fixture.MissionFixture.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class MissionVerificationAcceptanceTest extends AcceptanceTest { + + @MockBean + private ObjectStorageClient objectStorageClient; + + @Test + void 미션_인증에_성공한다() { + when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); + + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 1); + MissionDetailResponse mission = 미션_생성(missionRequest, login.accessToken()).as(MissionDetailResponse.class); + 미션_참여(mission.invitationCode(), login.accessToken()); + + ExtractableResponse response = 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); + + assertThat(response.statusCode()).isEqualTo(200); + } + + @Test + void 지정한_인증_일자가_아니므로_인증에_실패한다() { + when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); + + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); + LocalDate today = LocalDate.now(); + List missionDays = WEEK.stream().filter(d -> d != DayOfWeek.valueOf(today.getDayOfWeek().name())).toList(); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, missionDays, 1); + MissionDetailResponse mission = 미션_생성(missionRequest, login.accessToken()).as(MissionDetailResponse.class); + 미션_참여(mission.invitationCode(), login.accessToken()); + + ExtractableResponse response = 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); + + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(400), + () -> assertThat(response.jsonPath().getString("message")).isEqualTo(ErrorCode.NOT_VERIFICATION_DAY.getMessage()) + ); + } + + @Test + void 이미_완료된_미션이므로_인증에_실패한다() { + when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); + + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 1); + MissionDetailResponse mission = 미션_생성(missionRequest, login.accessToken()).as(MissionDetailResponse.class); + 미션_참여(mission.invitationCode(), login.accessToken()); + + ExtractableResponse firstResponse = 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); + ExtractableResponse secondResponse = 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); + + assertAll( + () -> assertThat(firstResponse.statusCode()).isEqualTo(200), + () -> assertThat(secondResponse.statusCode()).isEqualTo(400), + () -> assertThat(secondResponse.jsonPath().getString("message")).isEqualTo(ErrorCode.ALREADY_COMPLETED_MISSION.getMessage()) + ); + } + + @Test + void 이미_인증한_미션이므로_인증에_실패한다() { + when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); + + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 2); + MissionDetailResponse mission = 미션_생성(missionRequest, login.accessToken()).as(MissionDetailResponse.class); + 미션_참여(mission.invitationCode(), login.accessToken()); + + ExtractableResponse firstResponse = 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); + ExtractableResponse secondResponse = 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); + + assertAll( + () -> assertThat(firstResponse.statusCode()).isEqualTo(200), + () -> assertThat(secondResponse.statusCode()).isEqualTo(400), + () -> assertThat(secondResponse.jsonPath().getString("message")).isEqualTo(ErrorCode.DUPLICATE_VERIFICATION.getMessage()) + ); + } + +// TODO 프로필 생성 후 확인 필요 +// @Test +// void 특정_일자의_미션_인증_현황을_조회한다() { +// when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); +// +// LoginResponse login1 = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); +// +// CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 1); +// MissionDetailResponse mission = 미션_생성(missionRequest, login1.accessToken()).as(MissionDetailResponse.class); +// 미션_참여(mission.invitationCode(), login1.accessToken()); +// 미션_인증(IMAGE_FILE, mission.missionId(), login1.accessToken()); +// +// LoginResponse login2 = 구글_로그인(new GoogleLoginCommand(EMAIL2)).as(LoginResponse.class); +// 미션_참여(mission.invitationCode(), login2.accessToken()); +// 미션_인증(IMAGE_FILE, mission.missionId(), login2.accessToken()); +// +// List verifications = 일자별_미션_인증_조회(mission.missionId(), LocalDate.now(), login1.accessToken()).as(List.class); +// +// assertAll( +// () -> assertThat(verifications.size()).isEqualTo(2) +// ); +// } + + @Test + void 보드칸_번호에_해당하는_나의_미션_인증_내역을_조회한다() { + when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); + + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 1); + MissionDetailResponse mission = 미션_생성(missionRequest, login.accessToken()).as(MissionDetailResponse.class); + 미션_참여(mission.invitationCode(), login.accessToken()); + 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); + + MissionVerificationResponse verification = 내_미션_인증_조회(1, mission.missionId(), login.accessToken()).as(MissionVerificationResponse.class); + + assertAll( + // TODO 추후 닉네임, 장기말 타입 검증도 추가 + () -> assertThat(verification.imageUrl()).isEqualTo(UPLOADED_IMAGE_URL) + ); + } +} diff --git a/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java b/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java index 5015bd17..20822f76 100644 --- a/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java +++ b/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java @@ -23,7 +23,8 @@ class MissionTest { LocalDateTime.now().plusDays(7), TimeOfDay.EVERYDAY, List.of(DayOfWeek.FRIDAY), - BOARD_COUNT + BOARD_COUNT, + InvitationCode.generate() ); assertAll( @@ -42,7 +43,8 @@ class MissionTest { LocalDateTime.now().plusDays(7), TimeOfDay.EVERYDAY, List.of(DayOfWeek.FRIDAY), - 0 + 0, + InvitationCode.generate() )); } @@ -55,7 +57,8 @@ class MissionTest { LocalDateTime.now().minusDays(7), TimeOfDay.EVERYDAY, List.of(DayOfWeek.FRIDAY), - BOARD_COUNT + BOARD_COUNT, + InvitationCode.generate() )); } } \ No newline at end of file diff --git a/src/test/java/com/nexters/goalpanzi/fixture/MemberFixture.java b/src/test/java/com/nexters/goalpanzi/fixture/MemberFixture.java index b649dc84..afb7c49a 100644 --- a/src/test/java/com/nexters/goalpanzi/fixture/MemberFixture.java +++ b/src/test/java/com/nexters/goalpanzi/fixture/MemberFixture.java @@ -2,8 +2,8 @@ public class MemberFixture { public static final Long MEMBER_ID = 1L; - public static final String ID_TOKEN = "token"; public static final String EMAIL = "test@gmail.com"; + public static final String EMAIL2 = "test2@gmail.com"; public static final String SOCIAL_ID = "12345"; public static final String NICKNAME = "song2"; } diff --git a/src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java b/src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java index 732323d5..5ccceaa5 100644 --- a/src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java +++ b/src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java @@ -1,6 +1,34 @@ package com.nexters.goalpanzi.fixture; +import com.nexters.goalpanzi.domain.mission.DayOfWeek; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; + +import static com.nexters.goalpanzi.domain.mission.DayOfWeek.*; + public class MissionFixture { public static final String DESCRIPTION = "운동하기"; public static final Integer BOARD_COUNT = 30; + + public static final List WEEK = List.of(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY); + public static final MultipartFile IMAGE_FILE; + public static final String UPLOADED_IMAGE_URL = "uploadedImageUrl"; + + static { + try { + File tempFile = File.createTempFile("미션 인증 이미지", ".jpg"); + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + fos.write("이미지 파일 내용".getBytes()); + } + IMAGE_FILE = new MockMultipartFile("imageFile", tempFile.getName(), "image/jpeg", new FileInputStream(tempFile)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/com/nexters/goalpanzi/infrastructure/ncp/ObjectStorageManagerTest.java b/src/test/java/com/nexters/goalpanzi/infrastructure/aws/S3ClientTest.java similarity index 85% rename from src/test/java/com/nexters/goalpanzi/infrastructure/ncp/ObjectStorageManagerTest.java rename to src/test/java/com/nexters/goalpanzi/infrastructure/aws/S3ClientTest.java index d09d788b..701f2865 100644 --- a/src/test/java/com/nexters/goalpanzi/infrastructure/ncp/ObjectStorageManagerTest.java +++ b/src/test/java/com/nexters/goalpanzi/infrastructure/aws/S3ClientTest.java @@ -1,4 +1,4 @@ -//package com.nexters.goalpanzi.infrastructure.ncp; +//package com.nexters.goalpanzi.infrastructure.aws; // //import org.junit.jupiter.api.AfterEach; //import org.junit.jupiter.api.BeforeEach; @@ -16,10 +16,10 @@ //import static org.assertj.core.api.Assertions.assertThat; // //@SpringBootTest -//class ObjectStorageManagerTest { +//class S3ClientTest { // // @Autowired -// private ObjectStorageManager objectStorageManager; +// private S3Client s3Client; // // private File tempFile; // private MultipartFile imageFile; @@ -43,7 +43,7 @@ // // @AfterEach // public void 테스트_후_업로드된_이미지_파일을_삭제한다() { -// objectStorageManager.deleteFile(uploadedFileUrl); +// s3Client.deleteFile(uploadedFileUrl); // // if (tempFile != null && tempFile.exists()) { // tempFile.delete(); @@ -52,7 +52,7 @@ // // @Test // public void 파일을_업로드한다() { -// uploadedFileUrl = objectStorageManager.uploadFile(imageFile); +// uploadedFileUrl = s3Client.uploadFile(imageFile); // // assertThat(uploadedFileUrl).isNotNull(); //