diff --git a/src/main/java/io/oeid/mogakgo/common/base/CursorPaginationResult.java b/src/main/java/io/oeid/mogakgo/common/base/CursorPaginationResult.java index 83d90fb9..490e3863 100644 --- a/src/main/java/io/oeid/mogakgo/common/base/CursorPaginationResult.java +++ b/src/main/java/io/oeid/mogakgo/common/base/CursorPaginationResult.java @@ -1,6 +1,7 @@ package io.oeid.mogakgo.common.base; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -18,19 +19,19 @@ public class CursorPaginationResult { private Integer size; private CursorPaginationResult(List data, Integer size) { - this.data = data; this.size = size; if (data.size() > size) { this.hasNext = true; - this.data.remove(data.size() - 1); + this.data = new ArrayList<>(data.subList(0, size)); } else { this.hasNext = false; + this.data = new ArrayList<>(data); } this.numberOfElements = data.size(); } private CursorPaginationResult(List data, Integer size, boolean hasNext) { - this.data = data; + this.data = new ArrayList<>(data); this.numberOfElements = data.size(); this.hasNext = hasNext; this.size = size; diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/MatchingSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/MatchingSwagger.java new file mode 100644 index 00000000..0838d801 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/MatchingSwagger.java @@ -0,0 +1,47 @@ +package io.oeid.mogakgo.common.swagger.template; + +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerMatchingErrorExamples; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectErrorExamples; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples; +import io.oeid.mogakgo.domain.matching.presentation.dto.MatchingId; +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.ExampleObject; +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 org.springframework.http.ResponseEntity; + +@Tag(name = "Matching", description = "매칭 관련 API") +@SuppressWarnings("unused") +public interface MatchingSwagger { + + @Operation(summary = "매칭 취소", description = "회원이 현재 진행 중인 매칭을 취소할때 사용하는 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "매칭 취소 완료"), + @ApiResponse(responseCode = "400", description = "매칭을 취소할 수 없는 상태임", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject(name = "E090101", value = SwaggerMatchingErrorExamples.MATCHING_CANCEL_NOT_ALLOWED), + @ExampleObject(name = "E030110", value = SwaggerProjectErrorExamples.INVALID_PROJECT_STATUS_TO_FINISH) + })), + @ApiResponse(responseCode = "403", description = "요청을 보낸 사람이 매칭 취소할 권한이 안됨 (매칭 참여자가 아님)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(name = "E090201", value = SwaggerMatchingErrorExamples.MATCHING_FORBIDDEN_OPERATION))), + @ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject(name = "E090201", value = SwaggerMatchingErrorExamples.MATCHING_NOT_FOUND), + @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND) + })), + }) + ResponseEntity cancel( + @Parameter(hidden = true) Long userId, + @Parameter(description = "매칭 ID", required = true) Long matchingId + ); + +} diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/NotificationSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/NotificationSwagger.java index c6a1defb..235197ff 100644 --- a/src/main/java/io/oeid/mogakgo/common/swagger/template/NotificationSwagger.java +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/NotificationSwagger.java @@ -2,8 +2,11 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; +import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples; import io.oeid.mogakgo.domain.notification.presentation.dto.req.FCMTokenApiRequest; +import io.oeid.mogakgo.domain.notification.presentation.dto.res.NotificationPublicApiRes; import io.oeid.mogakgo.exception.dto.ErrorResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -29,4 +32,18 @@ public interface NotificationSwagger { ) ResponseEntity manageFCMToken(@Parameter(hidden = true) Long userId, FCMTokenApiRequest request); + + @Operation(summary = "알림 조회", description = "회원의 알림을 조회할 때 사용하는 API") + @ApiResponse(responseCode = "200", description = "알림 조회 성공", + content = @Content(schema = @Schema(implementation = NotificationPublicApiRes.class))) + @ApiResponse(responseCode = "404", description = "요청한 유저가 존재하지 않음", content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND) + }) + ) + ResponseEntity> getByUserId( + @Parameter(hidden = true) Long id, + CursorPaginationInfoReq pageable); } diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/OAuth2Swagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/OAuth2Swagger.java index bd35fecf..dd24baa8 100644 --- a/src/main/java/io/oeid/mogakgo/common/swagger/template/OAuth2Swagger.java +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/OAuth2Swagger.java @@ -2,7 +2,6 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import io.oeid.mogakgo.domain.auth.presentation.dto.res.AuthAccessTokenResponse; import io.oeid.mogakgo.domain.auth.presentation.dto.res.AuthLoginUrlResponse; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; @@ -11,19 +10,22 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.core.user.OAuth2User; @Tag(name = "OAuth2", description = "OAuth2 관련 API") @SuppressWarnings("unused") public interface OAuth2Swagger { + @Operation(summary = "로그인 URL 반환", description = "로그인 URL을 반환하는 API") @ApiResponse(responseCode = "200", description = "로그인 URL 반환 성공", content = @Content( mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = AuthLoginUrlResponse.class))) ResponseEntity login(); + @Hidden - ResponseEntity loginSuccess( - OAuth2User oAuth2User, HttpServletResponse response); + void loginSuccess( + OAuth2User oAuth2User, HttpServletResponse response) throws IOException; } diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectJoinReqSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectJoinReqSwagger.java index 5771a754..bdd8b1ff 100644 --- a/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectJoinReqSwagger.java +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectJoinReqSwagger.java @@ -6,7 +6,7 @@ import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectJoinReqErrorExamples; import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectJoinRequestErrorExamples; import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples; -import io.oeid.mogakgo.domain.matching.presentation.dto.res.MatchingId; +import io.oeid.mogakgo.domain.matching.presentation.dto.MatchingId; import io.oeid.mogakgo.domain.project_join_req.application.dto.req.ProjectJoinCreateReq; import io.oeid.mogakgo.domain.project_join_req.presentation.dto.res.ProjectJoinRequestAPIRes; import io.oeid.mogakgo.domain.project_join_req.presentation.dto.res.ProjectJoinRequestDetailAPIRes; diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectSwagger.java index 89d22532..7afd418b 100644 --- a/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectSwagger.java +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectSwagger.java @@ -1,11 +1,13 @@ package io.oeid.mogakgo.common.swagger.template; +import io.oeid.mogakgo.common.annotation.UserId; import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.core.properties.swagger.error.SwaggerGeoErrorExamples; import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectErrorExamples; import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples; import io.oeid.mogakgo.domain.geo.domain.enums.Region; +import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDensityRankRes; import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; import io.oeid.mogakgo.domain.project.presentation.dto.req.ProjectCreateReq; import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectIdRes; @@ -20,7 +22,9 @@ 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 jakarta.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; @Tag(name = "Project Card", description = "프로젝트 카드 관련 API") @SuppressWarnings("unused") @@ -171,4 +175,11 @@ ResponseEntity> getJoinRequest( @Parameter(description = "프로젝트 ID", required = true) Long id, @Parameter(hidden = true) CursorPaginationInfoReq pageable ); + + @Operation(summary = "지역별 프로젝트 밀도 순위 조회", description = "지역별 프로젝트 밀도 순위를 조회할 때 사용하는 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "지역별 프로젝트 밀도 순위 조회 성공", + content = @Content(schema = @Schema(implementation = ProjectDensityRankRes.class))), + }) + ResponseEntity getDensityRankProjects(); } diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerMatchingErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerMatchingErrorExamples.java new file mode 100644 index 00000000..e7eb5503 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerMatchingErrorExamples.java @@ -0,0 +1,9 @@ +package io.oeid.mogakgo.core.properties.swagger.error; + +public class SwaggerMatchingErrorExamples { + + public static final String MATCHING_CANCEL_NOT_ALLOWED = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E090101\",\"message\":\"이미 취소 되었거나 종료된 매칭은 취소할 수 없습니다.\"}"; + public static final String MATCHING_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":404,\"code\":\"E090301\",\"message\":\"해당 매칭이 존재하지 않습니다.\"}"; + public static final String MATCHING_FORBIDDEN_OPERATION = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":403,\"code\":\"E090201\",\"message\":\"해당 매칭에 대한 권한이 없습니다.\"}"; + +} diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerProjectErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerProjectErrorExamples.java index d14bb8d5..f57426ef 100644 --- a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerProjectErrorExamples.java +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerProjectErrorExamples.java @@ -14,6 +14,7 @@ public class SwaggerProjectErrorExamples { public static final String PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":403,\"code\":\"E050201\",\"message\":\"해당 프로젝트 요청에 대한 권한이 없습니다.\"}"; public static final String INVALID_MATCHING_USER_TO_CREATE_PROJECT = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E030108\",\"message\":\"매칭이 진행 중인 유저는 프로젝트 생성을 할 수 없습니다.\"}"; public static final String ALREADY_EXIST_PROGRESS_PROJECT = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E030109\",\"message\":\"종료되지 않은 프로젝트 카드가 있으면 프로젝트 생성을 할 수 없습니다.\"}"; + public static final String INVALID_PROJECT_STATUS_TO_FINISH = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E030110\",\"message\":\"매칭중인 프로젝트가 아니여서 프로젝트를 종료할 수 없습니다.\"}"; private SwaggerProjectErrorExamples() { } diff --git a/src/main/java/io/oeid/mogakgo/domain/auth/presentation/OAuth2Controller.java b/src/main/java/io/oeid/mogakgo/domain/auth/presentation/OAuth2Controller.java index b5f049f9..046efde1 100644 --- a/src/main/java/io/oeid/mogakgo/domain/auth/presentation/OAuth2Controller.java +++ b/src/main/java/io/oeid/mogakgo/domain/auth/presentation/OAuth2Controller.java @@ -1,10 +1,10 @@ package io.oeid.mogakgo.domain.auth.presentation; import io.oeid.mogakgo.common.swagger.template.OAuth2Swagger; -import io.oeid.mogakgo.domain.auth.presentation.dto.res.AuthAccessTokenResponse; import io.oeid.mogakgo.domain.auth.presentation.dto.res.AuthLoginUrlResponse; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; @@ -20,20 +20,24 @@ public class OAuth2Controller implements OAuth2Swagger { private final String serverUrl; + private final String clientUrl; - public OAuth2Controller(@Value("${auth.server-url}") String serverUrl) { + public OAuth2Controller(@Value("${auth.server-url}") String serverUrl, + @Value("${auth.client-url}") String clientUrl) { this.serverUrl = serverUrl; + this.clientUrl = clientUrl; } @GetMapping("/login/success") - public ResponseEntity loginSuccess( - @AuthenticationPrincipal OAuth2User oAuth2User, HttpServletResponse response) { + public void loginSuccess(@AuthenticationPrincipal OAuth2User oAuth2User, + HttpServletResponse response) throws IOException { String accessToken = oAuth2User.getAttributes().get("accessToken").toString(); + boolean signUpComplete = (boolean) oAuth2User.getAttributes().get("signUpComplete"); String refreshToken = oAuth2User.getAttributes().get("refreshToken").toString(); int refreshTokenExpireTime = (int) oAuth2User.getAttributes().get("refreshTokenExpireTime"); - boolean signUpComplete = (boolean) oAuth2User.getAttributes().get("signUpComplete"); setCookie(refreshToken, refreshTokenExpireTime, response); - return ResponseEntity.ok(AuthAccessTokenResponse.of(accessToken, signUpComplete)); + String redirectUrl = signUpComplete ? clientUrl : clientUrl + "/signup"; + response.sendRedirect(redirectUrl + "?accessToken=" + accessToken); } @GetMapping("/login") diff --git a/src/main/java/io/oeid/mogakgo/domain/geo/domain/enums/Region.java b/src/main/java/io/oeid/mogakgo/domain/geo/domain/enums/Region.java index 765986f4..6cbb4027 100644 --- a/src/main/java/io/oeid/mogakgo/domain/geo/domain/enums/Region.java +++ b/src/main/java/io/oeid/mogakgo/domain/geo/domain/enums/Region.java @@ -1,5 +1,6 @@ package io.oeid.mogakgo.domain.geo.domain.enums; +import java.util.List; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -47,5 +48,13 @@ public static Region getByAreaCode(int areaCode) { return null; } + public static List getDefaultDensityRank() { + return List.of( + JONGRO, JUNG, YONGSAN, SEONGDONG, KWANGJIN, DONGDAEMUN, JUNGRANG, SEONGBUK, KANGBUK, + DOBONG, NOWON, EUNPYEONG, SEODAEMUN, MAPO, YANGCHUN, KANGSEO, GURO, GEUMCHUN, + YOUNGDEUNGPO, DONGJAK, KWANAK, SEOCHO, KANGNAM, SONGPA, KANGDONG + ); + } + } diff --git a/src/main/java/io/oeid/mogakgo/domain/matching/application/MatchingService.java b/src/main/java/io/oeid/mogakgo/domain/matching/application/MatchingService.java index c320b5a6..da9094f8 100644 --- a/src/main/java/io/oeid/mogakgo/domain/matching/application/MatchingService.java +++ b/src/main/java/io/oeid/mogakgo/domain/matching/application/MatchingService.java @@ -1,8 +1,13 @@ package io.oeid.mogakgo.domain.matching.application; +import static io.oeid.mogakgo.exception.code.ErrorCode404.MATCHING_NOT_FOUND; + import io.oeid.mogakgo.domain.matching.domain.entity.Matching; +import io.oeid.mogakgo.domain.matching.exception.MatchingException; import io.oeid.mogakgo.domain.matching.infrastructure.MatchingJpaRepository; import io.oeid.mogakgo.domain.project_join_req.domain.entity.ProjectJoinRequest; +import io.oeid.mogakgo.domain.user.application.UserCommonService; +import io.oeid.mogakgo.domain.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,6 +19,8 @@ public class MatchingService { private final MatchingJpaRepository matchingJpaRepository; + private final UserCommonService userCommonService; + @Transactional public Long create(ProjectJoinRequest projectJoinRequest) { Matching matching = Matching.builder() @@ -25,4 +32,22 @@ public Long create(ProjectJoinRequest projectJoinRequest) { return matching.getId(); } + + @Transactional + public Long cancel(Long tokenUserId, Long matchingId) { + User tokenUser = userCommonService.getUserById(tokenUserId); + + Matching matching = getMatching(matchingId); + + // 매칭 취소 + // 프로젝트 종료 상태 변경 + matching.cancel(tokenUser); + + return matching.getId(); + } + + private Matching getMatching(Long matchingId) { + return matchingJpaRepository.findById(matchingId) + .orElseThrow(() -> new MatchingException(MATCHING_NOT_FOUND)); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/matching/domain/entity/Matching.java b/src/main/java/io/oeid/mogakgo/domain/matching/domain/entity/Matching.java index 770649d3..2b70d08e 100644 --- a/src/main/java/io/oeid/mogakgo/domain/matching/domain/entity/Matching.java +++ b/src/main/java/io/oeid/mogakgo/domain/matching/domain/entity/Matching.java @@ -1,8 +1,10 @@ package io.oeid.mogakgo.domain.matching.domain.entity; import static io.oeid.mogakgo.domain.matching.domain.entity.enums.MatchingStatus.PROGRESS; +import static io.oeid.mogakgo.exception.code.ErrorCode403.MATCHING_FORBIDDEN_OPERATION; import io.oeid.mogakgo.domain.matching.domain.entity.enums.MatchingStatus; +import io.oeid.mogakgo.domain.matching.exception.MatchingException; import io.oeid.mogakgo.domain.project.domain.entity.Project; import io.oeid.mogakgo.domain.user.domain.User; import jakarta.persistence.Column; @@ -19,6 +21,7 @@ import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import java.time.LocalDateTime; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -63,4 +66,25 @@ private Matching( this.sender = sender; this.matchingStatus = PROGRESS; } + + // TODO: 프로젝트 상태 변경과 매칭 상태 변경을 동시에 처리하는 것을 보장하는것이 좋을 것 같다. + public void cancel(User tokenUser) { + validateAvailableCancel(tokenUser); + project.finish(); + this.matchingStatus = MatchingStatus.CANCELED; + } + + private void validateAvailableCancel(User tokenUser) { + validateParticipants(tokenUser); + matchingStatus.validateAvailableCancel(); + } + + public void validateParticipants(User tokenUser) { + var participants = List.of(project.getCreator().getId(), sender.getId()); + + if (!participants.contains(tokenUser.getId())) { + throw new MatchingException(MATCHING_FORBIDDEN_OPERATION); + } + } + } diff --git a/src/main/java/io/oeid/mogakgo/domain/matching/domain/entity/enums/MatchingStatus.java b/src/main/java/io/oeid/mogakgo/domain/matching/domain/entity/enums/MatchingStatus.java index 32aa7f4c..5283687f 100644 --- a/src/main/java/io/oeid/mogakgo/domain/matching/domain/entity/enums/MatchingStatus.java +++ b/src/main/java/io/oeid/mogakgo/domain/matching/domain/entity/enums/MatchingStatus.java @@ -1,7 +1,26 @@ package io.oeid.mogakgo.domain.matching.domain.entity.enums; +import static io.oeid.mogakgo.exception.code.ErrorCode400.MATCHING_CANCEL_NOT_ALLOWED; + +import io.oeid.mogakgo.domain.matching.exception.MatchingException; +import lombok.Getter; + +@Getter public enum MatchingStatus { - PROGRESS, - CANCELED, - FINISHED + PROGRESS("매칭 진행중"), + CANCELED("매칭 취소됨"), + FINISHED("매칭 종료됨") + ; + + private final String description; + + MatchingStatus(String description) { + this.description = description; + } + + public void validateAvailableCancel() { + if (this != PROGRESS) { + throw new MatchingException(MATCHING_CANCEL_NOT_ALLOWED); + } + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/matching/exception/MatchingException.java b/src/main/java/io/oeid/mogakgo/domain/matching/exception/MatchingException.java new file mode 100644 index 00000000..d8412856 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/matching/exception/MatchingException.java @@ -0,0 +1,11 @@ +package io.oeid.mogakgo.domain.matching.exception; + +import io.oeid.mogakgo.exception.code.ErrorCode; +import io.oeid.mogakgo.exception.exception_class.CustomException; + +public class MatchingException extends CustomException { + + public MatchingException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/matching/presentation/MatchingController.java b/src/main/java/io/oeid/mogakgo/domain/matching/presentation/MatchingController.java new file mode 100644 index 00000000..d82ba55a --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/matching/presentation/MatchingController.java @@ -0,0 +1,26 @@ +package io.oeid.mogakgo.domain.matching.presentation; + +import io.oeid.mogakgo.common.annotation.UserId; +import io.oeid.mogakgo.common.swagger.template.MatchingSwagger; +import io.oeid.mogakgo.domain.matching.application.MatchingService; +import io.oeid.mogakgo.domain.matching.presentation.dto.MatchingId; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/matches") +@RestController +public class MatchingController implements MatchingSwagger { + + private final MatchingService matchingService; + + @PostMapping("/{matchingId}/cancel") + public ResponseEntity cancel(@UserId Long userId, @PathVariable Long matchingId) { + return ResponseEntity.ok(new MatchingId(matchingService.cancel(userId, matchingId))); + } + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/matching/presentation/dto/res/MatchingId.java b/src/main/java/io/oeid/mogakgo/domain/matching/presentation/dto/MatchingId.java similarity index 83% rename from src/main/java/io/oeid/mogakgo/domain/matching/presentation/dto/res/MatchingId.java rename to src/main/java/io/oeid/mogakgo/domain/matching/presentation/dto/MatchingId.java index 9cea1e37..4653835e 100644 --- a/src/main/java/io/oeid/mogakgo/domain/matching/presentation/dto/res/MatchingId.java +++ b/src/main/java/io/oeid/mogakgo/domain/matching/presentation/dto/MatchingId.java @@ -1,4 +1,4 @@ -package io.oeid.mogakgo.domain.matching.presentation.dto.res; +package io.oeid.mogakgo.domain.matching.presentation.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/application/NotificationService.java b/src/main/java/io/oeid/mogakgo/domain/notification/application/NotificationService.java index 7f3a7338..6a863886 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/application/NotificationService.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/application/NotificationService.java @@ -1,9 +1,12 @@ package io.oeid.mogakgo.domain.notification.application; +import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; +import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.domain.notification.application.dto.req.NotificationCreateRequest; import io.oeid.mogakgo.domain.notification.application.dto.res.NotificationCreateResponse; import io.oeid.mogakgo.domain.notification.domain.Notification; import io.oeid.mogakgo.domain.notification.infrastructure.NotificationJpaRepository; +import io.oeid.mogakgo.domain.notification.presentation.dto.res.NotificationPublicApiRes; import io.oeid.mogakgo.domain.user.application.UserCommonService; import io.oeid.mogakgo.domain.user.domain.User; import lombok.RequiredArgsConstructor; @@ -23,8 +26,15 @@ public class NotificationService { @Transactional public NotificationCreateResponse createNotification(NotificationCreateRequest request) { log.info("createNotification request: {}", request); - User user = userCommonService.getUserById(request.getUserId()); - Notification notification = notificationRepository.save(request.toEntity(user)); + User sender = userCommonService.getUserById(request.getSenderId()); + User receiver = userCommonService.getUserById(request.getReceiverId()); + Notification notification = notificationRepository.save(request.toEntity(sender, receiver)); return NotificationCreateResponse.from(notification); } + + public CursorPaginationResult getNotifications(Long userId, + CursorPaginationInfoReq pageable) { + User user = userCommonService.getUserById(userId); + return notificationRepository.findByUserIdWithPagination(user.getId(), pageable); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/req/NotificationCreateRequest.java b/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/req/NotificationCreateRequest.java index 6992d4d2..63cf6be5 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/req/NotificationCreateRequest.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/req/NotificationCreateRequest.java @@ -11,16 +11,18 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class NotificationCreateRequest { - private final Long userId; + private final Long senderId; + private final Long receiverId; private final NotificationTag notificationTag; private final String detailData; - public static NotificationCreateRequest of(Long userId, NotificationTag notificationTag, + public static NotificationCreateRequest of(Long senderId, Long receiverId, + NotificationTag notificationTag, String detailData) { - return new NotificationCreateRequest(userId, notificationTag, detailData); + return new NotificationCreateRequest(senderId, receiverId, notificationTag, detailData); } - public Notification toEntity(User user) { - return Notification.of(user, notificationTag, detailData); + public Notification toEntity(User sender, User receiver) { + return Notification.of(sender, receiver, notificationTag, detailData); } } diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/res/NotificationCreateResponse.java b/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/res/NotificationCreateResponse.java index c5b597ae..82849c67 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/res/NotificationCreateResponse.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/res/NotificationCreateResponse.java @@ -12,7 +12,8 @@ public class NotificationCreateResponse { private final Long id; - private final Long userId; + private final Long senderId; + private final Long receiverId; private final NotificationTag notificationTag; private final String detailData; private final LocalDateTime createdAt; @@ -21,7 +22,8 @@ public class NotificationCreateResponse { public static NotificationCreateResponse from(Notification notification) { return new NotificationCreateResponse( notification.getId(), - notification.getUser().getId(), + notification.getSender().getId(), + notification.getReceiver().getId(), notification.getNotificationTag(), notification.getDetailData(), notification.getCreatedAt(), diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/domain/Notification.java b/src/main/java/io/oeid/mogakgo/domain/notification/domain/Notification.java index 5f39cd8c..10488682 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/domain/Notification.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/domain/Notification.java @@ -33,8 +33,12 @@ public class Notification { private Long id; @ManyToOne - @JoinColumn(name = "user_id") - private User user; + @JoinColumn(name = "sender_id") + private User sender; + + @ManyToOne + @JoinColumn(name = "receiver_id") + private User receiver; @Enumerated(EnumType.STRING) @Column(name = "tag") @@ -50,15 +54,18 @@ public class Notification { @Column(name = "created_at") private LocalDateTime createdAt; - private Notification(User user, NotificationTag notificationTag, String detailData) { - this.user = user; + private Notification(User sender, User receiver, NotificationTag notificationTag, + String detailData) { + this.sender = sender; + this.receiver = receiver; this.notificationTag = validateNotificationTag(notificationTag); this.detailData = validateDetailData(detailData); this.checkedYn = false; } - public static Notification of(User user, NotificationTag notificationTag, String detail) { - return new Notification(user, notificationTag, detail); + public static Notification of(User sender, User receiver, NotificationTag notificationTag, + String detail) { + return new Notification(sender, receiver, notificationTag, detail); } private NotificationTag validateNotificationTag(NotificationTag notificationTag) { diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationCustomRepository.java b/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationCustomRepository.java new file mode 100644 index 00000000..9d5d0271 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationCustomRepository.java @@ -0,0 +1,11 @@ +package io.oeid.mogakgo.domain.notification.infrastructure; + + +import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; +import io.oeid.mogakgo.common.base.CursorPaginationResult; +import io.oeid.mogakgo.domain.notification.presentation.dto.res.NotificationPublicApiRes; + +public interface NotificationCustomRepository { + + CursorPaginationResult findByUserIdWithPagination(Long userId, CursorPaginationInfoReq pageable); +} diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationCustomRepositoryImpl.java b/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationCustomRepositoryImpl.java new file mode 100644 index 00000000..b1ffb028 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationCustomRepositoryImpl.java @@ -0,0 +1,60 @@ +package io.oeid.mogakgo.domain.notification.infrastructure; + + +import static io.oeid.mogakgo.domain.notification.domain.QNotification.notification; +import static io.oeid.mogakgo.domain.user.domain.QUser.user; + +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; +import io.oeid.mogakgo.common.base.CursorPaginationResult; +import io.oeid.mogakgo.domain.notification.presentation.dto.res.NotificationPublicApiRes; +import io.oeid.mogakgo.domain.notification.presentation.vo.NotificationDataVo; +import io.oeid.mogakgo.domain.notification.presentation.vo.NotificationSenderVo; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class NotificationCustomRepositoryImpl implements NotificationCustomRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public CursorPaginationResult findByUserIdWithPagination(Long userId, + CursorPaginationInfoReq pageable) { + List result = jpaQueryFactory.select( + Projections.constructor( + NotificationPublicApiRes.class, + notification.id, + notification.notificationTag, + Projections.constructor( + NotificationSenderVo.class, + notification.sender.username, + notification.sender.id, + notification.sender.avatarUrl + ), + Projections.constructor( + NotificationDataVo.class, + notification.detailData, + notification.createdAt + ) + ) + ) + .from(notification) + .join(notification.sender, user) + .where( + cursorIdCondition(pageable.getCursorId()), + notification.receiver.id.eq(userId)) + .orderBy(notification.id.desc()) + .limit(pageable.getPageSize() + 1L) + .fetch(); + return CursorPaginationResult.fromDataWithExtraItemForNextCheck(result, + pageable.getPageSize()); + } + + private BooleanExpression cursorIdCondition(Long cursorId) { + return cursorId != null ? notification.id.lt(cursorId) : null; + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationJpaRepository.java b/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationJpaRepository.java index 99d885e9..261771f8 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationJpaRepository.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationJpaRepository.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository; @Repository -public interface NotificationJpaRepository extends JpaRepository { +public interface NotificationJpaRepository extends JpaRepository, + NotificationCustomRepository { } \ No newline at end of file diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/presentation/NotificationController.java b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/NotificationController.java index b98098b6..49fac334 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/presentation/NotificationController.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/NotificationController.java @@ -1,12 +1,18 @@ package io.oeid.mogakgo.domain.notification.presentation; import io.oeid.mogakgo.common.annotation.UserId; +import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; +import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.common.swagger.template.NotificationSwagger; import io.oeid.mogakgo.domain.notification.application.FCMNotificationService; +import io.oeid.mogakgo.domain.notification.application.NotificationService; import io.oeid.mogakgo.domain.notification.presentation.dto.req.FCMTokenApiRequest; +import io.oeid.mogakgo.domain.notification.presentation.dto.res.NotificationPublicApiRes; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -18,6 +24,7 @@ public class NotificationController implements NotificationSwagger { private final FCMNotificationService fcmNotificationService; + private final NotificationService notificationService; @PostMapping("/fcm") public ResponseEntity manageFCMToken(@UserId Long userId, @RequestBody @Valid @@ -26,5 +33,9 @@ public ResponseEntity manageFCMToken(@UserId Long userId, @RequestBody @Va return ResponseEntity.ok().build(); } - + @GetMapping + public ResponseEntity> getByUserId( + @UserId Long id, @Valid @ModelAttribute CursorPaginationInfoReq pageable) { + return ResponseEntity.ok().body(notificationService.getNotifications(id, pageable)); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/presentation/dto/res/NotificationPublicApiRes.java b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/dto/res/NotificationPublicApiRes.java new file mode 100644 index 00000000..4183e597 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/dto/res/NotificationPublicApiRes.java @@ -0,0 +1,22 @@ +package io.oeid.mogakgo.domain.notification.presentation.dto.res; + +import io.oeid.mogakgo.domain.notification.domain.enums.NotificationTag; +import io.oeid.mogakgo.domain.notification.presentation.vo.NotificationDataVo; +import io.oeid.mogakgo.domain.notification.presentation.vo.NotificationSenderVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Schema(description = "알림 API 응답") +@Getter +@AllArgsConstructor +public class NotificationPublicApiRes { + + @Schema(description = "알림 ID") + private Long id; + @Schema(description = "알림 형식") + private NotificationTag tag; + private NotificationSenderVo sender; + private NotificationDataVo data; + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/presentation/vo/NotificationDataVo.java b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/vo/NotificationDataVo.java new file mode 100644 index 00000000..29e3bb0a --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/vo/NotificationDataVo.java @@ -0,0 +1,16 @@ +package io.oeid.mogakgo.domain.notification.presentation.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Schema(description = "알림 데이터") +@Getter +@AllArgsConstructor +public class NotificationDataVo { + + private final String detail; + private final LocalDateTime createdAt; + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/presentation/vo/NotificationSenderVo.java b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/vo/NotificationSenderVo.java new file mode 100644 index 00000000..0c3fe247 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/vo/NotificationSenderVo.java @@ -0,0 +1,19 @@ +package io.oeid.mogakgo.domain.notification.presentation.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Schema(description = "알림 발신자 정보") +@Getter +@AllArgsConstructor(access = AccessLevel.PUBLIC) +public class NotificationSenderVo { + @Schema(description = "알림 전송자 이름", defaultValue = "mogakgo") + private String name; + @Schema(description = "알림 전송자 아이디", nullable = true) + private Long id; + @Schema(description = "알림 전송자 프로필 이미지 URL", nullable = true) + private String profileImageUrl; + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java b/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java index 47c0d643..b704bf84 100644 --- a/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java +++ b/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java @@ -29,12 +29,13 @@ public CursorPaginationResult getRandomOrderedProfileCard validateToken(userId); validateRegionCoverage(region); - CursorPaginationResult projects = profileCardRepository + CursorPaginationResult profiles = profileCardRepository .findByConditionWithPagination( null, region, pageable ); - Collections.shuffle(projects.getData()); - return projects; + + Collections.shuffle(profiles.getData()); + return profiles; } private User validateToken(Long userId) { diff --git a/src/main/java/io/oeid/mogakgo/domain/profile/domain/entity/ProfileCard.java b/src/main/java/io/oeid/mogakgo/domain/profile/domain/entity/ProfileCard.java index a987c3bd..69c3b569 100644 --- a/src/main/java/io/oeid/mogakgo/domain/profile/domain/entity/ProfileCard.java +++ b/src/main/java/io/oeid/mogakgo/domain/profile/domain/entity/ProfileCard.java @@ -25,7 +25,7 @@ public class ProfileCard { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "profile_card_id") + @Column(name = "id") private Long id; @OneToOne(cascade = CascadeType.ALL) diff --git a/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardRepositoryCustomImpl.java b/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardRepositoryCustomImpl.java index 2c51a4f8..1740e202 100644 --- a/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardRepositoryCustomImpl.java +++ b/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardRepositoryCustomImpl.java @@ -1,14 +1,16 @@ package io.oeid.mogakgo.domain.profile.infrastructure; import static io.oeid.mogakgo.domain.profile.domain.entity.QProfileCard.profileCard; -import static io.oeid.mogakgo.domain.project.domain.entity.QProject.project; +import static io.oeid.mogakgo.domain.user.domain.QUser.user; -import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.domain.geo.domain.enums.Region; +import io.oeid.mogakgo.domain.profile.domain.entity.ProfileCard; +import io.oeid.mogakgo.domain.user.domain.UserDevelopLanguageTag; +import io.oeid.mogakgo.domain.user.domain.UserWantedJobTag; import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; @@ -24,21 +26,9 @@ public class ProfileCardRepositoryCustomImpl implements ProfileCardRepositoryCus public CursorPaginationResult findByConditionWithPagination( Long userId, Region region, CursorPaginationInfoReq pageable ) { - List result = jpaQueryFactory.select( - Projections.constructor( - UserPublicApiResponse.class, - profileCard.user.id, - profileCard.user.username, - profileCard.user.githubId, - profileCard.user.avatarUrl, - profileCard.user.bio, - profileCard.user.jandiRate, - profileCard.user.achievement.title, - profileCard.user.userDevelopLanguageTags, - profileCard.user.userWantedJobTags - ) - ) - .from(profileCard) + List entities = jpaQueryFactory.selectFrom(profileCard) + .innerJoin(profileCard.user, user) + .on(profileCard.user.id.eq(user.id)) .where( cursorIdCondition(pageable.getCursorId()), userIdEq(userId), @@ -47,6 +37,23 @@ public CursorPaginationResult findByConditionWithPaginati .limit(pageable.getPageSize() + 1) .fetch(); + List result = entities.stream().map( + profileCard -> new UserPublicApiResponse( + profileCard.getUser().getId(), + profileCard.getUser().getUsername(), + profileCard.getUser().getGithubId(), + profileCard.getUser().getAvatarUrl(), + profileCard.getUser().getGithubUrl(), + profileCard.getUser().getBio(), + profileCard.getUser().getJandiRate(), + profileCard.getUser().getAchievement().getTitle(), + profileCard.getUser().getUserDevelopLanguageTags().stream().map( + UserDevelopLanguageTag::getDevelopLanguage).map(String::valueOf).toList(), + profileCard.getUser().getUserWantedJobTags().stream().map( + UserWantedJobTag::getWantedJob).map(String::valueOf).toList() + ) + ).toList(); + return CursorPaginationResult.fromDataWithExtraItemForNextCheck( result, pageable.getPageSize() ); @@ -61,6 +68,6 @@ private BooleanExpression userIdEq(Long userId) { } private BooleanExpression cursorIdCondition(Long cursorId) { - return cursorId != null ? project.id.gt(cursorId) : null; + return cursorId != null ? profileCard.id.gt(cursorId) : null; } } diff --git a/src/main/java/io/oeid/mogakgo/domain/project/application/ProjectService.java b/src/main/java/io/oeid/mogakgo/domain/project/application/ProjectService.java index 4787fd4e..4233ba2d 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project/application/ProjectService.java +++ b/src/main/java/io/oeid/mogakgo/domain/project/application/ProjectService.java @@ -13,21 +13,24 @@ import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.domain.geo.application.GeoService; import io.oeid.mogakgo.domain.geo.domain.enums.Region; -import io.oeid.mogakgo.domain.matching.application.UserMatchingService; import io.oeid.mogakgo.domain.geo.exception.GeoException; -import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; +import io.oeid.mogakgo.domain.matching.application.UserMatchingService; import io.oeid.mogakgo.domain.project.domain.entity.Project; import io.oeid.mogakgo.domain.project.domain.entity.enums.ProjectStatus; import io.oeid.mogakgo.domain.project.exception.ProjectException; import io.oeid.mogakgo.domain.project.infrastructure.ProjectJpaRepository; import io.oeid.mogakgo.domain.project.presentation.dto.req.ProjectCreateReq; +import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDensityRankRes; +import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; import io.oeid.mogakgo.domain.project_join_req.exception.ProjectJoinRequestException; import io.oeid.mogakgo.domain.project_join_req.infrastructure.ProjectJoinRequestJpaRepository; import io.oeid.mogakgo.domain.project_join_req.presentation.dto.res.projectJoinRequestRes; import io.oeid.mogakgo.domain.user.domain.User; import io.oeid.mogakgo.domain.user.exception.UserException; import io.oeid.mogakgo.domain.user.infrastructure.UserJpaRepository; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -37,6 +40,7 @@ @RequiredArgsConstructor @Service public class ProjectService { + private static final int DENSITY_RANK_LIMIT = 3; private final UserJpaRepository userJpaRepository; private final ProjectJpaRepository projectJpaRepository; @@ -147,6 +151,27 @@ public CursorPaginationResult getRandomOrderedProjectsByReg return projects; } + public ProjectDensityRankRes getDensityRankProjects() { + List regionRankList = projectJpaRepository.getDensityRankProjectsByRegion(DENSITY_RANK_LIMIT); + + // 필요한 경우 기본 지역 순위 목록으로 채움 + fillWithDefaultRegionsIfNecessary(regionRankList); + + return new ProjectDensityRankRes(regionRankList); + } + + private void fillWithDefaultRegionsIfNecessary(List regionRankList) { + if (regionRankList.size() < DENSITY_RANK_LIMIT) { + // 기본 지역 순위에서 이미 리스트에 있는 지역을 제외하고 남은 지역을 추가 + List defaultRegionsToAdd = Region.getDefaultDensityRank().stream() + .filter(region -> !regionRankList.contains(region)) + .limit(DENSITY_RANK_LIMIT - regionRankList.size()) + .toList(); + + regionRankList.addAll(defaultRegionsToAdd); + } + } + private User getUser(Long userId) { return userJpaRepository.findById(userId) .orElseThrow(() -> new UserException(USER_NOT_FOUND)); diff --git a/src/main/java/io/oeid/mogakgo/domain/project/domain/entity/Project.java b/src/main/java/io/oeid/mogakgo/domain/project/domain/entity/Project.java index 384c35db..51b0bfc2 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project/domain/entity/Project.java +++ b/src/main/java/io/oeid/mogakgo/domain/project/domain/entity/Project.java @@ -3,8 +3,8 @@ import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_PROJECT_TAG_COUNT; import static io.oeid.mogakgo.exception.code.ErrorCode403.PROJECT_FORBIDDEN_OPERATION; -import io.oeid.mogakgo.domain.project.domain.entity.enums.ProjectStatus; import io.oeid.mogakgo.common.base.BaseTimeEntity; +import io.oeid.mogakgo.domain.project.domain.entity.enums.ProjectStatus; import io.oeid.mogakgo.domain.project.domain.entity.vo.CreatorInfo; import io.oeid.mogakgo.domain.project.domain.entity.vo.MeetingInfo; import io.oeid.mogakgo.domain.project.exception.ProjectException; @@ -79,6 +79,13 @@ private Project( addProjectTagsWithValidation(projectTags); } + //TODO: 백그라운드에서 작업할때 에러처리 어떻게 할지 고민 + public void finish() { + projectStatus.validateAvailableFinish(); + this.projectStatus = ProjectStatus.FINISHED; + // TODO: 리뷰 전송 + } + public void match(User tokenUser) { validateCreator(tokenUser); projectStatus.validateAvailableMatched(); diff --git a/src/main/java/io/oeid/mogakgo/domain/project/domain/entity/enums/ProjectStatus.java b/src/main/java/io/oeid/mogakgo/domain/project/domain/entity/enums/ProjectStatus.java index f6cd4108..30ea3cb4 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project/domain/entity/enums/ProjectStatus.java +++ b/src/main/java/io/oeid/mogakgo/domain/project/domain/entity/enums/ProjectStatus.java @@ -1,11 +1,13 @@ package io.oeid.mogakgo.domain.project.domain.entity.enums; +import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_PROJECT_STATUS_TO_FINISH; import static io.oeid.mogakgo.exception.code.ErrorCode400.PROJECT_CANCEL_NOT_ALLOWED; import static io.oeid.mogakgo.exception.code.ErrorCode400.PROJECT_DELETION_NOT_ALLOWED; import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_PROJECT_STATUS_TO_ACCEPT; import io.oeid.mogakgo.domain.project.exception.ProjectException; import io.oeid.mogakgo.domain.project_join_req.exception.ProjectJoinRequestException; +import io.oeid.mogakgo.exception.code.ErrorCode; import lombok.Getter; @Getter @@ -38,4 +40,10 @@ public void validateAvailableMatched() { throw new ProjectJoinRequestException(INVALID_PROJECT_STATUS_TO_ACCEPT); } } + + public void validateAvailableFinish() { + if (this != MATCHED) { + throw new ProjectException(INVALID_PROJECT_STATUS_TO_FINISH); + } + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/project/infrastructure/ProjectRepositoryCustom.java b/src/main/java/io/oeid/mogakgo/domain/project/infrastructure/ProjectRepositoryCustom.java index 0d0cefdb..a3fa995b 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project/infrastructure/ProjectRepositoryCustom.java +++ b/src/main/java/io/oeid/mogakgo/domain/project/infrastructure/ProjectRepositoryCustom.java @@ -3,12 +3,15 @@ import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.domain.geo.domain.enums.Region; -import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; import io.oeid.mogakgo.domain.project.domain.entity.enums.ProjectStatus; +import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; +import java.util.List; public interface ProjectRepositoryCustom { CursorPaginationResult findByConditionWithPagination( Long userId, Region region, ProjectStatus projectStatus, CursorPaginationInfoReq pageable ); + + List getDensityRankProjectsByRegion(int limit); } diff --git a/src/main/java/io/oeid/mogakgo/domain/project/infrastructure/ProjectRepositoryCustomImpl.java b/src/main/java/io/oeid/mogakgo/domain/project/infrastructure/ProjectRepositoryCustomImpl.java index 20132a7e..1fe450d0 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project/infrastructure/ProjectRepositoryCustomImpl.java +++ b/src/main/java/io/oeid/mogakgo/domain/project/infrastructure/ProjectRepositoryCustomImpl.java @@ -1,17 +1,20 @@ package io.oeid.mogakgo.domain.project.infrastructure; import static io.oeid.mogakgo.domain.project.domain.entity.QProject.project; -import static io.oeid.mogakgo.domain.project.domain.entity.QProjectTag.projectTag; +import static io.oeid.mogakgo.domain.user.domain.QUser.user; -import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.domain.geo.domain.enums.Region; +import io.oeid.mogakgo.domain.project.domain.entity.Project; +import io.oeid.mogakgo.domain.project.domain.entity.ProjectTag; +import io.oeid.mogakgo.domain.project.domain.entity.enums.ProjectStatus; import io.oeid.mogakgo.domain.project.presentation.dto.res.MeetingInfoResponse; import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; -import io.oeid.mogakgo.domain.project.domain.entity.enums.ProjectStatus; +import io.oeid.mogakgo.domain.user.domain.UserDevelopLanguageTag; +import io.oeid.mogakgo.domain.user.domain.UserWantedJobTag; import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; @@ -27,32 +30,10 @@ public class ProjectRepositoryCustomImpl implements ProjectRepositoryCustom { public CursorPaginationResult findByConditionWithPagination( Long userId, Region region, ProjectStatus projectStatus, CursorPaginationInfoReq pageable ) { - List result = jpaQueryFactory.select( - Projections.constructor( - ProjectDetailAPIRes.class, - project.id, - Projections.constructor( - UserPublicApiResponse.class, - project.creator.username, - project.creator.githubId, - project.creator.avatarUrl, - project.creator.githubUrl, - project.creator.bio, - project.creator.jandiRate, - project.creator.userDevelopLanguageTags, - project.creator.userWantedJobTags - ), - projectTag.content, - Projections.constructor( - MeetingInfoResponse.class, - project.meetingInfo.meetStartTime, - project.meetingInfo.meetEndTime, - project.meetingInfo.meetDetail - ) - ) - ) - .from(project) - .innerJoin(projectTag).on(project.id.eq(projectTag.project.id)) + List entities = jpaQueryFactory.selectFrom(project) + .innerJoin(project.creator, user) + .on(project.creator.id.eq(user.id)) + .join(project.projectTags).fetchJoin() .where( cursorIdCondition(pageable.getCursorId()), userIdEq(userId), @@ -62,11 +43,48 @@ public CursorPaginationResult findByConditionWithPagination .limit(pageable.getPageSize() + 1) .fetch(); + List result = entities.stream().map( + project -> new ProjectDetailAPIRes( + project.getId(), + new UserPublicApiResponse( + project.getCreator().getId(), + project.getCreator().getUsername(), + project.getCreator().getGithubId(), + project.getCreator().getAvatarUrl(), + project.getCreator().getGithubUrl(), + project.getCreator().getBio(), + project.getCreator().getJandiRate(), + project.getCreator().getAchievement().getTitle(), + project.getCreator().getUserDevelopLanguageTags().stream().map( + UserDevelopLanguageTag::getDevelopLanguage).map(String::valueOf).toList(), + project.getCreator().getUserWantedJobTags().stream().map( + UserWantedJobTag::getWantedJob).map(String::valueOf).toList() + ), + project.getProjectTags().stream().map(ProjectTag::getContent).toList(), + new MeetingInfoResponse( + project.getMeetingInfo().getMeetStartTime(), + project.getMeetingInfo().getMeetEndTime(), + project.getMeetingInfo().getMeetDetail() + ) + ) + ).toList(); + return CursorPaginationResult.fromDataWithExtraItemForNextCheck( result, pageable.getPageSize() ); } + @Override + public List getDensityRankProjectsByRegion(int limit) { + return jpaQueryFactory.select(project.creatorInfo.region) + .from(project) + .groupBy(project.creatorInfo.region) + .having(project.creatorInfo.region.count().gt(0L)) + .orderBy(project.creatorInfo.region.count().desc()) + .limit(limit) + .fetch(); + } + private BooleanExpression cursorIdCondition(Long cursorId) { return cursorId != null ? project.id.gt(cursorId) : null; } diff --git a/src/main/java/io/oeid/mogakgo/domain/project/presentation/ProjectController.java b/src/main/java/io/oeid/mogakgo/domain/project/presentation/ProjectController.java index 906956be..8b66ceed 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project/presentation/ProjectController.java +++ b/src/main/java/io/oeid/mogakgo/domain/project/presentation/ProjectController.java @@ -6,8 +6,9 @@ import io.oeid.mogakgo.common.swagger.template.ProjectSwagger; import io.oeid.mogakgo.domain.geo.domain.enums.Region; import io.oeid.mogakgo.domain.project.application.ProjectService; -import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; import io.oeid.mogakgo.domain.project.presentation.dto.req.ProjectCreateReq; +import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDensityRankRes; +import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectIdRes; import io.oeid.mogakgo.domain.project_join_req.presentation.dto.res.projectJoinRequestRes; import jakarta.validation.Valid; @@ -72,4 +73,9 @@ public ResponseEntity> getRandomOrde ); } + @GetMapping("density/rank") + public ResponseEntity getDensityRankProjects() { + return ResponseEntity.ok().body(projectService.getDensityRankProjects()); + } + } diff --git a/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/ProjectDensityRankRes.java b/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/ProjectDensityRankRes.java new file mode 100644 index 00000000..0f7dc8e5 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/ProjectDensityRankRes.java @@ -0,0 +1,19 @@ +package io.oeid.mogakgo.domain.project.presentation.dto.res; + +import io.oeid.mogakgo.domain.geo.domain.enums.Region; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Getter; + +@Schema(description = "지역별 프로젝트 밀도 순위") +@Getter +public class ProjectDensityRankRes { + + @Schema(description = "지역별 프로젝트 밀도 순위. 밀도가 높은 순서대로 정렬됨. 인덱스 0부터 1위로 시작.", + example = "[\"JONGRO\", \"JUNG\", \"YONGSAN\"]") + private List densityRankByRegion; + + public ProjectDensityRankRes(List densityRankByRegion) { + this.densityRankByRegion = densityRankByRegion; + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/ProjectJoinRequestController.java b/src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/ProjectJoinRequestController.java index ba6fa403..7d9ce1eb 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/ProjectJoinRequestController.java +++ b/src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/ProjectJoinRequestController.java @@ -4,7 +4,7 @@ import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.common.swagger.template.ProjectJoinReqSwagger; -import io.oeid.mogakgo.domain.matching.presentation.dto.res.MatchingId; +import io.oeid.mogakgo.domain.matching.presentation.dto.MatchingId; import io.oeid.mogakgo.domain.project_join_req.application.ProjectJoinRequestService; import io.oeid.mogakgo.domain.project_join_req.application.dto.req.ProjectJoinCreateReq; import io.oeid.mogakgo.domain.project_join_req.presentation.dto.res.ProjectJoinRequestAPIRes; diff --git a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java index 2fae3cea..a909c4ac 100644 --- a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java +++ b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java @@ -22,6 +22,7 @@ public enum ErrorCode400 implements ErrorCode { PROJECT_CANCEL_NOT_ALLOWED("E030107", "이미 취소 되었거나 종료된 프로젝트는 취소할 수 없습니다."), INVALID_MATCHING_USER_TO_CREATE_PROJECT("E030108", "매칭이 진행 중인 유저는 프로젝트 생성을 할 수 없습니다."), ALREADY_EXIST_PROGRESS_PROJECT("E030109", "종료되지 않은 프로젝트 카드가 있으면 프로젝트 생성을 할 수 없습니다."), + INVALID_PROJECT_STATUS_TO_FINISH("E030110", "매칭이 진행 중인 프로젝트가 아니여서 프로젝트를 종료할 수 없습니다."), INVALID_SERVICE_REGION("E080101", "해당 지역은 서비스 지역이 아닙니다."), USER_DEVELOP_LANGUAGE_BAD_REQUEST("E020101", "개발 언어는 3개까지만 등록 가능합니다."), @@ -40,6 +41,8 @@ public enum ErrorCode400 implements ErrorCode { INVALID_PROJECT_JOIN_REQUEST_REGION("E090102", "동네 인증한 구역에서만 프로젝트 매칭 요청을 생성할 수 있습니다."), PROJECT_JOIN_REQUEST_SHOULD_BE_ONLY_ONE("E090103", "프로젝트 매칭 요청은 한 번에 한 개만 전송할 수 있습니다."), + MATCHING_CANCEL_NOT_ALLOWED("E090101", "이미 종료되거나 취소된 매칭은 취소할 수 없습니다."), + ; private final HttpStatus httpStatus = HttpStatus.BAD_REQUEST; diff --git a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java index 50c541f1..cf4faf14 100644 --- a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java +++ b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java @@ -9,6 +9,7 @@ public enum ErrorCode403 implements ErrorCode { PROJECT_FORBIDDEN_OPERATION("E030201", "해당 프로젝트에 대한 권한이 없습니다."), PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION("E050201", "해당 프로젝트 요청에 대한 권한이 없습니다."), INVALID_CERT_INFORMATION("E070201", "동네 인증을 수행할 권한이 없습니다."), + MATCHING_FORBIDDEN_OPERATION("E090201", "해당 매칭에 대한 권한이 없습니다."), ; private final HttpStatus httpStatus = HttpStatus.FORBIDDEN; diff --git a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode404.java b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode404.java index ddc70c89..354e25b5 100644 --- a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode404.java +++ b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode404.java @@ -9,6 +9,7 @@ public enum ErrorCode404 implements ErrorCode { PROJECT_NOT_FOUND("E030301", "해당 프로젝트가 존재하지 않습니다."), NOTIFICATION_FCM_TOKEN_NOT_FOUND("E060301", "해당 유저의 FCM 토큰이 존재하지 않습니다."), PROJECT_JOIN_REQUEST_NOT_FOUND("E050301", "해당 프로젝트 참여 요청이 존재하지 않습니다."), + MATCHING_NOT_FOUND("E090301", "해당 매칭이 존재하지 않습니다."), ; private final HttpStatus httpStatus = HttpStatus.NOT_FOUND;