From 6911ccde1e84054eed47928d99627e3cc06ac42b Mon Sep 17 00:00:00 2001 From: JIN-076 <57834671+JIN-076@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:18:57 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EC=84=A0=ED=83=9D=ED=95=9C=20?= =?UTF-8?q?=EA=B5=AC=EC=97=AD=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8,=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=9E=9C?= =?UTF-8?q?=EB=8D=A4=20=EC=A1=B0=ED=9A=8C=20API,=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EB=A7=A4=EC=B9=AD=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20=EA=B0=9C=EB=B0=9C=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 선택한 구역의 '대기중'인 프로젝트 카드 리스트 랜덤 조회 메서드 추가, 커서 기반 페이지네이션 공통 포맷 적용 * [FEAT] 프로젝트 상세 카드를 조회하는 동적 쿼리 조회 메서드에 커서 기반 페이지네이션 공통 포맷 적용 * [FEAT] Region에 대한 프로젝트 카드 리스트 조회 Controller 구현 * [FEAT] 프로젝트 카드 랜덤 리스트 조회 API에 대해 Swagger 추가 * [FEAT] 프로필 카드 리스트 랜덤 조회 API 구현 * [FEAT] 프로필 카드 리스트 랜덤 조회 비즈니스 로직 구현, 커서 기반 페이지네이션 공통 포맷 적용 * [FEAT] 프로필 카드 리스트 랜덤 조회 API를 Swagger에 추가 * [FEAT] 프로젝트 카드 상세 페이지 응답 DTO 구현 * [FEAT] 프로필 카드 동적 쿼리 조회 메서드 구현, 커서 기반 페이지네이션 및 공통 포맷 적용 * [REFACT] 선택한 구역에 대한 프로필 카드 리스트 조회 API 응답 변경 * [REFACT] 프로필 카드 리스트 조회 API 응답 타입 Swagger에서 변경 * [REFACT] 프로젝트 카드의 상세 정보를 위해 사용자 정보와 프로젝트 만남 정보에 대한 응답 수정 * [REFACT] 프로필 카드의 상세 정보를 위해 사용자 정보에 대한 응답 수정 * [REFACT] queryDsl에서 Projections.constructor 사용을 위해 public 생성자 생성, projectTag와의 조인 로직 추가 * [FEAT] 프로젝트 매칭 엔티티에 private 생성자, 정적 팩토리 메서드 추가 구현 * [FEAT] 프로젝트 매칭 생성 요청 DTO 구현 * [FEAT] 프로젝트 매칭 중복 생성 검증 메서드 추가 구현 * [FEAT] 프로젝트 매칭 요청 관련 에러코드 작성 * [FEAT] 프로젝트 매칭 요청 생성 메서드 구현, 프로젝트 매칭 요청 관련 유효성 검사 메서드 구현 * [FEAT] 프로젝트 매칭 요청 생성 API 작성, API 관련 Swagger 작성, 불필요한 에러코드 제거, 에러코드 관련 오류메시지 샘플 작성 * [FEAT] 프로젝트 매칭 생성 API 응답 DTO 생성 * [FIX] 패키지명 오타 수정 * [FIX] 에러코드, 오타 관련 수정 * [FIX] 에러코드, 오타 관련 수정 * [REFACT] 에러코드 수정 (403 -> 400) * [FEAT] pageable 수정 * [REFACT] Profile Card 랜덤 조회 리스트 API 리팩토링 * [REFACT] 읽기전용 @Transactional 추가 * [REFACT] 메서드명 변경, 중복 매칭 요청 로직 수정, 네이밍 수정 * [FEAT] 불필요한 import문 제거 * [REFACT] 어노테이션 수정 * [REFACT] ProjectJoinRequest 엔티티의 Controller 엔드포인트 수정 * [REFACT] 엔터 제거 --- .../swagger/template/ProfileCardSwagger.java | 51 ++++++++++++ .../template/ProjectJoinRequestSwagger.java | 56 +++++++++++++ .../swagger/template/ProjectSwagger.java | 30 +++++++ ...waggerProjectJoinRequestErrorExamples.java | 11 +++ .../domain/cert/application/CertService.java | 4 +- .../application/ProfileCardService.java | 50 ++++++++++++ .../ProfileCardJpaRepository.java | 3 +- .../ProfileCardRepositoryCustom.java | 10 +-- .../ProfileCardRepositoryCustomImpl.java | 49 +++++++----- .../presentation/ProfileCardController.java | 34 ++++++++ .../project/application/ProjectService.java | 30 +++++++ .../ProjectRepositoryCustom.java | 10 +-- .../ProjectRepositoryCustomImpl.java | 66 ++++++++++------ .../presentation/ProjectController.java | 12 +++ .../dto/res/MeetingInfoResponse.java | 35 +++++++++ .../dto/res/ProjectDetailAPIRes.java | 43 ++++++++++ .../ProjectJoinRequestService.java | 78 +++++++++++++++++++ .../dto/req/ProjectJoinCreateReq.java | 36 +++++++++ .../domain/entity/ProjectJoinRequest.java | 12 +++ .../ProjectJoinRequestJpaRepository.java | 4 + .../ProjectJoinRequestRepositoryCustom.java | 1 - .../ProjectJoinRequestController.java | 30 +++++++ .../dto/res/ProjectJoinRequestAPIRes.java | 21 +++++ .../dto/res/UserPublicApiResponse.java | 3 +- .../mogakgo/exception/code/ErrorCode400.java | 2 + .../mogakgo/exception/code/ErrorCode403.java | 2 +- 26 files changed, 624 insertions(+), 59 deletions(-) create mode 100644 src/main/java/io/oeid/mogakgo/common/swagger/template/ProfileCardSwagger.java create mode 100644 src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectJoinRequestSwagger.java create mode 100644 src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerProjectJoinRequestErrorExamples.java create mode 100644 src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java create mode 100644 src/main/java/io/oeid/mogakgo/domain/profile/presentation/ProfileCardController.java create mode 100644 src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/MeetingInfoResponse.java create mode 100644 src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/ProjectDetailAPIRes.java create mode 100644 src/main/java/io/oeid/mogakgo/domain/project_join_req/application/ProjectJoinRequestService.java create mode 100644 src/main/java/io/oeid/mogakgo/domain/project_join_req/application/dto/req/ProjectJoinCreateReq.java create mode 100644 src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/ProjectJoinRequestController.java create mode 100644 src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/dto/res/ProjectJoinRequestAPIRes.java diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/ProfileCardSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/ProfileCardSwagger.java new file mode 100644 index 00000000..48bfdecf --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/ProfileCardSwagger.java @@ -0,0 +1,51 @@ +package io.oeid.mogakgo.common.swagger.template; + +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.SwaggerUserErrorExamples; +import io.oeid.mogakgo.domain.geo.domain.enums.Region; +import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; +import io.oeid.mogakgo.exception.dto.ErrorResponse; +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.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +@Tag(name = "Profile Card", description = "프로필 카드 관련 API") +public interface ProfileCardSwagger { + + @Operation(summary = "선택한 서비스 지역에 대한 랜덤 순서의 프로필 리스트 조회", description = "사용자가 서비스 지역의 프로필 리스트를 조회할 때 사용하는 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "프로필 리스트 조회 성공"), + @ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject(name = "E080101", value = SwaggerGeoErrorExamples.INVALID_SERVICE_REGION) + )), + @ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND) + )), + }) + @Parameters({ + @Parameter(name = "cursorId", description = "기준이 되는 커서 ID", example = "1"), + @Parameter(name = "pageSize", description = "요청할 데이터 크기", example = "5", required = true), + @Parameter(name = "sortOrder", description = "정렬 방향", example = "ASC"), + }) + ResponseEntity> getRandomOrderedProfileCardsByRegion( + @Parameter(hidden = true) Long userId, + @Parameter(description = "조회하려는 서비스 지역", required = true) Region region, + @Parameter(hidden = true) CursorPaginationInfoReq pageable + ); + +} diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectJoinRequestSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectJoinRequestSwagger.java new file mode 100644 index 00000000..85bceeb4 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/ProjectJoinRequestSwagger.java @@ -0,0 +1,56 @@ +package io.oeid.mogakgo.common.swagger.template; + +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectErrorExamples; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectJoinRequestErrorExamples; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples; +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.exception.dto.ErrorResponse; +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.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +@Tag(name = "Project Join Request", description = "프로젝트 매칭 요청 관련 API") +public interface ProjectJoinRequestSwagger { + + @Operation(summary = "프로젝트 매칭 요청 생성", description = "회원이 프로젝트 매칭 요청을 생성할 때 사용하는 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "프로젝트 매칭 요청 생성 성공", + content = @Content(schema = @Schema(implementation = ProjectJoinRequestAPIRes.class))), + @ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "E090101", + value = SwaggerProjectJoinRequestErrorExamples.PROJECT_JOIN_REQUEST_ALREADY_EXIST), + @ExampleObject(name = "E090102", + value = SwaggerProjectJoinRequestErrorExamples.PROJECT_JOIN_REQUEST_INVALID_REGION) + })), + @ApiResponse(responseCode = "403", description = "프로젝트 매칭 요청 권한이 없음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject(name = "E050201", + value = SwaggerProjectJoinRequestErrorExamples.PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION))), + @ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND), + @ExampleObject(name = "E030301", value = SwaggerProjectErrorExamples.PROJECT_NOT_FOUND) + })), + }) + ResponseEntity create( + @Parameter(hidden = true) Long userId, + ProjectJoinCreateReq request + ); + +} 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 47884af6..cb86b11b 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 @@ -2,8 +2,11 @@ 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.ProjectDetailAPIRes; import io.oeid.mogakgo.domain.project.presentation.dto.req.ProjectCreateReq; import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectIdRes; import io.oeid.mogakgo.domain.project_join_req.presentation.projectJoinRequestRes; @@ -112,6 +115,33 @@ ResponseEntity cancel( @Parameter(description = "프로젝트 ID", required = true) Long id ); + @Operation(summary = "선택한 서비스 지역에 대한 랜덤 순서의 프로젝트 리스트 조회", description = "사용자가 서비스 지역의 프로젝트 리스트를 조회할 때 사용하는 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "프로젝트 리스트 조회 성공"), + @ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject(name = "E080101", value = SwaggerGeoErrorExamples.INVALID_SERVICE_REGION) + )), + @ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND) + )), + }) + @Parameters({ + @Parameter(name = "cursorId", description = "기준이 되는 커서 ID", example = "1"), + @Parameter(name = "pageSize", description = "요청할 데이터 크기", example = "5", required = true), + @Parameter(name = "sortOrder", description = "정렬 방향", example = "ASC"), + }) + ResponseEntity> getRandomOrderedProjectsByRegion( + @Parameter(hidden = true) Long userId, + @Parameter(description = "조회하려는 서비스 지역", required = true) Region region, + @Parameter(hidden = true) CursorPaginationInfoReq pageable + ); + @Operation(summary = "프로젝트 카드 참가 요청 조회", description = "회원이 프로젝트 카드의 참가 요청을 조회할 때 사용하는 API") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "프로젝트 카드 참가 요청 조회 성공"), diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerProjectJoinRequestErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerProjectJoinRequestErrorExamples.java new file mode 100644 index 00000000..792f5671 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerProjectJoinRequestErrorExamples.java @@ -0,0 +1,11 @@ +package io.oeid.mogakgo.core.properties.swagger.error; + +public class SwaggerProjectJoinRequestErrorExamples { + + public static final String PROJECT_JOIN_REQUEST_ALREADY_EXIST = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E050201\",\"message\":\"이미 매칭 요청을 보낸 프로젝트에 매칭 요청을 보낼 수 없습니다.\"}"; + public static final String PROJECT_JOIN_REQUEST_INVALID_REGION = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E090102\",\"message\":\"프로젝트 매칭 서비스는 동네 인증되지 않은 구역에서 진행할 수 없습니다.\"}"; + public static final String PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":403,\"code\":\"E090101\",\"message\":\"프로젝트 생성자는 해당 프로젝트에 매칭 요청을 보낼 수 없습니다.\"}"; + private SwaggerProjectJoinRequestErrorExamples() { + } + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/cert/application/CertService.java b/src/main/java/io/oeid/mogakgo/domain/cert/application/CertService.java index 2f21f0ed..1b731075 100644 --- a/src/main/java/io/oeid/mogakgo/domain/cert/application/CertService.java +++ b/src/main/java/io/oeid/mogakgo/domain/cert/application/CertService.java @@ -1,7 +1,7 @@ package io.oeid.mogakgo.domain.cert.application; import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_SERVICE_REGION; -import static io.oeid.mogakgo.exception.code.ErrorCode403.CERT_INVALID_INFORMATION; +import static io.oeid.mogakgo.exception.code.ErrorCode403.INVALID_CERT_INFORMATION; import io.oeid.mogakgo.domain.cert.exception.CertException; import io.oeid.mogakgo.domain.geo.domain.enums.Region; @@ -41,7 +41,7 @@ private boolean isPossibleCertification(Long userId, Region region) { private void validateCertificator(User tokenUser, Long userId) { if (!tokenUser.getId().equals(userId)) { - throw new CertException(CERT_INVALID_INFORMATION); + throw new CertException(INVALID_CERT_INFORMATION); } } 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 new file mode 100644 index 00000000..47c0d643 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java @@ -0,0 +1,50 @@ +package io.oeid.mogakgo.domain.profile.application; + +import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_SERVICE_REGION; + +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.geo.exception.GeoException; +import io.oeid.mogakgo.domain.profile.infrastructure.ProfileCardJpaRepository; +import io.oeid.mogakgo.domain.user.application.UserCommonService; +import io.oeid.mogakgo.domain.user.domain.User; +import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; +import java.util.Collections; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ProfileCardService { + + private final ProfileCardJpaRepository profileCardRepository; + private final UserCommonService userCommonService; + + public CursorPaginationResult getRandomOrderedProfileCardsByRegion( + Long userId, Region region, CursorPaginationInfoReq pageable + ) { + validateToken(userId); + validateRegionCoverage(region); + + CursorPaginationResult projects = profileCardRepository + .findByConditionWithPagination( + null, region, pageable + ); + Collections.shuffle(projects.getData()); + return projects; + } + + private User validateToken(Long userId) { + return userCommonService.getUserById(userId); + } + + private void validateRegionCoverage(Region region) { + if (Region.getByAreaCode(region.getAreaCode()) == null) { + throw new GeoException(INVALID_SERVICE_REGION); + } + } + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardJpaRepository.java b/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardJpaRepository.java index bb45a235..def1a7e7 100644 --- a/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardJpaRepository.java +++ b/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardJpaRepository.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository; @Repository -public interface ProfileCardJpaRepository extends JpaRepository, ProfileCardRepositoryCustom { +public interface ProfileCardJpaRepository extends JpaRepository, + ProfileCardRepositoryCustom { } diff --git a/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardRepositoryCustom.java b/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardRepositoryCustom.java index fdb6ba43..d763682a 100644 --- a/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardRepositoryCustom.java +++ b/src/main/java/io/oeid/mogakgo/domain/profile/infrastructure/ProfileCardRepositoryCustom.java @@ -1,13 +1,13 @@ package io.oeid.mogakgo.domain.profile.infrastructure; +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 org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; +import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; public interface ProfileCardRepositoryCustom { - Slice findByCondition( - Long cursorId, Long userId, Region region, Pageable pageable + CursorPaginationResult findByConditionWithPagination( + Long userId, Region region, CursorPaginationInfoReq pageable ); } 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 31bad7b1..2c51a4f8 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,16 +1,17 @@ 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 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.presentation.dto.res.UserPublicApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; @Repository @@ -20,19 +21,35 @@ public class ProfileCardRepositoryCustomImpl implements ProfileCardRepositoryCus private final JPAQueryFactory jpaQueryFactory; @Override - public Slice findByCondition( - Long cursorId, Long userId, Region region, Pageable pageable + public CursorPaginationResult findByConditionWithPagination( + Long userId, Region region, CursorPaginationInfoReq pageable ) { - List result = jpaQueryFactory.selectFrom(profileCard) + 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) .where( - cursorIdEq(cursorId), + cursorIdCondition(pageable.getCursorId()), userIdEq(userId), regionEq(region) ) .limit(pageable.getPageSize() + 1) .fetch(); - boolean hasNext = checkLastPage(result, pageable); - return new SliceImpl<>(result, pageable, hasNext); + + return CursorPaginationResult.fromDataWithExtraItemForNextCheck( + result, pageable.getPageSize() + ); } private BooleanExpression regionEq(Region region) { @@ -43,15 +60,7 @@ private BooleanExpression userIdEq(Long userId) { return userId != null ? profileCard.user.id.eq(userId) : null; } - private BooleanExpression cursorIdEq(Long cursorId) { - return cursorId != null ? profileCard.id.gt(cursorId) : null; - } - - private boolean checkLastPage(List profileCards, Pageable pageable) { - if (profileCards.size() > pageable.getPageSize()) { - profileCards.remove(pageable.getPageSize()); - return true; - } - return false; + private BooleanExpression cursorIdCondition(Long cursorId) { + return cursorId != null ? project.id.gt(cursorId) : null; } } diff --git a/src/main/java/io/oeid/mogakgo/domain/profile/presentation/ProfileCardController.java b/src/main/java/io/oeid/mogakgo/domain/profile/presentation/ProfileCardController.java new file mode 100644 index 00000000..f410d331 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/profile/presentation/ProfileCardController.java @@ -0,0 +1,34 @@ +package io.oeid.mogakgo.domain.profile.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.ProfileCardSwagger; +import io.oeid.mogakgo.domain.geo.domain.enums.Region; +import io.oeid.mogakgo.domain.profile.application.ProfileCardService; +import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; +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.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/profiles") +@RequiredArgsConstructor +public class ProfileCardController implements ProfileCardSwagger { + + private final ProfileCardService profileCardService; + + @GetMapping("/{region}") + public ResponseEntity> getRandomOrderedProfileCardsByRegion( + @UserId Long userId, @PathVariable Region region, + @Valid @ModelAttribute CursorPaginationInfoReq pageable + ) { + return ResponseEntity.ok().body( + profileCardService.getRandomOrderedProfileCardsByRegion(userId, region, pageable)); + } +} 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 2c9d3034..370732c9 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 @@ -1,5 +1,6 @@ package io.oeid.mogakgo.domain.project.application; +import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_SERVICE_REGION; import static io.oeid.mogakgo.exception.code.ErrorCode400.NOT_MATCH_MEET_LOCATION; import static io.oeid.mogakgo.exception.code.ErrorCode403.PROJECT_FORBIDDEN_OPERATION; import static io.oeid.mogakgo.exception.code.ErrorCode403.PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION; @@ -10,7 +11,10 @@ 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.geo.exception.GeoException; +import io.oeid.mogakgo.domain.project.presentation.dto.res.ProjectDetailAPIRes; 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; @@ -21,6 +25,7 @@ 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.Collections; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -102,6 +107,26 @@ public CursorPaginationResult getJoinRequest( null, projectId, null, pageable); } + // 선택한 구역에 대한 프로젝트 카드 리스트 랜덤 조회 + public CursorPaginationResult getRandomOrderedProjectsByRegion( + Long userId, Region region, CursorPaginationInfoReq pageable + ) { + getUser(userId); + + // 선택한 구역의 서비스 지역 여부 체크 + validateRegionCoverage(region); + + // 선택한 구역에 대해 Pending 상태인 프로젝트 리스트를 조회할 수 있음 + CursorPaginationResult projects = projectJpaRepository + .findByConditionWithPagination( + null, region, ProjectStatus.PENDING, pageable + ); + + // 요청할 때마다 랜덤 정렬 + Collections.shuffle(projects.getData()); + return projects; + } + private User getUser(Long userId) { return userJpaRepository.findById(userId) .orElseThrow(() -> new UserException(USER_NOT_FOUND)); @@ -128,4 +153,9 @@ private void validateProjectCardCreator( } } + private void validateRegionCoverage(Region region) { + if (Region.getByAreaCode(region.getAreaCode()) == null) { + throw new GeoException(INVALID_SERVICE_REGION); + } + } } 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 205cfc0e..0d0cefdb 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 @@ -1,14 +1,14 @@ package io.oeid.mogakgo.domain.project.infrastructure; +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.presentation.dto.res.ProjectDetailAPIRes; import io.oeid.mogakgo.domain.project.domain.entity.enums.ProjectStatus; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; public interface ProjectRepositoryCustom { - Slice findByCondition( - Long cursorId, Long userId, Region region, ProjectStatus projectStatus, Pageable pageable + CursorPaginationResult findByConditionWithPagination( + Long userId, Region region, ProjectStatus projectStatus, CursorPaginationInfoReq pageable ); } 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 d33a7b93..20132a7e 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 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.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 org.springframework.data.domain.Pageable; +import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; @Repository @@ -21,20 +24,51 @@ public class ProjectRepositoryCustomImpl implements ProjectRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; @Override - public Slice findByCondition( - Long cursorId, Long userId, Region region, ProjectStatus projectStatus, Pageable pageable + public CursorPaginationResult findByConditionWithPagination( + Long userId, Region region, ProjectStatus projectStatus, CursorPaginationInfoReq pageable ) { - List result = jpaQueryFactory.selectFrom(project) + 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)) .where( - cursorIdEq(cursorId), + cursorIdCondition(pageable.getCursorId()), userIdEq(userId), regionEq(region), projectStatusEq(projectStatus) ) .limit(pageable.getPageSize() + 1) .fetch(); - boolean hasNext = checkLastPage(result, pageable); - return new SliceImpl<>(result, pageable, hasNext); + + return CursorPaginationResult.fromDataWithExtraItemForNextCheck( + result, pageable.getPageSize() + ); + } + + private BooleanExpression cursorIdCondition(Long cursorId) { + return cursorId != null ? project.id.gt(cursorId) : null; } private BooleanExpression userIdEq(Long userId) { @@ -48,16 +82,4 @@ private BooleanExpression regionEq(Region region) { private BooleanExpression projectStatusEq(ProjectStatus projectStatus) { return projectStatus != null ? project.projectStatus.eq(projectStatus) : null; } - - private BooleanExpression cursorIdEq(Long cursorId) { - return cursorId != null ? project.id.gt(cursorId) : null; - } - - private boolean checkLastPage(List projects, Pageable pageable) { - if (projects.size() > pageable.getPageSize()) { - projects.remove(pageable.getPageSize()); - return true; - } - return false; - } } 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 e738ef22..acb7e007 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 @@ -4,7 +4,9 @@ import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; import io.oeid.mogakgo.common.base.CursorPaginationResult; 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.ProjectIdRes; import io.oeid.mogakgo.domain.project_join_req.presentation.projectJoinRequestRes; @@ -60,4 +62,14 @@ public ResponseEntity> getJoinRequ return ResponseEntity.ok().body(projectService.getJoinRequest(userId, id, pageable)); } + @GetMapping("/{region}") + public ResponseEntity> getRandomOrderedProjectsByRegion( + @UserId Long userId, @PathVariable Region region, + @Valid @ModelAttribute CursorPaginationInfoReq pageable + ) { + return ResponseEntity.ok().body( + projectService.getRandomOrderedProjectsByRegion(userId, region, pageable) + ); + } + } diff --git a/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/MeetingInfoResponse.java b/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/MeetingInfoResponse.java new file mode 100644 index 00000000..98aa17b3 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/MeetingInfoResponse.java @@ -0,0 +1,35 @@ +package io.oeid.mogakgo.domain.project.presentation.dto.res; + +import io.oeid.mogakgo.domain.project.domain.entity.vo.MeetingInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.Getter; + +@Schema(description = "프로젝트 만남 정보 응답 DTO") +@Getter +public class MeetingInfoResponse { + + @Schema(description = "프로젝트 만남 시작 시간", example = "2024-02-19T13:00:00") + private final LocalDateTime meetStartTime; + + @Schema(description = "프로젝트 만남 종료 시간", example = "2024-02-29T15:00:00") + private final LocalDateTime meetEndTime; + + @Schema(description = "프로젝트 만남 장소", example = "맥심플랜트 이태원점") + private final String meetDetail; + + public MeetingInfoResponse(LocalDateTime meetStartTime, LocalDateTime meetEndTime, String meetDetail) { + this.meetStartTime = meetStartTime; + this.meetEndTime = meetEndTime; + this.meetDetail = meetDetail; + } + + public static MeetingInfoResponse of(MeetingInfo meetingInfo) { + return new MeetingInfoResponse( + meetingInfo.getMeetStartTime(), + meetingInfo.getMeetEndTime(), + meetingInfo.getMeetDetail() + ); + } + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/ProjectDetailAPIRes.java b/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/ProjectDetailAPIRes.java new file mode 100644 index 00000000..68543b04 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/project/presentation/dto/res/ProjectDetailAPIRes.java @@ -0,0 +1,43 @@ +package io.oeid.mogakgo.domain.project.presentation.dto.res; + +import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Getter; + +@Schema(description = "프로젝트 카드 조회 리스트 응답 DTO") +@Getter +public class ProjectDetailAPIRes { + + @Schema(description = "프로젝트 ID", example = "0", implementation = Long.class) + private final Long projectId; + + @Schema(description = "프로젝트 생성자 정보") + private final UserPublicApiResponse creator; + + @Schema(description = "프로젝트 모임 태그", example = "[\"수다스러운\", \"재밌는\"]") + private final List projectTags; + + @Schema(description = "프로젝트 만남 장소 정보") + private final MeetingInfoResponse meetingInfo; + + + public ProjectDetailAPIRes(Long projectId, UserPublicApiResponse creator, + List projectTags, MeetingInfoResponse meetingInfo + ) { + this.projectId = projectId; + this.creator = creator; + this.projectTags = projectTags; + this.meetingInfo = meetingInfo; + } + + public static ProjectDetailAPIRes of(Long projectId, UserPublicApiResponse creator, + List projectTags, MeetingInfoResponse meetingInfo) { + return new ProjectDetailAPIRes( + projectId, + creator, + projectTags, + meetingInfo + ); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/project_join_req/application/ProjectJoinRequestService.java b/src/main/java/io/oeid/mogakgo/domain/project_join_req/application/ProjectJoinRequestService.java new file mode 100644 index 00000000..1078a51c --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/project_join_req/application/ProjectJoinRequestService.java @@ -0,0 +1,78 @@ +package io.oeid.mogakgo.domain.project_join_req.application; + +import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_PROJECT_JOIN_REQUEST_REGION; +import static io.oeid.mogakgo.exception.code.ErrorCode400.PROJECT_JOIN_REQUEST_ALREADY_EXIST; +import static io.oeid.mogakgo.exception.code.ErrorCode403.PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION; +import static io.oeid.mogakgo.exception.code.ErrorCode404.PROJECT_NOT_FOUND; +import static io.oeid.mogakgo.exception.code.ErrorCode404.USER_NOT_FOUND; + +import io.oeid.mogakgo.domain.project.domain.entity.Project; +import io.oeid.mogakgo.domain.project.exception.ProjectException; +import io.oeid.mogakgo.domain.project.infrastructure.ProjectJpaRepository; +import io.oeid.mogakgo.domain.project_join_req.application.dto.req.ProjectJoinCreateReq; +import io.oeid.mogakgo.domain.project_join_req.domain.entity.ProjectJoinRequest; +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.user.domain.User; +import io.oeid.mogakgo.domain.user.exception.UserException; +import io.oeid.mogakgo.domain.user.infrastructure.UserJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ProjectJoinRequestService { + + private final ProjectJoinRequestJpaRepository projectJoinRequestRepository; + private final UserJpaRepository userRepository; + private final ProjectJpaRepository projectRepository; + + public Long create(Long userId, ProjectJoinCreateReq request) { + User tokenUser = validateToken(userId); + validateSender(tokenUser, request.getSenderId()); + Project project = validateProjectExist(request.getProjectId()); + validateProjectCreator(project, userId); + validateUserCertRegion(project, tokenUser); + validateAlreadyExistRequest(userId, project.getId()); + + // 프로젝트 매칭 요청 생성 + ProjectJoinRequest joinRequest = request.toEntity(tokenUser, project); + projectJoinRequestRepository.save(joinRequest); + + return joinRequest.getId(); + } + + private User validateToken(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); + } + + private void validateSender(User tokenUser, Long userId) { + if (!tokenUser.getId().equals(userId)) { + throw new ProjectJoinRequestException(PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION); + } + } + + private Project validateProjectExist(Long projectId) { + return projectRepository.findById(projectId) + .orElseThrow(() -> new ProjectException(PROJECT_NOT_FOUND)); + } + + private void validateProjectCreator(Project project, Long userId) { + if (project.getCreator().getId().equals(userId)) { + throw new ProjectJoinRequestException(PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION); + } + } + + private void validateAlreadyExistRequest(Long userId, Long projectId) { + if (projectJoinRequestRepository.findAlreadyExists(userId, projectId).isPresent()) { + throw new ProjectJoinRequestException(PROJECT_JOIN_REQUEST_ALREADY_EXIST); + } + } + + private void validateUserCertRegion(Project project, User tokenUser) { + if (!tokenUser.getRegion().equals(project.getCreatorInfo().getRegion())) { + throw new ProjectJoinRequestException(INVALID_PROJECT_JOIN_REQUEST_REGION); + } + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/project_join_req/application/dto/req/ProjectJoinCreateReq.java b/src/main/java/io/oeid/mogakgo/domain/project_join_req/application/dto/req/ProjectJoinCreateReq.java new file mode 100644 index 00000000..2c33864c --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/project_join_req/application/dto/req/ProjectJoinCreateReq.java @@ -0,0 +1,36 @@ +package io.oeid.mogakgo.domain.project_join_req.application.dto.req; + +import io.oeid.mogakgo.domain.project.domain.entity.Project; +import io.oeid.mogakgo.domain.project_join_req.domain.entity.ProjectJoinRequest; +import io.oeid.mogakgo.domain.project_join_req.domain.entity.enums.RequestStatus; +import io.oeid.mogakgo.domain.user.domain.User; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Schema(description = "프로젝트 매칭 요청 생성") +@Getter +public class ProjectJoinCreateReq { + + @Schema(description = "프로젝트 매칭 요청자 ID", example = "2", implementation = Long.class) + @NotNull + private final Long senderId; + + @Schema(description = "매칭이 진행될 프로젝트 ID", example = "1", implementation = Long.class) + @NotNull + private final Long projectId; + + @Schema(description = "프로젝트 매칭 요청 상태", example = "PENDING", implementation = RequestStatus.class) + private final RequestStatus requestStatus; + + private ProjectJoinCreateReq(Long senderId, Long projectId) { + this.senderId = senderId; + this.projectId = projectId; + this.requestStatus = RequestStatus.PENDING; + } + + public ProjectJoinRequest toEntity(User sender, Project project) { + return ProjectJoinRequest.of(sender, project); + } + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/project_join_req/domain/entity/ProjectJoinRequest.java b/src/main/java/io/oeid/mogakgo/domain/project_join_req/domain/entity/ProjectJoinRequest.java index 447f9ac2..128b3846 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project_join_req/domain/entity/ProjectJoinRequest.java +++ b/src/main/java/io/oeid/mogakgo/domain/project_join_req/domain/entity/ProjectJoinRequest.java @@ -17,6 +17,7 @@ import jakarta.persistence.Table; import java.time.LocalDateTime; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.CreatedDate; @@ -49,4 +50,15 @@ public class ProjectJoinRequest { @CreatedDate @Column(name = "created_at") private LocalDateTime createdAt; + + @Builder + private ProjectJoinRequest(User sender, Project project) { + this.sender = sender; + this.project = project; + this.requestStatus = RequestStatus.PENDING; + } + + public static ProjectJoinRequest of(User sender, Project project) { + return new ProjectJoinRequest(sender, project); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/project_join_req/infrastructure/ProjectJoinRequestJpaRepository.java b/src/main/java/io/oeid/mogakgo/domain/project_join_req/infrastructure/ProjectJoinRequestJpaRepository.java index 111b7302..8d4479be 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project_join_req/infrastructure/ProjectJoinRequestJpaRepository.java +++ b/src/main/java/io/oeid/mogakgo/domain/project_join_req/infrastructure/ProjectJoinRequestJpaRepository.java @@ -1,11 +1,15 @@ package io.oeid.mogakgo.domain.project_join_req.infrastructure; import io.oeid.mogakgo.domain.project_join_req.domain.entity.ProjectJoinRequest; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface ProjectJoinRequestJpaRepository extends JpaRepository, ProjectJoinRequestRepositoryCustom { boolean existsByProjectId(Long projectId); + @Query("select pjr from ProjectJoinRequest pjr where pjr.sender.id = :userId and pjr.project.id = :projectId") + Optional findAlreadyExists(Long userId, Long projectId); } diff --git a/src/main/java/io/oeid/mogakgo/domain/project_join_req/infrastructure/ProjectJoinRequestRepositoryCustom.java b/src/main/java/io/oeid/mogakgo/domain/project_join_req/infrastructure/ProjectJoinRequestRepositoryCustom.java index 8386917a..810a22f2 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project_join_req/infrastructure/ProjectJoinRequestRepositoryCustom.java +++ b/src/main/java/io/oeid/mogakgo/domain/project_join_req/infrastructure/ProjectJoinRequestRepositoryCustom.java @@ -10,5 +10,4 @@ public interface ProjectJoinRequestRepositoryCustom { CursorPaginationResult findByConditionWithPagination( Long senderId, Long projectId, RequestStatus requestStatus, CursorPaginationInfoReq pageable ); - } 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 new file mode 100644 index 00000000..81cb7e8d --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/ProjectJoinRequestController.java @@ -0,0 +1,30 @@ +package io.oeid.mogakgo.domain.project_join_req.presentation; + +import io.oeid.mogakgo.common.annotation.UserId; +import io.oeid.mogakgo.common.swagger.template.ProjectJoinRequestSwagger; +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; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/projects/join-request") +@RequiredArgsConstructor +public class ProjectJoinRequestController implements ProjectJoinRequestSwagger { + + private final ProjectJoinRequestService projectJoinRequestService; + + @PostMapping + public ResponseEntity create( + @UserId Long userId, @Valid @RequestBody ProjectJoinCreateReq request + ) { + return ResponseEntity.status(201) + .body(ProjectJoinRequestAPIRes.from(projectJoinRequestService.create(userId, request))); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/dto/res/ProjectJoinRequestAPIRes.java b/src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/dto/res/ProjectJoinRequestAPIRes.java new file mode 100644 index 00000000..6a7f6789 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/project_join_req/presentation/dto/res/ProjectJoinRequestAPIRes.java @@ -0,0 +1,21 @@ +package io.oeid.mogakgo.domain.project_join_req.presentation.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Schema(description = "프로젝트 매칭 생성 요청 DTO") +@Getter +public class ProjectJoinRequestAPIRes { + + @Schema(description = "생성된 프로젝트 매칭 ID", example = "1", implementation = Long.class) + private final Long id; + + private ProjectJoinRequestAPIRes(Long id) { + this.id = id; + } + + public static ProjectJoinRequestAPIRes from(Long id) { + return new ProjectJoinRequestAPIRes(id); + } + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserPublicApiResponse.java b/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserPublicApiResponse.java index 6c6df290..0f6bf707 100644 --- a/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserPublicApiResponse.java +++ b/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserPublicApiResponse.java @@ -6,13 +6,12 @@ import io.oeid.mogakgo.domain.user.domain.UserWantedJobTag; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; -import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @Schema(description = "회원 프로필 조회 응답") @Getter -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor public class UserPublicApiResponse { @Schema(description = "회원 식별자", example = "1") private final long id; 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 7eeb9f2b..7acb9040 100644 --- a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java +++ b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java @@ -26,6 +26,8 @@ public enum ErrorCode400 implements ErrorCode { USERNAME_SHOULD_BE_NOT_EMPTY("E020103", "유저 이름은 비어있을 수 없습니다."), USER_REGION_SHOULD_BE_NOT_EMPTY("E020104", "유저 지역은 비어있을 수 없습니다."), USER_ID_NOT_NULL("E020001", "유저 아이디는 필수값입니다."), + PROJECT_JOIN_REQUEST_ALREADY_EXIST("E090101", "이미 매칭 요청을 전송한 프로젝트에 매칭 요청을 생성할 수 없습니다."), + INVALID_PROJECT_JOIN_REQUEST_REGION("E090102", "동네 인증한 구역에서만 프로젝트 매칭 요청을 생성할 수 있습니다."), ; 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 1deaa9b9..50c541f1 100644 --- a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java +++ b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java @@ -8,7 +8,7 @@ public enum ErrorCode403 implements ErrorCode { AUTH_ACCESS_DENIED("E010203", "접근 권한이 없습니다."), PROJECT_FORBIDDEN_OPERATION("E030201", "해당 프로젝트에 대한 권한이 없습니다."), PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION("E050201", "해당 프로젝트 요청에 대한 권한이 없습니다."), - CERT_INVALID_INFORMATION("E070201", "동네 인증을 수행할 권한이 없습니다."), + INVALID_CERT_INFORMATION("E070201", "동네 인증을 수행할 권한이 없습니다."), ; private final HttpStatus httpStatus = HttpStatus.FORBIDDEN;