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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
application.yml

### STS ###
.apt_generated
Expand Down
42 changes: 42 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,50 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// QueryDSL : OpenFeign
implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0"
implementation "io.github.openfeign.querydsl:querydsl-core:7.0"
annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.13'

// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'
}

tasks.named('test') {
useJUnitPlatform()
}

// QueryDSL 관련 설정
// generated/querydsl 폴더 생성 & 삽입
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile

// 소스 세트에 생성 경로 추가 (구체적인 경로 지정)
sourceSets {
main.java.srcDirs += [ querydslDir ]
}

// 컴파일 시 생성 경로 지정
tasks.withType(JavaCompile).configureEach {
options.generatedSourceOutputDirectory.set(querydslDir)
}

// clean 태스크에 생성 폴더 삭제 로직 추가
clean.doLast {
file(querydslDir).deleteDir()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.example.umc9th.domain.mission.controller;

import com.example.umc9th.domain.mission.converter.MissionConverter;
import com.example.umc9th.domain.mission.dto.MissionRequestDTO;
import com.example.umc9th.domain.mission.dto.MissionResponseDTO;
import com.example.umc9th.domain.mission.entity.Mission;
import com.example.umc9th.domain.mission.service.MissionService;
import com.example.umc9th.domain.user.entity.UserMission;
import com.example.umc9th.global.apiPayload.ApiResponse;
import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode;
import com.example.umc9th.global.validation.annotation.CheckPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@Validated
public class MissionController {

private final MissionService missionService;

@PostMapping("/restaurants/{restaurantId}/missions")
@Operation(summary = "가게에 미션 추가 API", description = "특정 가게에 미션을 추가하는 API입니다.")
public ApiResponse<MissionResponseDTO.AddResultDTO> addMission(@PathVariable Long restaurantId,
@RequestBody @Valid MissionRequestDTO.AddMissionDTO request) {
Mission mission = missionService.addMission(restaurantId, request);
return ApiResponse.onSuccess(GeneralSuccessCode.SUCCESS, MissionConverter.toAddResultDTO(mission));
}

@PostMapping("/missions/{missionId}/challenge")
@Operation(summary = "미션 도전하기 API", description = "미션을 도전하는 API입니다.")
public ApiResponse<MissionResponseDTO.ChallengeResultDTO> challengeMission(@PathVariable Long missionId,
@RequestBody @Valid MissionRequestDTO.ChallengeMissionDTO request) {
UserMission userMission = missionService.challengeMission(missionId, request);
return ApiResponse.onSuccess(GeneralSuccessCode.SUCCESS, MissionConverter.toChallengeResultDTO(userMission));
}

@GetMapping("/restaurants/{restaurantId}/missions")
@Operation(summary = "특정 가게의 미션 목록 조회 API", description = "특정 가게의 미션 목록을 조회하는 API이며, 페이징을 포함합니다. query String으로 page 번호를 주세요")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "acess 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON400", description = "잘못된 요청입니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
})
@Parameters({
@Parameter(name = "restaurantId", description = "가게의 아이디, path variable 입니다!"),
@Parameter(name = "page", description = "페이지 번호, 1번이 1 페이지 입니다."),
})
public ApiResponse<Page<MissionResponseDTO.MissionDTO>> getRestaurantMissions(@PathVariable Long restaurantId,
@CheckPage @RequestParam(name = "page") Integer page) {
Page<MissionResponseDTO.MissionDTO> response = missionService.getRestaurantMissions(restaurantId, page - 1);
return ApiResponse.onSuccess(GeneralSuccessCode.SUCCESS, response);
}

@GetMapping("/restaurants/{restaurantId}/missions/slice")
@Operation(summary = "특정 가게의 미션 목록 조회 API (Slice 버전)", description = "특정 가게의 미션 목록을 Slice 방식으로 조회하는 API입니다. query String으로 page 번호를 주세요")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "acess 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON400", description = "잘못된 요청입니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
})
@Parameters({
@Parameter(name = "restaurantId", description = "가게의 아이디, path variable 입니다!"),
@Parameter(name = "page", description = "페이지 번호, 1번이 1 페이지 입니다."),
})
public ApiResponse<Slice<MissionResponseDTO.MissionDTO>> getRestaurantMissionsSlice(@PathVariable Long restaurantId,
@CheckPage @RequestParam(name = "page") Integer page) {
Slice<MissionResponseDTO.MissionDTO> response = missionService.getRestaurantMissionsBySlice(restaurantId, page - 1);
return ApiResponse.onSuccess(GeneralSuccessCode.SUCCESS, response);
}
}

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

import com.example.umc9th.domain.mission.dto.MissionRequestDTO;
import com.example.umc9th.domain.mission.dto.MissionResponseDTO;
import com.example.umc9th.domain.mission.entity.Mission;
import com.example.umc9th.domain.restaurant.entity.Restaurant;
import com.example.umc9th.domain.user.entity.User;
import com.example.umc9th.domain.user.entity.UserMission;
import com.example.umc9th.domain.user.enums.MissionStatus;

import java.time.LocalDateTime;

