Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package trible.histour.application.domain.mission;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
Expand All @@ -10,12 +11,15 @@
import lombok.val;
import trible.histour.application.domain.quiz.Quiz;
import trible.histour.application.port.input.MissionUseCase;
import trible.histour.application.port.input.dto.request.mission.UpdatedMissionsRequest;
import trible.histour.application.port.input.dto.response.mission.MissionsResponse;
import trible.histour.application.port.output.persistence.MemberMissionPort;
import trible.histour.application.port.output.persistence.MemberQuizPort;
import trible.histour.application.port.output.persistence.MissionPort;
import trible.histour.application.port.output.persistence.PlacePort;
import trible.histour.application.port.output.persistence.QuizPort;
import trible.histour.common.exception.ExceptionCode;
import trible.histour.common.exception.HistourException;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -63,9 +67,64 @@ public MissionsResponse getMissions(long memberId, long placeId) {

@Transactional
@Override
public void completeMemberMission(long memberId, long missionId) {
val memberMission = memberMissionPort.findByMemberIdAndMissionId(memberId, missionId);
memberMission.complete();
memberMissionPort.update(memberMission);
public void completeMemberMission(long memberId, UpdatedMissionsRequest request) {
val completeMissionId = request.completedMissionId();
val completedMemberMission = memberMissionPort.findByMemberIdAndMissionId(memberId, completeMissionId);
completedMemberMission.complete();
memberMissionPort.update(completedMemberMission);

val nextMissionId = request.nextMissionId();
val mission = missionPort.findById(nextMissionId);
val missions = missionPort.findAllByPlaceId(mission.getPlaceId());
val missionIds = missions.stream().map(Mission::getId).toList();
val memberMissions = memberMissionPort.findAllByMemberIdAndMissionIds(memberId, missionIds);
validMemberQuiz(memberId, mission, memberMissions, missions);
memberMissionPort.save(memberId, nextMissionId);
}

private void validMemberQuiz(
long memberId,
Mission mission,
List<MemberMission> memberMissions,
List<Mission> missions
) {
val completedMemberMissions = memberMissions.stream().filter(MemberMission::isCompleted).toList();

memberMissions.stream()
.filter(it -> it.getMissionId() == mission.getId())
.findAny()
.orElseThrow(() -> new HistourException(
ExceptionCode.NOT_UNLOCK_MISSION,
"MissionID: " + mission.getId() + ", MemberID: " + memberId));

completedMemberMissions.stream()
.filter(it -> it.getMissionId() == mission.getId())
.findAny()
.ifPresent(it -> {
throw new HistourException(
ExceptionCode.BAD_REQUEST,
"이미 완료된 미션, MissionID: " + mission.getId() + ", MemberID: " + memberId);
});

if (mission.getType() == MissionType.NORMAL) {
val missionById = missions.stream().collect(Collectors.toMap(Mission::getId, it -> it));
completedMemberMissions.stream()
.filter(it -> missionById.get(it.getMissionId()).getType() == MissionType.INTRO)
.findAny()
.orElseThrow(() -> new HistourException(
ExceptionCode.BAD_REQUEST,
"Intro 미션을 수행하지 않음, MissionID: " + mission.getId() + ", MemberID: " + memberId));
}

if (mission.getType() == MissionType.FINAL) {
val place = placePort.findById(mission.getPlaceId());
if (completedMemberMissions.size() != place.getRequiredMissionCount() - 1) {
throw new HistourException(
ExceptionCode.BAD_REQUEST,
"Final 미션 전 필요한 완료 미션 개수가 부족함 ("
+ completedMemberMissions.size() + "/" + (place.getRequiredMissionCount() - 1)
+ "), MemberID: " + memberId);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package trible.histour.application.domain.quiz;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import lombok.val;
import trible.histour.application.domain.mission.MemberMission;
import trible.histour.application.domain.mission.Mission;
import trible.histour.application.domain.mission.MissionState;
import trible.histour.application.domain.mission.MissionType;
import trible.histour.application.port.input.QuizUseCase;
import trible.histour.application.port.input.dto.request.quiz.QuizGradeRequest;
import trible.histour.application.port.input.dto.response.quiz.QuizGradeResponse;
Expand All @@ -38,16 +35,6 @@ public class QuizService implements QuizUseCase {
@Override
public QuizzesResponse getQuizzes(long memberId, long missionId) {
val mission = missionPort.findById(missionId);
val missions = missionPort.findAllByPlaceId(mission.getPlaceId());
val missionIds = missions.stream().map(Mission::getId).toList();
val memberMissions = memberMissionPort.findAllByMemberIdAndMissionIds(memberId, missionIds);
validMemberQuiz(memberId, mission, memberMissions, missions);

memberMissions.stream()
.filter(memberMission -> memberMission.getMissionId() == missionId)
.findAny()
.orElseGet(() -> memberMissionPort.save(new MemberMission(memberId, missionId)));

val quizzes = quizPort.findAllByMissionId(missionId);
return QuizzesResponse.of(mission, quizzes);
}
Expand All @@ -67,58 +54,19 @@ public QuizGradeResponse gradeQuiz(long memberId, QuizGradeRequest quizGradeRequ
return QuizGradeResponse.of(isAnswerCorrect, clearMissionCount, requiredMissionCount);
}

private void validMemberQuiz(
long memberId,
Mission mission,
List<MemberMission> memberMissions,
List<Mission> missions
) {
val completedMemberMissions = memberMissions.stream().filter(MemberMission::isCompleted).toList();

completedMemberMissions.stream()
.filter(it -> it.getMissionId() == mission.getId())
.findAny()
.ifPresent(it -> {
throw new HistourException(
ExceptionCode.BAD_REQUEST,
"이미 완료된 미션, MissionID: " + mission.getId() + ", MemberID: " + memberId);
});

if (mission.getType() == MissionType.NORMAL) {
val missionById = missions.stream().collect(Collectors.toMap(Mission::getId, it -> it));
completedMemberMissions.stream()
.filter(it -> missionById.get(it.getMissionId()).getType() == MissionType.INTRO)
.findAny()
.orElseThrow(() -> new HistourException(
ExceptionCode.BAD_REQUEST,
"Intro 미션을 수행하지 않음, MissionID: " + mission.getId() + ", MemberID: " + memberId));
}

if (mission.getType() == MissionType.FINAL) {
val place = placePort.findById(mission.getPlaceId());
if (completedMemberMissions.size() != place.getRequiredMissionCount() - 1) {
throw new HistourException(
ExceptionCode.BAD_REQUEST,
"Final 미션 전 필요한 완료 미션 개수가 부족함 ("
+ completedMemberMissions.size() + "/" + (place.getRequiredMissionCount() - 1)
+ "), MemberID: " + memberId);
}
}
}

private Integer getPlaceClearMission(long memberId, long placeId, boolean isLastQuiz) {
if (!isLastQuiz) {
return null;
}
val placeMissions = missionPort.findAllByPlaceId(placeId);
val memberCompleteMissions = memberMissionPort.findAllByMemberIdAndState(memberId, MissionState.COMPLETE);
val placeMissionIds = placeMissions.stream()
.map(Mission::getId)
.collect(Collectors.toSet());
.map(Mission::getId)
.collect(Collectors.toSet());

val completeMissionNumber = memberCompleteMissions.stream()
.filter(memberMission -> placeMissionIds.contains(memberMission.getMissionId()))
.count();
.filter(memberMission -> placeMissionIds.contains(memberMission.getMissionId()))
.count();
return (int) completeMissionNumber;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package trible.histour.application.port.input;

import trible.histour.application.port.input.dto.request.mission.UpdatedMissionsRequest;
import trible.histour.application.port.input.dto.response.mission.MissionsResponse;

public interface MissionUseCase {
MissionsResponse getMissions(long memberId, long placeId);

void completeMemberMission(long memberId, long missionId);
void completeMemberMission(long memberId, UpdatedMissionsRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package trible.histour.application.port.input.dto.request.mission;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "미션 해금 요청 정보")
public record UpdatedMissionsRequest(
@Schema(description = "완료한 서브미션 id", example = "1")
long completedMissionId,
@Schema(description = "선택된 다음 서브미션 id", example = "1")
long nextMissionId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
public interface MemberMissionPort {
List<MemberMission> findAllByMemberIdAndMissionIds(long memberId, List<Long> missionIds);

MemberMission save(MemberMission memberMission);
MemberMission save(long memberId, long missionId);

List<MemberMission> findAllByMemberId(long memberId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum ExceptionCode {
// 4xx
UNAUTHORIZED(401, "유효하지 않은 토큰"),
BAD_REQUEST(400, "유효하지 않은 요청"),
NOT_UNLOCK_MISSION(400, "해금하지 않은 미션"),
NO_CHARACTER(400, "캐릭터 설정이 필요한 회원"),
NOT_FOUND(404, "존재하지 않는 자원"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import lombok.val;
import trible.histour.application.port.input.MissionUseCase;
import trible.histour.application.port.input.dto.request.mission.UpdatedMissionsRequest;
import trible.histour.application.port.input.dto.response.mission.MissionsResponse;
import trible.histour.input.http.controller.docs.MissionApiDocs;
import trible.histour.input.http.controller.dto.response.SuccessResponse;
Expand All @@ -33,11 +35,11 @@ public SuccessResponse<MissionsResponse> getMissions(Principal principal, @PathV
}

@ResponseStatus(HttpStatus.OK)
@PatchMapping("/{missionId}/member")
@PatchMapping("/unlock")
@Override
public SuccessResponse<?> completeMemberMission(Principal principal, @PathVariable long missionId) {
public SuccessResponse<?> unlockMemberMission(Principal principal, @RequestBody UpdatedMissionsRequest request) {
val memberId = Long.parseLong(principal.getName());
missionUseCase.completeMemberMission(memberId, missionId);
missionUseCase.completeMemberMission(memberId, request);
return SuccessResponse.ofEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
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.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import trible.histour.application.port.input.dto.request.mission.UpdatedMissionsRequest;
import trible.histour.application.port.input.dto.response.mission.MissionsResponse;
import trible.histour.input.http.controller.dto.response.SuccessResponse;

Expand All @@ -31,17 +35,17 @@ SuccessResponse<MissionsResponse> getMissions(

@Operation(
summary = "미션 해금 api",
description = "서브미션을 COMPLETE 상태로 변경합니다.",
description = "완료한 서브미션을 COMPLETE, 다음 선택된 서브미션을 PROGRESS 상태로 업데이트합니다.",
responses = {
@ApiResponse(responseCode = "200", description = "OK success")
}
)
SuccessResponse<?> completeMemberMission(
SuccessResponse<?> unlockMemberMission(
@Parameter(hidden = true) Principal principal,
@Parameter(
description = "미션 id",
@RequestBody(
description = "미션 해금 Request Body",
required = true,
in = ParameterIn.PATH
) long missionId
content = @Content(schema = @Schema(implementation = UpdatedMissionsRequest.class))
) UpdatedMissionsRequest request
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ public List<MemberMission> findAllByMemberIdAndMissionIds(long memberId, List<Lo
}

@Override
public MemberMission save(MemberMission memberMission) {
val memberMissionEntity = new MemberMissionEntity(memberMission);
return memberMissionRepository.save(memberMissionEntity).toDomain();
public MemberMission save(long memberId, long missionId) {
return memberMissionRepository.findByMemberIdAndMissionId(memberId, missionId)
.orElseGet(() -> memberMissionRepository.save(new MemberMissionEntity(memberId, missionId)))
.toDomain();
}

@Override
Expand All @@ -48,13 +49,18 @@ public MemberMission findByMemberIdAndMissionId(long memberId, long missionId) {

@Override
public void update(MemberMission memberMission) {
val memberMissionEntity = new MemberMissionEntity(memberMission);
val memberMissionEntity = find(memberMission.getId());
memberMissionEntity.update(memberMission);
}

@Override
public List<MemberMission> findAllByMemberIdAndState(long memberId, MissionState missionState) {
return memberMissionRepository.findAllByMemberIdAndState(memberId, missionState)
.stream().map(MemberMissionEntity::toDomain).toList();
.stream().map(MemberMissionEntity::toDomain).toList();
}

private MemberMissionEntity find(long id) {
return memberMissionRepository.findById(id)
.orElseThrow(() -> new HistourException(ExceptionCode.NOT_FOUND, "MemberMission ID: " + id));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public class MemberMissionEntity extends BaseEntity {
@Column(nullable = false)
private MissionState state;

public MemberMissionEntity(MemberMission memberMission) {
this.memberId = memberMission.getMemberId();
this.missionId = memberMission.getMissionId();
this.state = memberMission.getState();
public MemberMissionEntity(long memberId, long missionId) {
this.memberId = memberId;
this.missionId = missionId;
this.state = MissionState.PROGRESS;
}

public MemberMission toDomain() {
Expand Down