-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#11 미션 완료 응답에 트랙 완료 여부 추가 및 다음 트랙 전환 API 구현 #13
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.team4.hackerton.domain.mission.dto.response; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public class MissionCompleteResponse { | ||
|
|
||
| @Schema(description = "트랙 완료 및 다음 트랙 전환 여부", example = "true") | ||
| private final boolean trackCompleted; | ||
|
|
||
| @Schema(description = "전환된 다음 트랙 이름 (마지막 트랙 완료 시 null)", example = "바깥으로 나가기", nullable = true) | ||
| private final String nextTrackName; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| import com.team4.hackerton.domain.mission.code.MissionErrorCode; | ||
| import com.team4.hackerton.domain.mission.dto.request.CustomMissionRequest; | ||
| import com.team4.hackerton.domain.mission.dto.response.MissionCompleteResponse; | ||
| import com.team4.hackerton.domain.mission.dto.response.MissionItemResponse; | ||
| import com.team4.hackerton.domain.mission.dto.response.MissionListResponse; | ||
| import com.team4.hackerton.domain.mission.dto.response.MissionProgressResponse; | ||
|
|
@@ -10,9 +11,11 @@ | |
| import com.team4.hackerton.domain.mission.repository.MissionLogRepository; | ||
| import com.team4.hackerton.domain.mission.repository.MissionRepository; | ||
| import com.team4.hackerton.domain.mission.entity.MissionType; | ||
| import com.team4.hackerton.domain.track.code.TrackErrorCode; | ||
| import com.team4.hackerton.domain.track.entity.Track; | ||
| import com.team4.hackerton.domain.track.entity.TrackType; | ||
| import com.team4.hackerton.domain.track.entity.UserTrack; | ||
| import com.team4.hackerton.domain.track.repository.TrackRepository; | ||
| import com.team4.hackerton.domain.track.repository.UserTrackRepository; | ||
| import com.team4.hackerton.domain.user.entity.User; | ||
| import com.team4.hackerton.global.apiPayload.exception.AppException; | ||
|
|
@@ -35,6 +38,7 @@ public class MissionService { | |
| private final MissionRepository missionRepository; | ||
| private final MissionLogRepository missionLogRepository; | ||
| private final UserTrackRepository userTrackRepository; | ||
| private final TrackRepository trackRepository; | ||
|
|
||
| public MissionListResponse getTodayMissions(User user) { | ||
| UserTrack userTrack = userTrackRepository.findByUserAndIsCurrentTrue(user) | ||
|
|
@@ -51,18 +55,18 @@ public MissionListResponse getTodayMissions(User user) { | |
| List<MissionItemResponse> result = new ArrayList<>(); | ||
|
|
||
| if (track.getTrackType() == TrackType.SELF_CARE) { | ||
| missionRepository.findByTrackOrderByIdAsc(track).forEach(mission -> | ||
| missionRepository.findByTrackAndTypeOrderByIdAsc(track, MissionType.TRACK_DEFAULT).forEach(mission -> | ||
| result.add(new MissionItemResponse(mission, completedMissionIds.contains(mission.getId()))) | ||
| ); | ||
| } else { | ||
| List<Mission> commonMissions = missionRepository.findByTrackOrderByIdAsc(track); | ||
| List<Mission> commonMissions = missionRepository.findByTrackAndTypeOrderByIdAsc(track, MissionType.TRACK_DEFAULT); | ||
| if (!commonMissions.isEmpty()) { | ||
| long dayIndex = ChronoUnit.DAYS.between(userTrack.getJoinedAt(), today); | ||
| Mission todayCommon = commonMissions.get((int) (dayIndex % commonMissions.size())); | ||
| result.add(new MissionItemResponse(todayCommon, completedMissionIds.contains(todayCommon.getId()))); | ||
| } | ||
|
|
||
| missionRepository.findTopByUserOrderByCreatedAtDesc(user).ifPresent(custom -> | ||
| missionRepository.findTopByUserAndTrackOrderByCreatedAtDesc(user, track).ifPresent(custom -> | ||
| result.add(new MissionItemResponse(custom, completedMissionIds.contains(custom.getId()))) | ||
| ); | ||
| } | ||
|
|
@@ -71,7 +75,7 @@ public MissionListResponse getTodayMissions(User user) { | |
| } | ||
|
|
||
| @Transactional | ||
| public void completeMission(User user, Long missionId) { | ||
| public MissionCompleteResponse completeMission(User user, Long missionId) { | ||
| UserTrack userTrack = userTrackRepository.findByUserAndIsCurrentTrue(user) | ||
| .orElseThrow(() -> new AppException(MissionErrorCode.USER_TRACK_NOT_FOUND)); | ||
|
|
||
|
|
@@ -86,6 +90,56 @@ public void completeMission(User user, Long missionId) { | |
| } | ||
|
|
||
| missionLogRepository.save(new MissionLog(user, mission, track, today)); | ||
|
|
||
| if (!isDayCompleted(user, track, today, userTrack.getJoinedAt())) { | ||
| return new MissionCompleteResponse(false, null); | ||
| } | ||
|
|
||
| List<LocalDate> allDates = missionLogRepository.findCompletedDatesByUserAndTrack(user, track); | ||
| int completedDays = (int) allDates.stream() | ||
| .filter(date -> isDayCompleted(user, track, date, userTrack.getJoinedAt())) | ||
| .count(); | ||
|
|
||
| if (completedDays < track.getRequiredDays()) { | ||
| return new MissionCompleteResponse(false, null); | ||
| } | ||
|
|
||
| userTrack.complete(today); | ||
|
|
||
| TrackType nextType = track.getTrackType().next(); | ||
| if (nextType == null) { | ||
| return new MissionCompleteResponse(true, null); | ||
| } | ||
|
|
||
| Track nextTrack = trackRepository.findByTrackType(nextType) | ||
| .orElseThrow(() -> new AppException(TrackErrorCode.TRACK_NOT_FOUND)); | ||
| userTrackRepository.save(new UserTrack(user, nextTrack, today)); | ||
|
|
||
| return new MissionCompleteResponse(true, nextType.getDisplayName()); | ||
| } | ||
|
Comment on lines
+107
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Map the service and controller to inspect the relevant methods and docs.
ast-grep outline src/main/java/com/team4/hackerton/domain/mission/service/MissionService.java --view expanded
printf '\n---\n'
ast-grep outline src/main/java/com/team4/hackerton/domain/mission/controller/MissionController.java --view expanded
printf '\n---\n'
ast-grep outline src/main/java/com/team4/hackerton/domain/mission/dto/MissionCompleteResponse.java --view expanded
printf '\n---\n'
ast-grep outline src/main/java/com/team4/hackerton/domain/mission/dto --view expandedRepository: 8th-COKERTHON/server-team4 Length of output: 1598 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Read only the relevant slices after locating line numbers.
sed -n '1,260p' src/main/java/com/team4/hackerton/domain/mission/service/MissionService.java
printf '\n---CONTROLLER---\n'
sed -n '1,260p' src/main/java/com/team4/hackerton/domain/mission/controller/MissionController.javaRepository: 8th-COKERTHON/server-team4 Length of output: 18642
🤖 Prompt for AI Agents |
||
|
|
||
| private boolean isDayCompleted(User user, Track track, LocalDate date, LocalDate joinedAt) { | ||
| List<MissionLog> logs = missionLogRepository.findByUserAndTrackAndPerformedDate(user, track, date); | ||
| Set<Long> completedMissionIds = logs.stream() | ||
| .map(log -> log.getMission().getId()) | ||
| .collect(Collectors.toSet()); | ||
|
|
||
| List<Mission> defaultMissions = missionRepository.findByTrackAndTypeOrderByIdAsc(track, MissionType.TRACK_DEFAULT); | ||
|
|
||
| if (track.getTrackType() == TrackType.SELF_CARE) { | ||
| if (defaultMissions.isEmpty()) return false; | ||
| Set<Long> defaultIds = defaultMissions.stream().map(Mission::getId).collect(Collectors.toSet()); | ||
| return completedMissionIds.containsAll(defaultIds); | ||
| } else { | ||
| if (defaultMissions.isEmpty()) return false; | ||
| long dayIndex = ChronoUnit.DAYS.between(joinedAt, date); | ||
| Mission todayCommon = defaultMissions.get((int) (dayIndex % defaultMissions.size())); | ||
| if (!completedMissionIds.contains(todayCommon.getId())) return false; | ||
|
|
||
| Set<Long> customMissionIds = missionRepository.findByUserAndTrackAndType(user, track, MissionType.USER_CUSTOM) | ||
| .stream().map(Mission::getId).collect(Collectors.toSet()); | ||
| return completedMissionIds.stream().anyMatch(customMissionIds::contains); | ||
| } | ||
|
Comment on lines
+133
to
+142
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the target file around the reported lines
sed -n '1,240p' src/main/java/com/team4/hackerton/domain/mission/service/MissionService.java
# Search for related mission type logic and custom mission limits
rg -n "SELF_CARE|USER_CUSTOM|isDayCompleted|defaultMissions|completedMissionIds|custom mission|customMissions|missionRepository.findByUserAndTrackAndType" src/main/javaRepository: 8th-COKERTHON/server-team4 Length of output: 16602 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,240p' src/main/java/com/team4/hackerton/domain/mission/service/MissionService.java
printf '\n--- SEARCH ---\n'
rg -n "SELF_CARE|USER_CUSTOM|isDayCompleted|defaultMissions|completedMissionIds|custom mission|customMissions|missionRepository.findByUserAndTrackAndType" src/main/javaRepository: 8th-COKERTHON/server-team4 Length of output: 16618 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Map mission-related files and inspect the service and entity definitions.
git ls-files | rg '^src/main/java/.+Mission|^src/main/java/.+Track|^src/main/java/.+MissionType|^src/main/java/.+MissionRepository|^src/main/java/.+TrackRepository'
printf '\n--- MissionService outline ---\n'
ast-grep outline src/main/java/com/team4/hackerton/domain/mission/service/MissionService.java --view expanded || true
printf '\n--- Search for custom-mission constraints ---\n'
rg -n "0-2|2개|USER_CUSTOM|SELF_CARE|create.*custom|add.*custom|findByUserAndTrackAndType|isDayCompleted|completedMissionIds" src/main/javaRepository: 8th-COKERTHON/server-team4 Length of output: 7715 Non- 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @Transactional | ||
|
|
@@ -129,14 +183,48 @@ public MissionListResponse getCustomMissions(User user) { | |
| return new MissionListResponse(result); | ||
| } | ||
|
|
||
| @Transactional | ||
| public MissionCompleteResponse proceedToNextTrack(User user) { | ||
| UserTrack userTrack = userTrackRepository.findByUserAndIsCurrentTrue(user) | ||
| .orElseThrow(() -> new AppException(MissionErrorCode.USER_TRACK_NOT_FOUND)); | ||
|
|
||
| Track track = userTrack.getTrack(); | ||
| LocalDate today = LocalDate.now(); | ||
|
|
||
| List<LocalDate> allDates = missionLogRepository.findCompletedDatesByUserAndTrack(user, track); | ||
| int completedDays = (int) allDates.stream() | ||
| .filter(date -> isDayCompleted(user, track, date, userTrack.getJoinedAt())) | ||
| .count(); | ||
|
|
||
| if (completedDays < track.getRequiredDays()) { | ||
| throw new AppException(MissionErrorCode.CANNOT_PROCEED); | ||
| } | ||
|
|
||
| userTrack.complete(today); | ||
|
|
||
| TrackType nextType = track.getTrackType().next(); | ||
| if (nextType == null) { | ||
| return new MissionCompleteResponse(true, null); | ||
| } | ||
|
|
||
| Track nextTrack = trackRepository.findByTrackType(nextType) | ||
| .orElseThrow(() -> new AppException(TrackErrorCode.TRACK_NOT_FOUND)); | ||
| userTrackRepository.save(new UserTrack(user, nextTrack, today)); | ||
|
|
||
| return new MissionCompleteResponse(true, nextType.getDisplayName()); | ||
| } | ||
|
|
||
| public MissionProgressResponse getMissionProgress(User user) { | ||
| UserTrack userTrack = userTrackRepository.findByUserAndIsCurrentTrue(user) | ||
| .orElseThrow(() -> new AppException(MissionErrorCode.USER_TRACK_NOT_FOUND)); | ||
|
|
||
| Track track = userTrack.getTrack(); | ||
| int requiredDays = track.getRequiredDays(); | ||
|
|
||
| int completedDays = missionLogRepository.findCompletedDatesByUserAndTrack(user, track).size(); | ||
| List<LocalDate> allDates = missionLogRepository.findCompletedDatesByUserAndTrack(user, track); | ||
| int completedDays = (int) allDates.stream() | ||
| .filter(date -> isDayCompleted(user, track, date, userTrack.getJoinedAt())) | ||
| .count(); | ||
|
|
||
| return new MissionProgressResponse(requiredDays, completedDays, completedDays >= requiredDays); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
The doc instructs clients to call
POST /api/missions/proceedwhentrackCompletedistrue, but the service already advances the track insidecompleteMission. This is the downstream symptom of the service-layer issue flagged inMissionService.completeMission(Line 107-119); resolving it there keeps this contract consistent.🤖 Prompt for AI Agents