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,30 +1,39 @@
package com.example.umc.domain.mission.controller;

import com.example.umc.domain.mission.converter.MissionConverter;
import com.example.umc.domain.mission.dto.MissionReqDTO;
import com.example.umc.domain.mission.dto.MissionResDTO;
import com.example.umc.domain.mission.entity.Mission;
import com.example.umc.domain.mission.exception.code.MissionSuccessCode;
import com.example.umc.domain.mission.service.MissionCommandService;
import com.example.umc.domain.mission.service.MissionQueryService;
import com.example.umc.global.apiPayload.ApiResponse;
import com.example.umc.global.apiPayload.code.status.SuccessStatus;
import com.example.umc.global.validation.CheckPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/missions")
@RequiredArgsConstructor
@Validated
@Tag(name = "미션", description = "미션 관련 API")
public class MissionController {

private final MissionCommandService missionCommandService;
private final MissionQueryService missionQueryService;

// 미션 도전하기
@PostMapping("/challenge")
@Operation(summary = "미션 도전하기", description = "가게의 미션을 도전 중인 미션에 추가합니다.")
public ApiResponse<MissionResDTO.ChallengeMissionDTO> challengeMission(
@RequestBody @Valid MissionReqDTO.ChallengeMissionDTO dto
) {
@RequestBody @Valid MissionReqDTO.ChallengeMissionDTO dto) {
MissionResDTO.ChallengeMissionDTO response = missionCommandService.challengeMission(dto);
return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CHALLENGED, response);
}
Expand All @@ -33,9 +42,40 @@ public ApiResponse<MissionResDTO.ChallengeMissionDTO> challengeMission(
@PostMapping("")
@Operation(summary = "미션 추가", description = "가게에 미션을 추가합니다.")
public ApiResponse<MissionResDTO.CreateMissionDTO> createMission(
@RequestBody @Valid MissionReqDTO.CreateMissionDTO dto
) {
@RequestBody @Valid MissionReqDTO.CreateMissionDTO dto) {
MissionResDTO.CreateMissionDTO response = missionCommandService.createMission(dto);
return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CREATED, response);
}

@GetMapping("/stores/{storeId}")
@Operation(summary = "특정 가게의 미션 목록 조회", description = "특정 가게의 모든 미션을 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.")
public ApiResponse<MissionResDTO.MissionPreViewListDTO> getStoreMissionList(
@Parameter(description = "가게 ID", required = true) @PathVariable Long storeId,
@Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") @RequestParam(defaultValue = "1") @CheckPage Integer page) {
Page<Mission> missionPage = missionQueryService.getStoreMissions(storeId, page);
MissionResDTO.MissionPreViewListDTO response = MissionConverter.toMissionPreViewListDTO(missionPage);
return ApiResponse.onSuccess(SuccessStatus._OK, response);
}

@GetMapping("/my-ongoing")
@Operation(summary = "내가 진행중인 미션 목록 조회", description = "로그인한 사용자가 진행중인 미션을 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.")
public ApiResponse<MissionResDTO.UserMissionPreViewListDTO> getMyOngoingMissionList(
@Parameter(description = "사용자 ID", required = true) @RequestParam Long userId,
@Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") @RequestParam(defaultValue = "1") @CheckPage Integer page) {
Page<com.example.umc.domain.mission.entity.UserMission> userMissionPage = missionQueryService
.getUserMissionsByStatus(userId, com.example.umc.domain.mission.enums.UserMissionStatus.IN_PROGRESS,
page);
MissionResDTO.UserMissionPreViewListDTO response = MissionConverter
.toUserMissionPreViewListDTO(userMissionPage);
return ApiResponse.onSuccess(SuccessStatus._OK, response);
}

