From ff7b348c11f05cf0b75561cef6d3e24109ed7e68 Mon Sep 17 00:00:00 2001 From: djeu1116 Date: Tue, 10 Feb 2026 20:47:09 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/meetkey/server/ServerApplication.java | 2 + .../server/domain/badge/entity/Badge.java | 7 +- .../server/domain/badge/enums/ReasonType.java | 5 +- .../badge/respository/BadgeRepository.java | 12 ++ .../domain/badge/service/BadgeService.java | 55 +++--- .../server/domain/member/entity/Member.java | 2 + .../mission/controller/MissionController.java | 70 +++++++ .../controller/MissionTestController.java | 28 +++ .../mission/converter/MissionConverter.java | 37 ++++ .../domain/mission/dto/MissionResDTO.java | 28 +++ .../server/domain/mission/entity/Mission.java | 27 +++ .../entity/mapping/ChatRoomMission.java | 33 ++++ .../mission/entity/mapping/MissionLog.java | 45 +++++ .../domain/mission/enums/MissionStatus.java | 5 + .../domain/mission/enums/MissionType.java | 9 + .../mission/exception/MissionErrorStatus.java | 21 +++ .../mission/exception/MissionException.java | 10 + .../ChatRoomMissionRepository.java | 15 ++ .../respository/MissionLogRepository.java | 12 ++ .../MissionLogRepositoryCustom.java | 10 + .../respository/MissionLogRepositoryImpl.java | 33 ++++ .../respository/MissionRepository.java | 8 + .../respository/MissionRepositoryCustom.java | 9 + .../respository/MissionRepositoryImpl.java | 31 ++++ .../mission/service/MissionScheduler.java | 26 +++ .../mission/service/MissionService.java | 174 ++++++++++++++++++ 26 files changed, 687 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/meetkey/server/domain/badge/respository/BadgeRepository.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/controller/MissionController.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/controller/MissionTestController.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/converter/MissionConverter.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/dto/MissionResDTO.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/entity/Mission.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/entity/mapping/ChatRoomMission.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/entity/mapping/MissionLog.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/enums/MissionStatus.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/enums/MissionType.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/exception/MissionErrorStatus.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/exception/MissionException.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/respository/ChatRoomMissionRepository.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepository.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepositoryCustom.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepositoryImpl.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/respository/MissionRepository.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/respository/MissionRepositoryCustom.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/respository/MissionRepositoryImpl.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/service/MissionScheduler.java create mode 100644 src/main/java/com/meetkey/server/domain/mission/service/MissionService.java diff --git a/src/main/java/com/meetkey/server/ServerApplication.java b/src/main/java/com/meetkey/server/ServerApplication.java index 7b77fbc..62c4b66 100644 --- a/src/main/java/com/meetkey/server/ServerApplication.java +++ b/src/main/java/com/meetkey/server/ServerApplication.java @@ -4,9 +4,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @EnableFeignClients @EnableJpaAuditing +@EnableScheduling @SpringBootApplication public class ServerApplication { public static void main(String[] args) { diff --git a/src/main/java/com/meetkey/server/domain/badge/entity/Badge.java b/src/main/java/com/meetkey/server/domain/badge/entity/Badge.java index 52fa55a..0e08395 100644 --- a/src/main/java/com/meetkey/server/domain/badge/entity/Badge.java +++ b/src/main/java/com/meetkey/server/domain/badge/entity/Badge.java @@ -18,7 +18,7 @@ public class Badge extends BaseEntity { private Long id; @JoinColumn(name = "member_id", nullable = false) - @OneToOne + @OneToOne(fetch = FetchType.LAZY) private Member member; @Builder.Default @@ -28,4 +28,9 @@ public class Badge extends BaseEntity { @Enumerated(EnumType.STRING) private BadgeLevel level; + public void addScore(int score) { + this.total_score += score; + } + + } diff --git a/src/main/java/com/meetkey/server/domain/badge/enums/ReasonType.java b/src/main/java/com/meetkey/server/domain/badge/enums/ReasonType.java index 5ab626d..0da7cad 100644 --- a/src/main/java/com/meetkey/server/domain/badge/enums/ReasonType.java +++ b/src/main/java/com/meetkey/server/domain/badge/enums/ReasonType.java @@ -9,7 +9,10 @@ public enum ReasonType { AUTH("본인 인증", 20), PROFILE("상세 프로필 작성 완료", 15), ACTIVE("활동적인 멤버", 25), - POSITIVE("긍정적인 평가", 15); + POSITIVE("긍정적인 평가", 15), + MISSION_SUCCESS("미션 완료", 3), + MISSION_FAILURE("미션 실패", -5); + private final String description; private final int defaultScore; diff --git a/src/main/java/com/meetkey/server/domain/badge/respository/BadgeRepository.java b/src/main/java/com/meetkey/server/domain/badge/respository/BadgeRepository.java new file mode 100644 index 0000000..5fa542b --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/badge/respository/BadgeRepository.java @@ -0,0 +1,12 @@ +package com.meetkey.server.domain.badge.respository; + +import com.meetkey.server.domain.badge.entity.Badge; +import com.meetkey.server.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BadgeRepository extends JpaRepository { + + Optional findByMember(Member member); +} diff --git a/src/main/java/com/meetkey/server/domain/badge/service/BadgeService.java b/src/main/java/com/meetkey/server/domain/badge/service/BadgeService.java index b85169c..d2d3b0c 100644 --- a/src/main/java/com/meetkey/server/domain/badge/service/BadgeService.java +++ b/src/main/java/com/meetkey/server/domain/badge/service/BadgeService.java @@ -1,8 +1,10 @@ package com.meetkey.server.domain.badge.service; import com.meetkey.server.domain.badge.converter.BadgeConverter; +import com.meetkey.server.domain.badge.entity.Badge; import com.meetkey.server.domain.badge.entity.PointHistory; import com.meetkey.server.domain.badge.enums.ReasonType; +import com.meetkey.server.domain.badge.respository.BadgeRepository; import com.meetkey.server.domain.badge.respository.PointHistoryRepository; import com.meetkey.server.domain.member.entity.Member; import com.meetkey.server.domain.member.exception.MemberErrorStatus; @@ -23,6 +25,7 @@ public class BadgeService { private final MemberRepository memberRepository; private final PointHistoryRepository pointHistoryRepository; + private final BadgeRepository badgeRepository; private final BadgeConverter badgeConverter; // 뱃지 정보 조회 (내역 포함) @@ -48,37 +51,34 @@ public BadgeResponse getBadgeSummary(Long memberId) { } // 점수 부여 - public void rewardPoints(Long memberId, ReasonType reasonType) { - Member member = getMember(memberId); + public void rewardPoints(Member member, ReasonType reasonType) { + int amount = reasonType.getDefaultScore(); - if (pointHistoryRepository.existsByMemberAndReasonType(member, reasonType)) { - return; - } + Badge badge = badgeRepository.findByMember(member).orElseThrow( + () -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND)); - int currentTotalScore = pointHistoryRepository.calculateTotalScore(member); + int currentScore = badge.getTotal_score(); - // 100점 이상이라면 지급 X - if (currentTotalScore >= 100) { - return; - } + int potetialScore = currentScore + amount; - int pointsToAdd = reasonType.getDefaultScore(); - int potentialScore = currentTotalScore + pointsToAdd; + int potentialScore = currentScore + amount; - // 더해서 100점 초과라면 일부만 지급 if (potentialScore > 100) { - pointsToAdd = 100 - currentTotalScore; + amount = 100 - currentScore; // 100점까지만 채움 + } else if (potentialScore < 0) { + amount = -currentScore; } - if (pointsToAdd > 0) { - PointHistory pointHistory = PointHistory.builder() - .member(member) - .reasonType(reasonType) - .changeAmount(reasonType.getDefaultScore()) - .build(); - pointHistoryRepository.save(pointHistory); - } + if (amount == 0) return; // 변동 없으면 종료 + badge.addScore(amount); // Badge 엔티티 업데이트 + + PointHistory pointHistory = PointHistory.builder() + .member(member) + .reasonType(reasonType) + .changeAmount(amount) + .build(); + pointHistoryRepository.save(pointHistory); } // 본인 인증 @@ -86,7 +86,7 @@ public void checkAuthentication(Long memberId) { Member member = getMember(memberId); if (Boolean.TRUE.equals(member.isVerified())) { - rewardPoints(member.getId(), ReasonType.AUTH); + rewardPoints(member, ReasonType.AUTH); } } @@ -101,7 +101,7 @@ public void checkProfileCompletion(Long memberId) { member.getTargetLanguageLevel() != null; if (isComplete) { - rewardPoints(member.getId(), ReasonType.PROFILE); + rewardPoints(member, ReasonType.PROFILE); } } @@ -110,10 +110,15 @@ public void checkPositiveEvaluation(Long memberId) { Member member = getMember(memberId); if (member.getRecommendCount() >= 10) { - rewardPoints(member.getId(), ReasonType.POSITIVE); + rewardPoints(member, ReasonType.POSITIVE); } } + // 미션용 지급 + public void rewardRepeatable(Long memberId, ReasonType reasonType) { + Member member = getMember(memberId); + rewardPoints(member, reasonType); + } // 사용자 찾기 메소드 private Member getMember(Long memberId) { diff --git a/src/main/java/com/meetkey/server/domain/member/entity/Member.java b/src/main/java/com/meetkey/server/domain/member/entity/Member.java index 996b7f4..d37f0be 100644 --- a/src/main/java/com/meetkey/server/domain/member/entity/Member.java +++ b/src/main/java/com/meetkey/server/domain/member/entity/Member.java @@ -78,6 +78,7 @@ public class Member extends BaseEntity { @Column(nullable = false) @Builder.Default private Integer notRecommendCount = 0; + /* * 관심사 */ @@ -133,4 +134,5 @@ public void updateCertificated() { this.isVerified = true; } + } diff --git a/src/main/java/com/meetkey/server/domain/mission/controller/MissionController.java b/src/main/java/com/meetkey/server/domain/mission/controller/MissionController.java new file mode 100644 index 0000000..8652219 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/controller/MissionController.java @@ -0,0 +1,70 @@ +package com.meetkey.server.domain.mission.controller; + +import com.meetkey.server.domain.mission.dto.MissionResDTO; +import com.meetkey.server.domain.mission.service.MissionService; +import com.meetkey.server.global.apiPayload.response.BasicResponse; +import com.meetkey.server.global.apiPayload.status.CommonSuccessStatus; +import com.meetkey.server.global.security.CustomUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +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 lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import static com.meetkey.server.domain.mission.dto.MissionResDTO.*; + +@Tag(name = "Mission", description = "채팅방 미션 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/chat-rooms/{chatRoomId}/missions") +public class MissionController { + + private final MissionService missionService; + + @Operation(summary = "오늘의 미션 조회", description = "해당 채팅방의 현재 진행 중인 미션을 조회합니다. (없으면 미션 자동 생성)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(schema = @Schema(implementation = Info.class))), + @ApiResponse(responseCode = "400", description = "CHAT2041 : 존재하지 않는 채팅방입니다. , MEMBER4041 : 존재하지 않는 사용자입니다." + + "MISSION4041 : 존재하지 않는 미션입니다.") + }) + @GetMapping("/today") + public BasicResponse getTodayMission( + @Parameter(description = "채팅방 ID", required = true) + @PathVariable Long chatRoomId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + Info response = missionService.getTodayMission(chatRoomId, userDetails.getMemberId()); + return BasicResponse.success(CommonSuccessStatus._OK, response); + + } + + @Operation(summary = "미션 완료 인증", description = "미션을 수행하고 완료 버튼을 눌렀을 때 호출합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(schema = @Schema(implementation = Completion.class))), + @ApiResponse(responseCode = "400", description = "MEMBER4041 : 존재하지 않는 사용자입니다., MISSION4041 : 존재하지않는 미션입니다., MISSION4001 : 이미 완료된 미션입니다." + + "MISSION4042 : 참여정보를 찾을 수 없습니다. , ") + }) + @PostMapping("/{missionId}/complete") + public BasicResponse completeMission( + @Parameter(description = "채팅방 ID", required = true) + @PathVariable Long chatRoomId, + + @Parameter(description = "오늘의 미션 조회 시 반환된 'missionId' 값 (ChatRoomMission ID). ※ 주의: 미션 내용 고유 번호(Mission ID)가 아닙니다.", required = true, example = "105") + @PathVariable Long missionId, // ChatRoomMission의 ID (Mission ID 아님 주의) + + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + Completion response = missionService.completeMission(chatRoomId, missionId, userDetails.getMemberId()); + return BasicResponse.success(CommonSuccessStatus._OK, response); + } + + + + +} diff --git a/src/main/java/com/meetkey/server/domain/mission/controller/MissionTestController.java b/src/main/java/com/meetkey/server/domain/mission/controller/MissionTestController.java new file mode 100644 index 0000000..0d65925 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/controller/MissionTestController.java @@ -0,0 +1,28 @@ +package com.meetkey.server.domain.mission.controller; + +import com.meetkey.server.domain.mission.service.MissionService; +import com.meetkey.server.global.apiPayload.response.BasicResponse; +import com.meetkey.server.global.apiPayload.status.CommonSuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Test - Mission", description = "[테스트용] 미션 스케줄러 강제 실행 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/test/missions") +public class MissionTestController { + + private final MissionService missionService; + + @Operation(summary = "만료된 미션 강제 실패 처리", + description = "스케줄러를 기다리지 않고, 현재 시간 기준으로 만료된 미션을 즉시 실패 처리(-5점) 합니다.") + @PostMapping("/expire-now") + public BasicResponse triggerExpire() { + int count = missionService.processExpiredMission(); + return BasicResponse.success(CommonSuccessStatus._OK, "수동 실행 완료. 총 " + count + "건의 미션이 실패 처리되었습니다."); + } +} diff --git a/src/main/java/com/meetkey/server/domain/mission/converter/MissionConverter.java b/src/main/java/com/meetkey/server/domain/mission/converter/MissionConverter.java new file mode 100644 index 0000000..40d9e1b --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/converter/MissionConverter.java @@ -0,0 +1,37 @@ +package com.meetkey.server.domain.mission.converter; + +import com.meetkey.server.domain.badge.entity.Badge; +import com.meetkey.server.domain.badge.enums.ReasonType; +import com.meetkey.server.domain.member.entity.Member; +import com.meetkey.server.domain.mission.entity.mapping.ChatRoomMission; +import com.meetkey.server.domain.mission.entity.mapping.MissionLog; +import org.springframework.stereotype.Component; + +import static com.meetkey.server.domain.mission.dto.MissionResDTO.*; + +@Component +public class MissionConverter { + + // 오늘의 미션 조회 응답 변환 + public Info toMissionInfo(ChatRoomMission chatRoomMission, MissionLog missionLog, long remainingSeconds) { + return Info.builder() + .missionId(chatRoomMission.getId()) + .content(chatRoomMission.getMission().getContent()) + .type(chatRoomMission.getMission().getMissionType()) + .remainingSeconds(remainingSeconds) + .myStatus(missionLog.getMissionStatus()) + .build(); + } + + // 미션 완료 응답 변환 + public Completion toMissionCompletion(Badge badge, ReasonType reasonType) { + return Completion.builder() + .status("SUCCESS") + .gainedPoints(reasonType.getDefaultScore()) + .currentScore(badge.getTotal_score()) + .badgeName(badge.getLevel().name()) + .build(); + } + + +} diff --git a/src/main/java/com/meetkey/server/domain/mission/dto/MissionResDTO.java b/src/main/java/com/meetkey/server/domain/mission/dto/MissionResDTO.java new file mode 100644 index 0000000..ff3fe71 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/dto/MissionResDTO.java @@ -0,0 +1,28 @@ +package com.meetkey.server.domain.mission.dto; + +import com.meetkey.server.domain.badge.enums.BadgeLevel; +import com.meetkey.server.domain.mission.enums.MissionStatus; +import com.meetkey.server.domain.mission.enums.MissionType; +import lombok.Builder; + +public class MissionResDTO { + + // 오늘의 미션 조회 응답 + @Builder + public record Info( + Long missionId, + String content, + MissionType type, + long remainingSeconds, // 남은 시간 (초) + MissionStatus myStatus // PENDING, SUCCESS, FAILED + ) {} + + // 미션 완료 응답 + @Builder + public record Completion( + String status, // "SUCCESS" + int gainedPoints, // 획득 점수 (+3) + int currentScore, // 현재 총 점수 + String badgeName + ) {} +} diff --git a/src/main/java/com/meetkey/server/domain/mission/entity/Mission.java b/src/main/java/com/meetkey/server/domain/mission/entity/Mission.java new file mode 100644 index 0000000..7f52c27 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/entity/Mission.java @@ -0,0 +1,27 @@ +package com.meetkey.server.domain.mission.entity; + +import com.meetkey.server.domain.mission.enums.MissionType; +import com.meetkey.server.global.common.BaseEntity; +import io.swagger.v3.oas.annotations.media.Content; +import jakarta.persistence.*; +import lombok.*; + +@Builder +@Table(name = "mission") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Mission extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String content; + + @Enumerated(EnumType.STRING) + private MissionType missionType; + +} diff --git a/src/main/java/com/meetkey/server/domain/mission/entity/mapping/ChatRoomMission.java b/src/main/java/com/meetkey/server/domain/mission/entity/mapping/ChatRoomMission.java new file mode 100644 index 0000000..518a002 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/entity/mapping/ChatRoomMission.java @@ -0,0 +1,33 @@ +package com.meetkey.server.domain.mission.entity.mapping; + +import com.meetkey.server.domain.chat.entity.ChatRoom; +import com.meetkey.server.domain.mission.entity.Mission; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Builder +@Table(name = "chat_room_mission") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ChatRoomMission { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private LocalDateTime assignedAt; + private LocalDateTime expiresAt; + + @JoinColumn(name = "chat_room_id") + @ManyToOne(fetch = FetchType.LAZY) + private ChatRoom chatRoom; + + @JoinColumn(name = "mission_id") + @ManyToOne(fetch = FetchType.LAZY) + private Mission mission; + +} diff --git a/src/main/java/com/meetkey/server/domain/mission/entity/mapping/MissionLog.java b/src/main/java/com/meetkey/server/domain/mission/entity/mapping/MissionLog.java new file mode 100644 index 0000000..941271d --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/entity/mapping/MissionLog.java @@ -0,0 +1,45 @@ +package com.meetkey.server.domain.mission.entity.mapping; + + +import com.meetkey.server.domain.member.entity.Member; +import com.meetkey.server.domain.mission.enums.MissionStatus; +import com.meetkey.server.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Builder +@Table(name = "mission_log") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class MissionLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private MissionStatus missionStatus; + + private LocalDateTime completedAt; + + @JoinColumn(name = "member_id") + @ManyToOne(fetch = FetchType.LAZY) + Member member; + + @JoinColumn(name = "chat_room_mission_id") + @ManyToOne(fetch = FetchType.LAZY) + ChatRoomMission chatRoomMission; + + public void complete() { + this.missionStatus = MissionStatus.SUCCESS; + this.completedAt = LocalDateTime.now(); + } + + public void fail() { + this.missionStatus = MissionStatus.FAILED; + } +} diff --git a/src/main/java/com/meetkey/server/domain/mission/enums/MissionStatus.java b/src/main/java/com/meetkey/server/domain/mission/enums/MissionStatus.java new file mode 100644 index 0000000..ef51b78 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/enums/MissionStatus.java @@ -0,0 +1,5 @@ +package com.meetkey.server.domain.mission.enums; + +public enum MissionStatus { + PENDING, SUCCESS, FAILED +} diff --git a/src/main/java/com/meetkey/server/domain/mission/enums/MissionType.java b/src/main/java/com/meetkey/server/domain/mission/enums/MissionType.java new file mode 100644 index 0000000..6b8aa7c --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/enums/MissionType.java @@ -0,0 +1,9 @@ +package com.meetkey.server.domain.mission.enums; + +public enum MissionType { + CONVERSATION, + PHOTO, + VALUE, + DAILY, + PARTICIPATION +} diff --git a/src/main/java/com/meetkey/server/domain/mission/exception/MissionErrorStatus.java b/src/main/java/com/meetkey/server/domain/mission/exception/MissionErrorStatus.java new file mode 100644 index 0000000..98c8261 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/exception/MissionErrorStatus.java @@ -0,0 +1,21 @@ +package com.meetkey.server.domain.mission.exception; + +import com.meetkey.server.global.apiPayload.code.BaseCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum MissionErrorStatus implements BaseCode { + + ALREADY_CLEAR(HttpStatus.BAD_REQUEST, "MISSION4001", "이미 완료된 미션입니다."), + MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION4041", "미션을 찾을 수 없습니다."), + PARTICIPATION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION4042", "참여 정보를 찾을 수 없습니다.") + ; + + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/meetkey/server/domain/mission/exception/MissionException.java b/src/main/java/com/meetkey/server/domain/mission/exception/MissionException.java new file mode 100644 index 0000000..b0095bd --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/exception/MissionException.java @@ -0,0 +1,10 @@ +package com.meetkey.server.domain.mission.exception; + +import com.meetkey.server.global.apiPayload.code.BaseCode; +import com.meetkey.server.global.apiPayload.exception.GeneralException; + +public class MissionException extends GeneralException { + public MissionException(BaseCode code) { + super(code); + } +} diff --git a/src/main/java/com/meetkey/server/domain/mission/respository/ChatRoomMissionRepository.java b/src/main/java/com/meetkey/server/domain/mission/respository/ChatRoomMissionRepository.java new file mode 100644 index 0000000..d063c3a --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/respository/ChatRoomMissionRepository.java @@ -0,0 +1,15 @@ +package com.meetkey.server.domain.mission.respository; + +import com.meetkey.server.domain.chat.entity.ChatRoom; +import com.meetkey.server.domain.mission.entity.Mission; +import com.meetkey.server.domain.mission.entity.mapping.ChatRoomMission; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ChatRoomMissionRepository extends JpaRepository { + + Optional findFirstByChatRoomOrderByAssignedAtDesc(ChatRoom chatRoom); + + Long mission(Mission mission); +} diff --git a/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepository.java b/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepository.java new file mode 100644 index 0000000..c4f91bc --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepository.java @@ -0,0 +1,12 @@ +package com.meetkey.server.domain.mission.respository; + +import com.meetkey.server.domain.member.entity.Member; +import com.meetkey.server.domain.mission.entity.mapping.ChatRoomMission; +import com.meetkey.server.domain.mission.entity.mapping.MissionLog; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MissionLogRepository extends JpaRepository, MissionLogRepositoryCustom { + Optional findByChatRoomMissionAndMember(ChatRoomMission chatRoomMission, Member member); +} diff --git a/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepositoryCustom.java b/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepositoryCustom.java new file mode 100644 index 0000000..fd42206 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.meetkey.server.domain.mission.respository; + +import com.meetkey.server.domain.mission.entity.mapping.MissionLog; + +import java.time.LocalDateTime; +import java.util.List; + +public interface MissionLogRepositoryCustom { + List findAllExpiredPendingLogs(LocalDateTime now); +} diff --git a/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepositoryImpl.java b/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepositoryImpl.java new file mode 100644 index 0000000..f6465ea --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/respository/MissionLogRepositoryImpl.java @@ -0,0 +1,33 @@ +package com.meetkey.server.domain.mission.respository; + +import com.meetkey.server.domain.mission.entity.mapping.MissionLog; +import com.meetkey.server.domain.mission.entity.mapping.QMissionLog; +import com.meetkey.server.domain.mission.enums.MissionStatus; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.meetkey.server.domain.mission.entity.mapping.QMissionLog.missionLog; +import static com.meetkey.server.domain.mission.entity.mapping.QChatRoomMission.chatRoomMission; + +@RequiredArgsConstructor +public class MissionLogRepositoryImpl implements MissionLogRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public List findAllExpiredPendingLogs(LocalDateTime now) { + return queryFactory + .selectFrom(missionLog) + .join(missionLog.chatRoomMission, chatRoomMission).fetchJoin() // missionLog와 chatRoomMission을 조인하여 fetch join 수행 + .where( + missionLog.missionStatus.eq(MissionStatus.PENDING), // 아직 안한 상태 + chatRoomMission.expiresAt.before(now) // 만료된 상태 + + ) + .fetch(); + + } +} diff --git a/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepository.java b/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepository.java new file mode 100644 index 0000000..cc2e0dd --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepository.java @@ -0,0 +1,8 @@ +package com.meetkey.server.domain.mission.respository; + +import com.meetkey.server.domain.mission.entity.Mission; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MissionRepository extends JpaRepository, MissionRepositoryCustom { + +} diff --git a/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepositoryCustom.java b/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepositoryCustom.java new file mode 100644 index 0000000..610bab7 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepositoryCustom.java @@ -0,0 +1,9 @@ +package com.meetkey.server.domain.mission.respository; + +import com.meetkey.server.domain.mission.entity.Mission; + +import java.util.Optional; + +public interface MissionRepositoryCustom { + Optional findRandomMission(); +} diff --git a/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepositoryImpl.java b/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepositoryImpl.java new file mode 100644 index 0000000..0f18cdc --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/respository/MissionRepositoryImpl.java @@ -0,0 +1,31 @@ +package com.meetkey.server.domain.mission.respository; + +import com.meetkey.server.domain.mission.entity.Mission; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberTemplate; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.util.Optional; + +import static com.meetkey.server.domain.mission.entity.QMission.mission; + + +@RequiredArgsConstructor +public class MissionRepositoryImpl implements MissionRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findRandomMission() { + NumberTemplate randomOrder = Expressions.numberTemplate(Double.class, "function('rand')"); + + Mission randomMission = queryFactory + .selectFrom(mission) + .orderBy(randomOrder.asc()) // 랜덤 정렬 + .limit(1) // 하나만 + .fetchFirst(); + + return Optional.ofNullable(randomMission); + } +} diff --git a/src/main/java/com/meetkey/server/domain/mission/service/MissionScheduler.java b/src/main/java/com/meetkey/server/domain/mission/service/MissionScheduler.java new file mode 100644 index 0000000..d9515b2 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/service/MissionScheduler.java @@ -0,0 +1,26 @@ +package com.meetkey.server.domain.mission.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class MissionScheduler { + + private final MissionService missionService; + + // 1시간 마다 실행 -> (매시 정각 0분 0초) + @Scheduled(cron = "0 0 * * * *") + public void checkExpiredMissions() { + log.info("[Scheduler] 미션 만료 체크 스케줄러 실행"); + try { + int processedCount = missionService.processExpiredMission(); + log.info("[Scheduler] 처리 완료: {}건", processedCount); + } catch (Exception e) { + log.error("[Scheduler] 미션 만료 처리 중 에러 발생", e); + } + } +} diff --git a/src/main/java/com/meetkey/server/domain/mission/service/MissionService.java b/src/main/java/com/meetkey/server/domain/mission/service/MissionService.java new file mode 100644 index 0000000..daaa926 --- /dev/null +++ b/src/main/java/com/meetkey/server/domain/mission/service/MissionService.java @@ -0,0 +1,174 @@ +package com.meetkey.server.domain.mission.service; + +import com.meetkey.server.domain.badge.entity.Badge; +import com.meetkey.server.domain.badge.enums.ReasonType; +import com.meetkey.server.domain.badge.respository.BadgeRepository; +import com.meetkey.server.domain.badge.respository.PointHistoryRepository; +import com.meetkey.server.domain.badge.service.BadgeService; +import com.meetkey.server.domain.chat.entity.ChatRoom; +import com.meetkey.server.domain.chat.exception.ChatErrorStatus; +import com.meetkey.server.domain.chat.exception.ChatException; +import com.meetkey.server.domain.chat.repository.ChatRoomRepository; +import com.meetkey.server.domain.member.entity.Member; +import com.meetkey.server.domain.member.exception.MemberErrorStatus; +import com.meetkey.server.domain.member.exception.MemberException; +import com.meetkey.server.domain.member.repository.MemberRepository; +import com.meetkey.server.domain.mission.converter.MissionConverter; +import com.meetkey.server.domain.mission.entity.Mission; +import com.meetkey.server.domain.mission.entity.mapping.ChatRoomMission; +import com.meetkey.server.domain.mission.entity.mapping.MissionLog; +import com.meetkey.server.domain.mission.enums.MissionStatus; +import com.meetkey.server.domain.mission.exception.MissionErrorStatus; +import com.meetkey.server.domain.mission.exception.MissionException; +import com.meetkey.server.domain.mission.respository.ChatRoomMissionRepository; +import com.meetkey.server.domain.mission.respository.MissionLogRepository; +import com.meetkey.server.domain.mission.respository.MissionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static com.meetkey.server.domain.mission.dto.MissionResDTO.*; + +@Service +@RequiredArgsConstructor +@Transactional +public class MissionService { + + private final MissionRepository missionRepository; + private final ChatRoomMissionRepository chatRoomMissionRepository; + private final MissionLogRepository missionLogRepository; + private final MemberRepository memberRepository; + private final PointHistoryRepository pointHistoryRepository; + private final ChatRoomRepository chatRoomRepository; + + private final MissionConverter missionConverter; + + private final BadgeService badgeService; + private final BadgeRepository badgeRepository; + + public Info getTodayMission(Long chatRoomId, Long memberId) { + ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId).orElseThrow( + () -> new ChatException(ChatErrorStatus.CHAT_ROOM_NOT_FOUND)); + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND)); + + // 미션 가져오기 (없으면 생성 시도) + ChatRoomMission currentMission = getOrCreateDailyMission(chatRoom); + + // 내 참여 로그 가져오기 (없으면 생성) + MissionLog myLog = getOrCreateMissionLog(currentMission, member); + + // 남은 시간 계산 및 반환 + long remainingSeconds = calculateRemainingSeconds(currentMission.getExpiresAt()); + + return missionConverter.toMissionInfo(currentMission, myLog, remainingSeconds); + } + + + + // 미션 완료 처리 + public Completion completeMission(Long chatRoomId, Long missionId, Long memberId) { + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND)); + + ChatRoomMission chatRoomMission = chatRoomMissionRepository.findById(missionId).orElseThrow( + () -> new MissionException(MissionErrorStatus.MISSION_NOT_FOUND)); + + if (chatRoomMission.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new MissionException(MissionErrorStatus.ALREADY_CLEAR); + } + + MissionLog log = missionLogRepository.findByChatRoomMissionAndMember(chatRoomMission, member) + .orElseThrow(() -> new MissionException(MissionErrorStatus.PARTICIPATION_NOT_FOUND)); + + if (log.getMissionStatus() == MissionStatus.SUCCESS) { + throw new MissionException(MissionErrorStatus.ALREADY_CLEAR); + } + + // 상태 변경 및 점수 지급 + log.complete(); + + badgeService.rewardPoints(member, ReasonType.MISSION_SUCCESS); + Badge badge = badgeRepository.findByMember(member).orElseThrow(); + return missionConverter.toMissionCompletion(badge, ReasonType.MISSION_SUCCESS); + + } + + public int processExpiredMission() { + LocalDateTime now = LocalDateTime.now(); + List expiredLogs = missionLogRepository.findAllExpiredPendingLogs(now); + + int count = 0; + for (MissionLog log : expiredLogs) { + log.fail(); + badgeService.rewardPoints(log.getMember(), ReasonType.MISSION_FAILURE); + + count ++; + } + + return count; + } + + + private ChatRoomMission getOrCreateDailyMission(ChatRoom chatRoom) { + Optional missionOpt = chatRoomMissionRepository + .findFirstByChatRoomOrderByAssignedAtDesc(chatRoom); + + // 미션이 존재하고, 만료되지 않았으면 리턴 + if (missionOpt.isPresent() && missionOpt.get().getExpiresAt().isAfter(LocalDateTime.now())) { + return missionOpt.get(); + } + + // 없거나 만료됐으면 -> 새로 생성 + try { + return createNewMission(chatRoom); + } catch (DataIntegrityViolationException e) { + return chatRoomMissionRepository.findFirstByChatRoomOrderByAssignedAtDesc(chatRoom) + .orElseThrow(() -> new MissionException(MissionErrorStatus.MISSION_NOT_FOUND)); + } + } + + private MissionLog getOrCreateMissionLog(ChatRoomMission mission, Member member) { + return missionLogRepository.findByChatRoomMissionAndMember(mission, member) + .orElseGet(() -> { + // 로그 생성 로직 + return createPendingLog(mission, member); + }); + } + + private long calculateRemainingSeconds(LocalDateTime expiresAt) { + long seconds = Duration.between(LocalDateTime.now(), expiresAt).getSeconds(); + return seconds < 0 ? 0 : seconds; + } + + // 새로운 미션 할당 로직 + private ChatRoomMission createNewMission(ChatRoom chatRoom) { + Mission randomMission = missionRepository.findRandomMission() + .orElseThrow(() -> new MissionException(MissionErrorStatus.MISSION_NOT_FOUND)); + + ChatRoomMission newAssignment = ChatRoomMission.builder() + .chatRoom(chatRoom) + .mission(randomMission) + .assignedAt(LocalDateTime.now()) + .expiresAt(LocalDateTime.now().plusHours(24)) + .build(); + + return chatRoomMissionRepository.save(newAssignment); + } + + // 멤버별 로그 생성 로직 + private MissionLog createPendingLog(ChatRoomMission mission, Member member) { + MissionLog log = MissionLog.builder() + .chatRoomMission(mission) + .member(member) + .missionStatus(MissionStatus.PENDING) + .build(); + return missionLogRepository.save(log); + } +} From 33f4ea9afa3c90f2881640a8dd87f7ad37173649 Mon Sep 17 00:00:00 2001 From: djeu1116 Date: Tue, 10 Feb 2026 21:10:31 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EA=B8=B0=EB=B3=B8=20=EB=B1=83=EC=A7=80?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/auth/service/AuthService.java | 11 +++++++++++ .../server/domain/badge/entity/PointHistory.java | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/meetkey/server/domain/auth/service/AuthService.java b/src/main/java/com/meetkey/server/domain/auth/service/AuthService.java index 99519ea..f624fa5 100644 --- a/src/main/java/com/meetkey/server/domain/auth/service/AuthService.java +++ b/src/main/java/com/meetkey/server/domain/auth/service/AuthService.java @@ -2,6 +2,9 @@ import com.meetkey.server.domain.auth.entity.RefreshToken; import com.meetkey.server.domain.auth.repository.RefreshTokenRepository; +import com.meetkey.server.domain.badge.entity.Badge; +import com.meetkey.server.domain.badge.enums.BadgeLevel; +import com.meetkey.server.domain.badge.service.BadgeService; import com.meetkey.server.domain.member.dto.MemberReqDTO; import com.meetkey.server.domain.member.entity.Member; import com.meetkey.server.domain.member.entity.SocialLogin; @@ -41,6 +44,7 @@ public class AuthService { private final OauthOidcHelper oAuthOIDCHelper; private final JwtUtil jwtUtil; private final MemberService memberService; + private final BadgeService badgeService; private final SocialLoginRepository socialLoginRepository; private final RefreshTokenRepository refreshTokenRepository; @@ -74,6 +78,13 @@ else if (provider == Provider.APPLE) { // Member 생성 Member member = memberService.signup(provider, providerId, memberReqDTO); + // 회원 가입시 자동적으로 뱃지 생성 + Badge initalBadge = Badge.builder() + .member(member) + .total_score(0) + .level(BadgeLevel.NONE) + .build(); + // 밋키 서비스 토큰 발급 return getJwtResponseDTO(member); } diff --git a/src/main/java/com/meetkey/server/domain/badge/entity/PointHistory.java b/src/main/java/com/meetkey/server/domain/badge/entity/PointHistory.java index a16f4f8..045ec51 100644 --- a/src/main/java/com/meetkey/server/domain/badge/entity/PointHistory.java +++ b/src/main/java/com/meetkey/server/domain/badge/entity/PointHistory.java @@ -24,7 +24,7 @@ public class PointHistory extends BaseEntity { @Column(nullable = false) private int changeAmount; - @Column(nullable = false) + @Column(nullable = false, length = 30) @Enumerated(EnumType.STRING) private ReasonType reasonType; } From 33dc2c5fb0eccf6173fe9c9c69bab17461fac326 Mon Sep 17 00:00:00 2001 From: djeu1116 Date: Tue, 10 Feb 2026 21:57:36 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ChatMessageRepository.java | 11 ++++++++ .../mission/controller/MissionController.java | 12 +++++---- .../mission/exception/MissionErrorStatus.java | 1 + .../mission/service/MissionService.java | 26 ++++++++++++++++++- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/meetkey/server/domain/chat/repository/ChatMessageRepository.java b/src/main/java/com/meetkey/server/domain/chat/repository/ChatMessageRepository.java index bcd09f7..c68d158 100644 --- a/src/main/java/com/meetkey/server/domain/chat/repository/ChatMessageRepository.java +++ b/src/main/java/com/meetkey/server/domain/chat/repository/ChatMessageRepository.java @@ -2,10 +2,13 @@ import com.meetkey.server.domain.chat.entity.ChatMessage; import com.meetkey.server.domain.chat.entity.ChatRoom; +import com.meetkey.server.domain.chat.entity.enums.MessageType; +import com.meetkey.server.domain.member.entity.Member; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -24,4 +27,12 @@ Slice findByChatRoomAndIdLessThanOrderByIdAsc( long countByChatRoom(ChatRoom chatRoom); long countByChatRoomAndIdGreaterThan(ChatRoom chatRoom, Long messageId); + + // 해당 채팅방에서 특정 시간 이후에 특정 타입의 메시지를 보낸적 있는지 판별 + boolean existsByChatRoomAndMemberAndCreatedAtAfterAndMessageType( + ChatRoom chatRoom, + Member sender, + LocalDateTime timestamp, + MessageType messageType + ); } diff --git a/src/main/java/com/meetkey/server/domain/mission/controller/MissionController.java b/src/main/java/com/meetkey/server/domain/mission/controller/MissionController.java index 8652219..52bdc3d 100644 --- a/src/main/java/com/meetkey/server/domain/mission/controller/MissionController.java +++ b/src/main/java/com/meetkey/server/domain/mission/controller/MissionController.java @@ -1,6 +1,5 @@ package com.meetkey.server.domain.mission.controller; -import com.meetkey.server.domain.mission.dto.MissionResDTO; import com.meetkey.server.domain.mission.service.MissionService; import com.meetkey.server.global.apiPayload.response.BasicResponse; import com.meetkey.server.global.apiPayload.status.CommonSuccessStatus; @@ -13,7 +12,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -44,11 +42,15 @@ public BasicResponse getTodayMission( } - @Operation(summary = "미션 완료 인증", description = "미션을 수행하고 완료 버튼을 눌렀을 때 호출합니다.") + @Operation(summary = "미션 완료 인증", description = "사용자가 채팅을 보내면 호출 -> 내부 로직으로 미션 성공 여부를 판단" + + "미션 성공 로직 (내용은 신경쓰지 않음)" + + "1. 미션 타입이 PHOTO인 경우는 사진을 하나 이상 보내야한다. " + + "2. PHOTO가 아닌 다른 타입의 미션들은 텍스트 채팅을 한 번 이상 남겨야 한다." + + "3. 미션 받은 이후에 해당 내용을 수행해야함.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(schema = @Schema(implementation = Completion.class))), - @ApiResponse(responseCode = "400", description = "MEMBER4041 : 존재하지 않는 사용자입니다., MISSION4041 : 존재하지않는 미션입니다., MISSION4001 : 이미 완료된 미션입니다." + - "MISSION4042 : 참여정보를 찾을 수 없습니다. , ") + @ApiResponse(responseCode = "400", description = "MEMBER4041 : 존재하지 않는 사용자입니다., MISSION4041 : 존재하지않는 미션입니다., MISSION4001 : 이미 완료된 미션입니다., " + + "MISSION4002 : 아직 미션을 수행하지 않았습니다., MISSION4042 : 참여정보를 찾을 수 없습니다.") }) @PostMapping("/{missionId}/complete") public BasicResponse completeMission( diff --git a/src/main/java/com/meetkey/server/domain/mission/exception/MissionErrorStatus.java b/src/main/java/com/meetkey/server/domain/mission/exception/MissionErrorStatus.java index 98c8261..71a196c 100644 --- a/src/main/java/com/meetkey/server/domain/mission/exception/MissionErrorStatus.java +++ b/src/main/java/com/meetkey/server/domain/mission/exception/MissionErrorStatus.java @@ -10,6 +10,7 @@ public enum MissionErrorStatus implements BaseCode { ALREADY_CLEAR(HttpStatus.BAD_REQUEST, "MISSION4001", "이미 완료된 미션입니다."), + NOT_COMPLETED_YET(HttpStatus.BAD_REQUEST, "MISSION4002", "아직 미션을 수행하지 않았습니다."), MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION4041", "미션을 찾을 수 없습니다."), PARTICIPATION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION4042", "참여 정보를 찾을 수 없습니다.") ; diff --git a/src/main/java/com/meetkey/server/domain/mission/service/MissionService.java b/src/main/java/com/meetkey/server/domain/mission/service/MissionService.java index daaa926..6c1b7e9 100644 --- a/src/main/java/com/meetkey/server/domain/mission/service/MissionService.java +++ b/src/main/java/com/meetkey/server/domain/mission/service/MissionService.java @@ -6,8 +6,10 @@ import com.meetkey.server.domain.badge.respository.PointHistoryRepository; import com.meetkey.server.domain.badge.service.BadgeService; import com.meetkey.server.domain.chat.entity.ChatRoom; +import com.meetkey.server.domain.chat.entity.enums.MessageType; import com.meetkey.server.domain.chat.exception.ChatErrorStatus; import com.meetkey.server.domain.chat.exception.ChatException; +import com.meetkey.server.domain.chat.repository.ChatMessageRepository; import com.meetkey.server.domain.chat.repository.ChatRoomRepository; import com.meetkey.server.domain.member.entity.Member; import com.meetkey.server.domain.member.exception.MemberErrorStatus; @@ -18,6 +20,7 @@ import com.meetkey.server.domain.mission.entity.mapping.ChatRoomMission; import com.meetkey.server.domain.mission.entity.mapping.MissionLog; import com.meetkey.server.domain.mission.enums.MissionStatus; +import com.meetkey.server.domain.mission.enums.MissionType; import com.meetkey.server.domain.mission.exception.MissionErrorStatus; import com.meetkey.server.domain.mission.exception.MissionException; import com.meetkey.server.domain.mission.respository.ChatRoomMissionRepository; @@ -44,7 +47,7 @@ public class MissionService { private final ChatRoomMissionRepository chatRoomMissionRepository; private final MissionLogRepository missionLogRepository; private final MemberRepository memberRepository; - private final PointHistoryRepository pointHistoryRepository; + private final ChatMessageRepository chatMessageRepository; private final ChatRoomRepository chatRoomRepository; private final MissionConverter missionConverter; @@ -91,6 +94,27 @@ public Completion completeMission(Long chatRoomId, Long missionId, Long memberId throw new MissionException(MissionErrorStatus.ALREADY_CLEAR); } + MissionType missionType = chatRoomMission.getMission().getMissionType(); + MessageType requiredMsgType; + + if (missionType == MissionType.PHOTO) { + requiredMsgType = MessageType.IMAGE; + } else { + requiredMsgType = MessageType.TEXT; + } + + boolean hasPerformed = chatMessageRepository.existsByChatRoomAndMemberAndCreatedAtAfterAndMessageType( + chatRoomMission.getChatRoom(), + member, + chatRoomMission.getAssignedAt(), + requiredMsgType + ); + + if (!hasPerformed) { + throw new MissionException(MissionErrorStatus.NOT_COMPLETED_YET); + } + + // 상태 변경 및 점수 지급 log.complete();