public class MissionConverter {

public static MissionResponseDTO.AddResultDTO toAddResultDTO(Mission mission) {
return MissionResponseDTO.AddResultDTO.builder()
.missionId(mission.getId())
.createdAt(LocalDateTime.now())
.build();
}

public static MissionResponseDTO.ChallengeResultDTO toChallengeResultDTO(UserMission userMission) {
return MissionResponseDTO.ChallengeResultDTO.builder()
.userMissionId(userMission.getId())
.createdAt(LocalDateTime.now())
.build();
}

public static Mission toMission(MissionRequestDTO.AddMissionDTO request, Restaurant restaurant) {
return Mission.builder()
.restaurant(restaurant)
.reward(request.getReward())
.deadline(request.getDeadline())
.goal(request.getGoal())
.build();
}

public static UserMission toUserMission(Mission mission, User user) {
return UserMission.builder()
.mission(mission)
.user(user)
.status(MissionStatus.CHALLENGING)
.build();
}

public static MissionResponseDTO.MissionDTO toMissionDTO(Mission mission) {
return MissionResponseDTO.MissionDTO.builder()
.id(mission.getId())
.reward(mission.getReward())
.deadline(mission.getDeadline())
.goal(mission.getGoal())
.build();
}
}

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

import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;

import java.time.LocalDateTime;

public class MissionRequestDTO {

@Getter
public static class AddMissionDTO {
@NotNull
private Integer reward;
@NotNull
private Integer goal;
@NotNull
@Future
private LocalDateTime deadline;
}

@Getter
public static class ChallengeMissionDTO {
@NotNull
private Long userId;
}
}

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

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

public class MissionResponseDTO {

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class AddResultDTO {
private Long missionId;
private LocalDateTime createdAt;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class ChallengeResultDTO {
private Long userMissionId;
private LocalDateTime createdAt;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class MissionDTO {
private Long id;
private Integer reward;
private LocalDateTime deadline;
private Integer goal;
}
}

Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.example.umc9th.domain.mission.repository;

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

public interface MissionRepository extends JpaRepository<Mission, Long> {
Page<Mission> findAllByRestaurantId(Long restaurantId, Pageable pageable);
Slice<Mission> findSliceByRestaurantId(Long restaurantId, Pageable pageable);
}

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

import com.example.umc9th.domain.mission.dto.MissionRequestDTO;
import com.example.umc9th.domain.mission.dto.MissionResponseDTO;
import com.example.umc9th.domain.mission.entity.Mission;
import com.example.umc9th.domain.user.entity.UserMission;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice;

public interface MissionService {
Mission addMission(Long restaurantId, MissionRequestDTO.AddMissionDTO request);
UserMission challengeMission(Long missionId, MissionRequestDTO.ChallengeMissionDTO request);
Page<MissionResponseDTO.MissionDTO> getRestaurantMissions(Long restaurantId, Integer page);
Slice<MissionResponseDTO.MissionDTO> getRestaurantMissionsBySlice(Long restaurantId, Integer page);
}

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

import com.example.umc9th.domain.mission.converter.MissionConverter;
import com.example.umc9th.domain.mission.dto.MissionRequestDTO;
import com.example.umc9th.domain.mission.dto.MissionResponseDTO;
import com.example.umc9th.domain.mission.entity.Mission;
import com.example.umc9th.domain.mission.repository.MissionRepository;
import com.example.umc9th.domain.restaurant.entity.Restaurant;
import com.example.umc9th.domain.restaurant.repository.RestaurantRepository;
import com.example.umc9th.domain.user.entity.User;
import com.example.umc9th.domain.user.entity.UserMission;
import com.example.umc9th.domain.user.repository.UserMissionRepository;
import com.example.umc9th.domain.user.repository.UserRepository;
import com.example.umc9th.global.apiPayload.code.GeneralErrorCode;
import com.example.umc9th.global.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MissionServiceImpl implements MissionService {

private final MissionRepository missionRepository;
private final RestaurantRepository restaurantRepository;
private final UserRepository userRepository;
private final UserMissionRepository userMissionRepository;

@Override
@Transactional
public Mission addMission(Long restaurantId, MissionRequestDTO.AddMissionDTO request) {
Restaurant restaurant = restaurantRepository.findById(restaurantId)
.orElseThrow(() -> new GeneralException(GeneralErrorCode.RESTAURANT_NOT_FOUND));

Mission mission = MissionConverter.toMission(request, restaurant);

return missionRepository.save(mission);
}

@Override
@Transactional
public UserMission challengeMission(Long missionId, MissionRequestDTO.ChallengeMissionDTO request) {
Mission mission = missionRepository.findById(missionId)
.orElseThrow(() -> new GeneralException(GeneralErrorCode.MISSION_NOT_FOUND));

User user = userRepository.findById(request.getUserId())
.orElseThrow(() -> new GeneralException(GeneralErrorCode.USER_NOT_FOUND));

// 이미 도전 중인지 체크하는 로직이 있으면 좋겠지만 생략

UserMission userMission = MissionConverter.toUserMission(mission, user);

return userMissionRepository.save(userMission);
}

@Override
public Page<MissionResponseDTO.MissionDTO> getRestaurantMissions(Long restaurantId, Integer page) {
Restaurant restaurant = restaurantRepository.findById(restaurantId)
.orElseThrow(() -> new GeneralException(GeneralErrorCode.RESTAURANT_NOT_FOUND));

Page<Mission> missionPage = missionRepository.findAllByRestaurantId(restaurantId, PageRequest.of(page, 10));
return missionPage.map(MissionConverter::toMissionDTO);
}

@Override
public Slice<MissionResponseDTO.MissionDTO> getRestaurantMissionsBySlice(Long restaurantId, Integer page) {
Restaurant restaurant = restaurantRepository.findById(restaurantId)
.orElseThrow(() -> new GeneralException(GeneralErrorCode.RESTAURANT_NOT_FOUND));

Slice<Mission> missionSlice = missionRepository.findSliceByRestaurantId(restaurantId, PageRequest.of(page, 10));
return missionSlice.map(MissionConverter::toMissionDTO);
}
}

Loading