// 진행중인 미션 완료 처리
@PatchMapping("/complete")
@Operation(summary = "진행중인 미션 완료 처리", description = "진행중인 미션을 완료 상태로 변경하고, 변경된 미션 정보를 조회하여 반환합니다.")
public ApiResponse<MissionResDTO.CompleteMissionDTO> completeMission(
@RequestBody @Valid MissionReqDTO.CompleteMissionDTO dto) {
MissionResDTO.CompleteMissionDTO response = missionCommandService.completeMission(dto);
return ApiResponse.onSuccess(MissionSuccessCode.MISSION_COMPLETED, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,123 @@
import com.example.umc.domain.mission.entity.UserMission;
import com.example.umc.domain.store.entity.Store;
import com.example.umc.domain.user.entity.User;
import com.example.umc.global.apiPayload.dto.PagingInfoDTO;
import com.example.umc.global.common.util.PageUtil;
import org.springframework.data.domain.Page;

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

public class MissionConverter {

// Entity -> DTO (ChallengeMission)
public static MissionResDTO.ChallengeMissionDTO toChallengeMissionDTO(UserMission userMission) {
return MissionResDTO.ChallengeMissionDTO.builder()
.challengeMissionId(userMission.getChallengeMissionId())
.createdAt(userMission.getCreatedAt())
.build();
}

// DTO -> Entity (ChallengeMission)
public static UserMission toUserMission(Mission mission, User user, Store store) {
return UserMission.builder()
.user(user)
.mission(mission)
.store(store)
.challengeAt(LocalDateTime.now())
.build();
}

// Entity -> DTO (CreateMission)
public static MissionResDTO.CreateMissionDTO toCreateMissionDTO(Mission mission) {
return MissionResDTO.CreateMissionDTO.builder()
.missionId(mission.getMissionId())
.createdAt(mission.getCreatedAt())
.build();
}

// DTO -> Entity (CreateMission)
public static Mission toMission(MissionReqDTO.CreateMissionDTO dto, Store store) {
return Mission.builder()
.store(store)
.region(dto.region())
.missionMoney(dto.missionMoney())
.missionPoint(dto.missionPoint())
.build();
}
// Entity -> DTO (ChallengeMission)
public static MissionResDTO.ChallengeMissionDTO toChallengeMissionDTO(UserMission userMission) {
return MissionResDTO.ChallengeMissionDTO.builder()
.challengeMissionId(userMission.getChallengeMissionId())
.createdAt(userMission.getCreatedAt())
.build();
}

// DTO -> Entity (ChallengeMission)
public static UserMission toUserMission(Mission mission, User user, Store store) {
return UserMission.builder()
.user(user)
.mission(mission)
.store(store)
.challengeAt(LocalDateTime.now())
.build();
}

// Entity -> DTO (CreateMission)
public static MissionResDTO.CreateMissionDTO toCreateMissionDTO(Mission mission) {
return MissionResDTO.CreateMissionDTO.builder()
.missionId(mission.getMissionId())
.createdAt(mission.getCreatedAt())
.build();
}

// DTO -> Entity (CreateMission)
public static Mission toMission(MissionReqDTO.CreateMissionDTO dto, Store store) {
return Mission.builder()
.store(store)
.region(dto.region())
.missionMoney(dto.missionMoney())
.missionPoint(dto.missionPoint())
.build();
}

// Entity -> MissionPreViewDTO
public static MissionResDTO.MissionPreViewDTO toMissionPreViewDTO(Mission mission) {
return MissionResDTO.MissionPreViewDTO.builder()
.missionId(mission.getMissionId())
.storeName(mission.getStore().getStoreName())
.missionMoney(mission.getMissionMoney())
.missionPoint(mission.getMissionPoint())
.region(mission.getRegion())
.createdAt(mission.getCreatedAt())
.build();
}

// Page<Mission> -> MissionPreViewListDTO
public static MissionResDTO.MissionPreViewListDTO toMissionPreViewListDTO(Page<Mission> missionPage) {
List<MissionResDTO.MissionPreViewDTO> missionList = missionPage.stream()
.map(MissionConverter::toMissionPreViewDTO)
.toList();

PagingInfoDTO pagingInfo = PageUtil.toPagingInfo(missionPage);

return MissionResDTO.MissionPreViewListDTO.builder()
.missionList(missionList)
.listSize(missionList.size())
.currentPage(pagingInfo.currentPage())
.totalPages(pagingInfo.totalPages())
.totalElements(pagingInfo.totalElements())
.isFirst(pagingInfo.isFirst())
.isLast(pagingInfo.isLast())
.build();
}

// UserMission -> UserMissionPreViewDTO
public static MissionResDTO.UserMissionPreViewDTO toUserMissionPreViewDTO(UserMission userMission) {
return MissionResDTO.UserMissionPreViewDTO.builder()
.challengeMissionId(userMission.getChallengeMissionId())
.storeName(userMission.getStore().getStoreName())
.missionMoney(userMission.getMission().getMissionMoney())
.missionPoint(userMission.getMission().getMissionPoint())
.region(userMission.getMission().getRegion())
.status(userMission.getStatus())
.challengeAt(userMission.getChallengeAt())
.createdAt(userMission.getCreatedAt())
.build();
}

// Page<UserMission> -> UserMissionPreViewListDTO
public static MissionResDTO.UserMissionPreViewListDTO toUserMissionPreViewListDTO(
Page<UserMission> userMissionPage) {
List<MissionResDTO.UserMissionPreViewDTO> userMissionList = userMissionPage.stream()
.map(MissionConverter::toUserMissionPreViewDTO)
.toList();

PagingInfoDTO pagingInfo = PageUtil.toPagingInfo(userMissionPage);

return MissionResDTO.UserMissionPreViewListDTO.builder()
.userMissionList(userMissionList)
.listSize(userMissionList.size())
.currentPage(pagingInfo.currentPage())
.totalPages(pagingInfo.totalPages())
.totalElements(pagingInfo.totalElements())
.isFirst(pagingInfo.isFirst())
.isLast(pagingInfo.isLast())
.build();
}

// UserMission -> CompleteMissionDTO
public static MissionResDTO.CompleteMissionDTO toCompleteMissionDTO(UserMission userMission) {
return MissionResDTO.CompleteMissionDTO.builder()
.challengeMissionId(userMission.getChallengeMissionId())
.status(userMission.getStatus())
.completedAt(userMission.getCompletedAt())
.updatedAt(userMission.getUpdatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ public record CreateMissionDTO(
Long missionMoney,
Long missionPoint) {
}

public record CompleteMissionDTO(
Long challengeMissionId) {
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.example.umc.domain.mission.dto;

import com.example.umc.domain.mission.enums.Region;
import com.example.umc.domain.mission.enums.UserMissionStatus;
import lombok.Builder;

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

public class MissionResDTO {
@Builder
Expand All @@ -16,4 +19,56 @@ public record CreateMissionDTO(
Long missionId,
LocalDateTime createdAt
) {}

@Builder
public record MissionPreViewDTO(
Long missionId,
String storeName,
Long missionMoney,
Long missionPoint,
Region region,
LocalDateTime createdAt
) {}

@Builder
public record MissionPreViewListDTO(
List<MissionPreViewDTO> missionList,
Integer listSize,
Integer currentPage,
Integer totalPages,
Long totalElements,
Boolean isFirst,
Boolean isLast
) {}

@Builder
public record UserMissionPreViewDTO(
Long challengeMissionId,
String storeName,
Long missionMoney,
Long missionPoint,
Region region,
UserMissionStatus status,
LocalDateTime challengeAt,
LocalDateTime createdAt
) {}

@Builder
public record UserMissionPreViewListDTO(
List<UserMissionPreViewDTO> userMissionList,
Integer listSize,
Integer currentPage,
Integer totalPages,
Long totalElements,
Boolean isFirst,
Boolean isLast
) {}

@Builder
public record CompleteMissionDTO(
Long challengeMissionId,
UserMissionStatus status,
java.time.LocalDateTime completedAt,
java.time.LocalDateTime updatedAt
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,12 @@ public class UserMission extends BaseEntity {

@Column(name = "success_id", length = 100)
private String successId;

// 미션 상태를 완료로 변경
public void updateStatus(UserMissionStatus status) {
this.status = status;
if (status == UserMissionStatus.COMPLETED) {
this.completedAt = java.time.LocalDateTime.now();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum MissionErrorCode implements BaseErrorCode {

MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404_1", "해당 미션을 찾지 못했습니다."),
MISSION_ALREADY_CHALLENGED(HttpStatus.BAD_REQUEST, "MISSION400_1", "이미 도전 중인 미션입니다."),
MISSION_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "MISSION400_2", "이미 완료된 미션입니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum MissionSuccessCode implements BaseCode {

MISSION_CHALLENGED(HttpStatus.CREATED, "MISSION201_1", "미션 도전이 성공적으로 등록되었습니다."),
MISSION_CREATED(HttpStatus.CREATED, "MISSION201_2", "미션이 성공적으로 생성되었습니다."),
MISSION_COMPLETED(HttpStatus.OK, "MISSION200_1", "미션이 성공적으로 완료되었습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface MissionRepository extends JpaRepository<Mission, Long> {
// 1. 홈 화면 미션 목록 조회 - 사용자가 아직 수행하지 않은 특정 지역 미션 조회
Expand Down Expand Up @@ -39,16 +37,11 @@ Page<Object[]> findAvailableMissionsWithCompletedCount(@Param("userId") Long use
@Param("region") Region region,
Pageable pageable);

// 2. 홈 화면 미션 목록 조회 - 커서 방식 페이징 (다음 페이지)
@Query("SELECT m FROM Mission m " +
"JOIN FETCH m.store s " +
"LEFT JOIN UserMission um ON m.missionId = um.mission.missionId AND um.user.userId = :userId " +
"WHERE m.region = :region " +
"AND um.mission.missionId IS NULL " +
"AND m.missionId < :cursor " +
"WHERE m.store.storeId = :storeId " +
"ORDER BY m.missionId DESC")
List<Mission> findAvailableMissionsNextPage(@Param("userId") Long userId,
@Param("region") Region region,
@Param("cursor") Long cursor,
Pageable pageable);
Page<Mission> findByStoreStoreId(
@Param("storeId") Long storeId,
Pageable pageable
);
}
Loading