Skip to content
Open
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
@@ -0,0 +1,15 @@
package com.example.umc9th.application.mission.dto;

import lombok.Builder;

public class MissionResDTO {

@Builder
public record StoreMission(
Long missionId,
Integer rewardPoint,
String title,
String description
) {}
}

Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ public record Challenge(
UserMissionStatus status,
LocalDateTime assignedAt
) {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.umc9th.application.mission.service;

import com.example.umc9th.application.mission.dto.MissionResDTO;
import com.example.umc9th.domain.mission.Mission;
import com.example.umc9th.domain.mission.converter.MissionConverter;
import com.example.umc9th.global.paging.PageResponse;
import com.example.umc9th.infrastructure.mission.MissionJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MissionQueryService {

private final MissionJpaRepository missionRepository;

public PageResponse<MissionResDTO.StoreMission> getStoreMissions(Long storeId, int page) {

Pageable pageable = PageRequest.of(page, 10);

Page<Mission> missions = missionRepository.findByStoreId(storeId, pageable);

Page<MissionResDTO.StoreMission> dtoPage =
missions.map(MissionConverter::toStoreMission);

return PageResponse.from(dtoPage);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.umc9th.application.mission.service;

import com.example.umc9th.application.mission.dto.UserMissionResDTO;
import com.example.umc9th.domain.mission.UserMission;
import com.example.umc9th.global.paging.PageResponse;
import com.example.umc9th.infrastructure.mission.UserMissionJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.example.umc9th.domain.mission.converter.UserMissionConverter;
import com.example.umc9th.domain.enums.UserMissionStatus;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserMissionQueryService {

private final UserMissionJpaRepository userMissionRepository;

public PageResponse<UserMissionResDTO.Challenge> getMyProgressMissions(Long userId, int page) {
Pageable pageable = PageRequest.of(page, 10);

Page<UserMission> pageEntity =
userMissionRepository.findByUserIdAndStatus(userId, UserMissionStatus.IN_PROGRESS, pageable);

Page<UserMissionResDTO.Challenge> dtoPage =
pageEntity.map(UserMissionConverter::toChallengeDTO);

return PageResponse.from(dtoPage);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.umc9th.domain.mission.converter;

import com.example.umc9th.application.mission.dto.MissionResDTO;
import com.example.umc9th.domain.mission.Mission;

public class MissionConverter {

public static MissionResDTO.StoreMission toStoreMission(Mission mission) {
return MissionResDTO.StoreMission.builder()
.missionId(mission.getId())
// Mission 엔티티에 아직 rewardPoint가 없으므로 임시로 null
// TODO: rewardPoint 필드 생기면 mission.getRewardPoint()로 변경
.rewardPoint(null)
.title(mission.getTitle())
.description(mission.getDescription())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.umc9th.domain.enums.UserMissionStatus;
import com.example.umc9th.domain.mission.Mission;
import com.example.umc9th.domain.mission.UserMission;
import com.example.umc9th.domain.store.Store;
import com.example.umc9th.domain.user.User;

import java.time.LocalDateTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@ public static Review toStoreReview(
.build();

if (dto.photoUrls() != null) {
for (String url : dto.photoUrls()) {
ReviewPhoto photo = ReviewPhoto.builder()
.review(review) // 연관관계 주인 설정
.url(url)
.build();
review.getPhotos().add(photo); // 양방향 컬렉션 쪽에도 추가
}
dto.photoUrls().stream()
.map(url -> ReviewPhoto.builder()
.review(review) // 연관관계 주인 설정
.url(url)
.build())
.forEach(photo -> review.getPhotos().add(photo)); // 양방향 컬렉션에도 추가
}

return review;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
Expand All @@ -23,6 +24,7 @@ public enum GeneralErrorCode implements BaseErrorCode{
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,
"COMMON500_1",
"예기치 않은 서버 에러가 발생했습니다."),
INVALID_PAGE(HttpStatus.BAD_REQUEST, "PAGE_001", "page는 1 이상의 정수여야 합니다."),
;

private final HttpStatus status;
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/example/umc9th/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.umc9th.global.config;

import com.example.umc9th.global.resolver.PositivePageArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final PositivePageArgumentResolver positivePageArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(positivePageArgumentResolver);
}
}

32 changes: 32 additions & 0 deletions src/main/java/com/example/umc9th/global/paging/PageResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.umc9th.global.paging;

import lombok.Builder;
import lombok.Getter;
import org.springframework.data.domain.Page;

import java.util.List;

// global/paging/PageResponse.java
@Getter
@Builder
public class PageResponse<T> {

private List<T> content;
private int page; // 프론트 기준 1-based
private int size;
private long totalElements;
private int totalPages;
private boolean last;

public static <T> PageResponse<T> from(Page<T> page) {
return PageResponse.<T>builder()
.content(page.getContent())
.page(page.getNumber() + 1) // 0-based → 1-based
.size(page.getSize())
.totalElements(page.getTotalElements())
.totalPages(page.getTotalPages())
.last(page.isLast())
.build();
}
}

13 changes: 13 additions & 0 deletions src/main/java/com/example/umc9th/global/resolver/PositivePage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.umc9th.global.resolver;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// global/resolver/PositivePage.java
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface PositivePage {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.umc9th.global.resolver;

import com.example.umc9th.global.apiPayload.code.GeneralErrorCode;
import com.example.umc9th.global.apiPayload.exception.GeneralException;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class PositivePageArgumentResolver implements HandlerMethodArgumentResolver {

private static final String PARAM_NAME = "page";
private static final int DEFAULT_PAGE = 1;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(PositivePage.class)
&& (parameter.getParameterType().equals(Integer.class)
|| parameter.getParameterType().equals(int.class));
}

@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {

String raw = webRequest.getParameter(PARAM_NAME);

int page;
if (raw == null || raw.isBlank()) {
page = DEFAULT_PAGE;
} else {
try {
page = Integer.parseInt(raw);
} catch (NumberFormatException e) {
throw new GeneralException(GeneralErrorCode.INVALID_PAGE);
}
}

if (page <= 0) {
throw new GeneralException(GeneralErrorCode.INVALID_PAGE);
}

// 프론트는 1부터, 서버는 0부터 사용
return page - 1;
}
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.example.umc9th.infrastructure.mission;

import com.example.umc9th.domain.mission.Mission;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MissionJpaRepository extends JpaRepository<Mission, Long> {
Page<Mission> findByStoreId(Long storeId, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.umc9th.application.mission.dto.MissionCardDto;
import com.example.umc9th.application.mission.dto.MyMissionRowDto;
import com.example.umc9th.application.mission.dto.ReviewTargetDto;
import com.example.umc9th.domain.enums.UserMissionStatus;
import com.example.umc9th.domain.mission.Mission;
import com.example.umc9th.domain.mission.UserMission;
import com.example.umc9th.domain.user.User;
Expand All @@ -13,8 +14,13 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface UserMissionJpaRepository extends JpaRepository<UserMission, Long> {

Page<UserMission> findByUserIdAndStatus(Long userId, UserMissionStatus status, Pageable pageable);

Optional<UserMission> findByIdAndUserId(Long id, Long userId);
// 이미 해당 미션 도전 중인지 체크
boolean existsByUserAndMission(User user, Mission mission);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.umc9th.presentation.mission;

import com.example.umc9th.application.mission.dto.MissionResDTO;
import com.example.umc9th.application.mission.service.MissionQueryService;
import com.example.umc9th.global.apiPayload.ApiResponse;
import com.example.umc9th.global.apiPayload.code.BaseSuccessCode;
import com.example.umc9th.global.paging.PageResponse;
import com.example.umc9th.global.resolver.PositivePage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/stores")
@Tag(name = "Mission", description = "미션 관련 API")
public class MissionController {

private final MissionQueryService missionQueryService;

@GetMapping("/{storeId}/missions")
@Operation(summary = "특정 가게의 미션 목록 조회")
public ApiResponse<Void> getStoreMissions(
@PathVariable Long storeId,
@PositivePage @RequestParam(name = "page", required = false) Integer page
) {
BaseSuccessCode response =
(BaseSuccessCode) missionQueryService.getStoreMissions(storeId, page);
return ApiResponse.onSuccess(response);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ public ApiResponse<UserMissionResDTO.Challenge> challengeMission(
);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.umc9th.presentation.mission;

import com.example.umc9th.application.mission.dto.UserMissionResDTO;
import com.example.umc9th.application.mission.service.UserMissionQueryService;
import com.example.umc9th.global.apiPayload.ApiResponse;
import com.example.umc9th.global.apiPayload.code.BaseSuccessCode;
import com.example.umc9th.global.paging.PageResponse;
import com.example.umc9th.global.resolver.PositivePage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/missions/me")
@Tag(name = "UserMission", description = "사용자 미션 API")
public class UserMissionController {

private final UserMissionQueryService userMissionQueryService;

@GetMapping("/progress")
@Operation(summary = "내가 진행중인 미션 목록 조회")
public ApiResponse<Void> getMyProgressMissions(
@PositivePage @RequestParam(name = "page", required = false) Integer page
) {
// TODO: 나중에 Spring Security 붙이면 로그인 사용자 ID로 변경
Long userId = 1L;

BaseSuccessCode result =
(BaseSuccessCode) userMissionQueryService.getMyProgressMissions(userId, page);

return ApiResponse.onSuccess(result);
}
}
Loading