From c40f77e9695103bcfe0235c5b665a67522e392f8 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Tue, 21 May 2024 00:03:28 +0900 Subject: [PATCH 01/35] =?UTF-8?q?[FIX]=20=ED=81=AC=EB=A3=A8=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80=20(#195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/makers/crew/main/common/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/config/SecurityConfig.java b/main/src/main/java/org/sopt/makers/crew/main/common/config/SecurityConfig.java index a586c7ee..c1b9172e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/config/SecurityConfig.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/config/SecurityConfig.java @@ -131,7 +131,7 @@ CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins( Arrays.asList("https://playground.sopt.org/", "http://localhost:3000/", - "https://sopt-internal-dev.pages.dev/")); + "https://sopt-internal-dev.pages.dev/", "https://crew.api.dev.sopt.org", "https://crew.api.prod.sopt.org")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE", "OPTIONS")); configuration.addAllowedHeader("*"); configuration.setAllowCredentials(false); From b6f7e1d18b3f2387a90da1cbfe91d96fb83b9c5c Mon Sep 17 00:00:00 2001 From: JiHwan <62228195+sgh002400@users.noreply.github.com> Date: Fri, 24 May 2024 06:37:21 +0900 Subject: [PATCH 02/35] =?UTF-8?q?[FIX]=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=B0=8F=20=EC=83=81=EC=84=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20updatedDate=20->=20createdDate=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20(#200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts | 4 ++-- .../get-meeting-posts/post-v1-get-posts-response-post.dto.ts | 4 ++-- server/src/post/v1/post-v1.service.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/post/v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts b/server/src/post/v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts index 359cea38..90eb010e 100644 --- a/server/src/post/v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts +++ b/server/src/post/v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts @@ -96,9 +96,9 @@ export class PostV1GetPostResponseDto { @IsString() contents: string; - /** 게시글 게시/업데이트 일자 */ + /** 게시글 게시/생성 일자 */ @IsDate() - updatedDate: Date; + createdDate: Date; /** 첨부 이미지 */ @IsOptional() diff --git a/server/src/post/v1/dto/get-meeting-posts/post-v1-get-posts-response-post.dto.ts b/server/src/post/v1/dto/get-meeting-posts/post-v1-get-posts-response-post.dto.ts index 9bd73659..d47b8829 100644 --- a/server/src/post/v1/dto/get-meeting-posts/post-v1-get-posts-response-post.dto.ts +++ b/server/src/post/v1/dto/get-meeting-posts/post-v1-get-posts-response-post.dto.ts @@ -68,9 +68,9 @@ export class PostV1GetPostsResponsePostDto { @IsString() contents: string; - /** 게시글 게시/업데이트 일자 */ + /** 게시글 게시/생성 일자 */ @IsDate() - updatedDate: Date; + createdDate: Date; /** 첨부 이미지 */ @IsOptional() diff --git a/server/src/post/v1/post-v1.service.ts b/server/src/post/v1/post-v1.service.ts index a6bdd092..1fc9fb52 100644 --- a/server/src/post/v1/post-v1.service.ts +++ b/server/src/post/v1/post-v1.service.ts @@ -102,7 +102,7 @@ export class PostV1Service { id: post.id, title: post.title, contents: post.contents, - updatedDate: post.updatedDate, + createdDate: post.createdDate, images: post.images, user: { id: post.user.id, @@ -161,7 +161,7 @@ export class PostV1Service { id: post.id, title: post.title, contents: post.contents, - updatedDate: post.updatedDate, + createdDate: post.createdDate, images: post.images, user: { id: post.user.id, From 64afced8f2f31e79969788febd9428da4b334ad9 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Fri, 31 May 2024 13:28:20 +0900 Subject: [PATCH 03/35] =?UTF-8?q?[FEAT]=20=EB=AA=A8=EC=9E=84=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=EC=9E=90=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20(#201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [ADD] querydsl 의존성 추가 * [ADD] queryFactory 추가 * [ADD] 신청자 정보 관련 Dto 추가 * [FEAT] querydsl를 사용한 신청자 리스트 조회 * [ADD] 신청자 정보 관련 Dto 추가 * [ADD] 컨트롤러 -> 서비스 간의 Dto 추가 * [ADD] 신청자 리스트 조회 API 스웨거 반영 * [FEAT] 신청자 리스트 조회 컨트롤러 구현 * [FEAT] 신청자 리스트 조회 서비스 로직 구현, 페이지네이션 구현 * [ADD] querydsl 사용을 위한 config 추가 * [ADD] ApplyRepositoryTest를 위한 sql 추가 * [REFACTOR] apply와 Meeting 간의 의존성 줄이기 * [TEST] ApplyRepository querydsl 로직 테스트 * [CHORE] type(신청, 초대) 제거 * [CHORE] @ModelAttribute 사용 * [CHORE] 주석제거 * [CHORE] studyCreatorId -> meetingCreatorId 변경 * [CHORE] join문 삭제 * [CHORE] leftjoin -> innerjoin 변경 * [CHORE] Dto 변수명 변경 * [CHORE] Dto 변수명 변경 * [CHORE] jsonb 타입 처리 UserUtil에서 처리 * [CHORE] 디폴트 value 설정 --- main/build.gradle | 6 + .../main/common/config/JpaAuditingConfig.java | 11 + .../crew/main/common/util/UserUtil.java | 7 + .../main/entity/apply/ApplyRepository.java | 2 +- .../entity/apply/ApplySearchRepository.java | 12 + .../apply/ApplySearchRepositoryImpl.java | 66 +++++ .../crew/main/meeting/v2/MeetingV2Api.java | 9 + .../main/meeting/v2/MeetingV2Controller.java | 15 + .../dto/query/MeetingGetApplyListCommand.java | 25 ++ .../meeting/v2/dto/response/ApplicantDto.java | 30 ++ .../meeting/v2/dto/response/ApplyInfoDto.java | 25 ++ .../MeetingGetApplyListResponseDto.java | 14 + .../meeting/v2/service/MeetingV2Service.java | 5 + .../v2/service/MeetingV2ServiceImpl.java | 21 ++ main/src/main/resources/application-test.yml | 4 +- .../crew/main/common/config/TestConfig.java | 18 ++ .../main/entity/user/UserRepositoryTest.java | 3 + .../v2/repository/ApplyRepositoryTest.java | 276 ++++++++++++++++++ .../sql/apply-repository-test-data.sql | 55 ++++ .../test/resources/sql/delete-all-data.sql | 30 ++ 20 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetApplyListCommand.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplicantDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplyInfoDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingGetApplyListResponseDto.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java create mode 100644 main/src/test/resources/sql/apply-repository-test-data.sql create mode 100644 main/src/test/resources/sql/delete-all-data.sql diff --git a/main/build.gradle b/main/build.gradle index a3c0bdae..b2d70c5b 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -65,6 +65,12 @@ dependencies { // MapStruct implementation 'org.mapstruct:mapstruct:1.5.3.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final' + + // queryDsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java b/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java index 98f3d5d7..5c83ce72 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java @@ -1,9 +1,20 @@ package org.sopt.makers.crew.main.common.config; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @EnableJpaAuditing @Configuration public class JpaAuditingConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory queryFactory() { + return new JPAQueryFactory(entityManager); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/UserUtil.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/UserUtil.java index d5b66e1a..91ce3151 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/util/UserUtil.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/UserUtil.java @@ -1,8 +1,11 @@ package org.sopt.makers.crew.main.common.util; import java.security.Principal; +import java.util.Comparator; +import java.util.List; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.exception.UnAuthorizedException; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; @RequiredArgsConstructor public class UserUtil { @@ -13,4 +16,8 @@ public static Integer getUserId(Principal principal) { } return Integer.valueOf(principal.getName()); } + + public static UserActivityVO getRecentUserActivity(List userActivityVOs){ + return userActivityVOs.stream().max(Comparator.comparingInt(UserActivityVO::getGeneration)).orElse(null); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java index 76b74b90..07a9448d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java @@ -12,7 +12,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -public interface ApplyRepository extends JpaRepository { +public interface ApplyRepository extends JpaRepository, ApplySearchRepository { @Query("select a from Apply a join fetch a.meeting m where a.userId = :userId and a.status = :statusValue") List findAllByUserIdAndStatus(@Param("userId") Integer userId, diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java new file mode 100644 index 00000000..c8309198 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java @@ -0,0 +1,12 @@ +package org.sopt.makers.crew.main.entity.apply; + +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + + +public interface ApplySearchRepository { + Page findApplyList(MeetingGetApplyListCommand queryCommand, Pageable pageable, Integer meetingId, + Integer meetingCreatorId, Integer userId); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java new file mode 100644 index 00000000..915e7c34 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java @@ -0,0 +1,66 @@ +package org.sopt.makers.crew.main.entity.apply; + +import static org.sopt.makers.crew.main.entity.apply.QApply.apply; +import static org.sopt.makers.crew.main.entity.user.QUser.user; + +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.QApplicantDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.QApplyInfoDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ApplySearchRepositoryImpl implements ApplySearchRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Page findApplyList(MeetingGetApplyListCommand queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId) { + List content = getContent(queryCommand, pageable, meetingId, meetingCreatorId, userId); + JPAQuery countQuery = getCount(queryCommand, meetingId); + + return PageableExecutionUtils.getPage(content, + PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()), countQuery::fetchFirst); + } + + private List getContent(MeetingGetApplyListCommand queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId) { + boolean isStudyCreator = Objects.equals(meetingCreatorId, userId); + return queryFactory + .select(new QApplyInfoDto( + apply.id, isStudyCreator ? apply.content : Expressions.constant(""), + apply.appliedDate, apply.status, + new QApplicantDto(user.id, user.name, user.orgId, user.activities, user.profileImage, + user.phone))) + .from(apply) + .innerJoin(apply.user, user) + .where( + apply.meetingId.eq(meetingId), + apply.status.in(queryCommand.getStatus()) + ) + .orderBy(queryCommand.getDate().equals("desc") ? apply.appliedDate.desc() : apply.appliedDate.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } + + private JPAQuery getCount(MeetingGetApplyListCommand queryCommand, Integer meetingId) { + return queryFactory + .select(apply.count()) + .from(apply) + .where( + apply.meetingId.eq(meetingId), + apply.status.in(queryCommand.getStatus()) + ); + + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java index 9a31956c..b234363b 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java @@ -11,9 +11,11 @@ import jakarta.validation.Valid; import java.security.Principal; import java.util.List; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; @@ -62,4 +64,11 @@ ResponseEntity applyMeeting(@RequestBody Meeti ResponseEntity applyMeetingCancel(@PathVariable Integer meetingId, Principal principal); + + @Operation(summary = "모임 지원자/참여자 조회", description = "모임 지원자/참여자 조회 (모임장이면 지원자, 아니면 참여자 조회)") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "모임 지원자/참여자 조회 성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content),}) + ResponseEntity findApplyList(@PathVariable Integer meetingId, + @ModelAttribute MeetingGetApplyListCommand queryCommand, + Principal principal); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java index 1efa33d7..1b1ab29f 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java @@ -6,9 +6,11 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.util.UserUtil; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; @@ -23,6 +25,7 @@ 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.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -77,4 +80,16 @@ public ResponseEntity applyMeetingCancel(@PathVariable Integer meetingId, meetingV2Service.applyMeetingCancel(meetingId, userId); return ResponseEntity.status(HttpStatus.OK).build(); } + + @Override + @GetMapping("/{meetingId}/list") + public ResponseEntity findApplyList(@PathVariable Integer meetingId, + @ModelAttribute MeetingGetApplyListCommand queryCommand, + Principal principal) { + + Integer userId = UserUtil.getUserId(principal); + + return ResponseEntity.status(HttpStatus.OK) + .body(meetingV2Service.findApplyList(queryCommand, meetingId, userId)); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetApplyListCommand.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetApplyListCommand.java new file mode 100644 index 00000000..a6c08449 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetApplyListCommand.java @@ -0,0 +1,25 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.query; + +import java.util.Arrays; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; +import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; + +@Getter +@Setter +public class MeetingGetApplyListCommand extends PageOptionsDto { + + private List status; + private String date; + + @Builder + public MeetingGetApplyListCommand(int page, int take, List status, String date) { + super(page, take); + this.status = (status == null || status.isEmpty()) ? + Arrays.asList(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT) : status; + this.date = date == null ? "desc" : date; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplicantDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplicantDto.java new file mode 100644 index 00000000..4eb516a3 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplicantDto.java @@ -0,0 +1,30 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import java.util.Comparator; +import java.util.List; +import lombok.Getter; +import org.sopt.makers.crew.main.common.util.UserUtil; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; + +@Getter +public class ApplicantDto { + private final Integer id; + private final String name; + private final Integer orgId; + private final UserActivityVO recentActivity; + private final String profileImage; + private final String phone; + + @QueryProjection + public ApplicantDto(Integer id, String name, Integer orgId, List userActivityVOs, String profileImage, + String phone) { + + this.id = id; + this.name = name; + this.orgId = orgId; + this.recentActivity = UserUtil.getRecentUserActivity(userActivityVOs); + this.profileImage = profileImage; + this.phone = phone; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplyInfoDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplyInfoDto.java new file mode 100644 index 00000000..23148265 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplyInfoDto.java @@ -0,0 +1,25 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import java.time.LocalDateTime; +import lombok.Getter; +import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; + +@Getter +public class ApplyInfoDto { + private final Integer id; + private final String content; + private final LocalDateTime appliedDate; + private final EnApplyStatus status; + private final ApplicantDto user; + + @QueryProjection + public ApplyInfoDto(Integer id, String content, LocalDateTime appliedDate, EnApplyStatus status, + ApplicantDto user) { + this.id = id; + this.content = content; + this.appliedDate = appliedDate; + this.status = status; + this.user = user; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingGetApplyListResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingGetApplyListResponseDto.java new file mode 100644 index 00000000..f1e85883 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingGetApplyListResponseDto.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; + +@Getter +@AllArgsConstructor(staticName = "of") +public class MeetingGetApplyListResponseDto { + + private final List apply; + private final PageMetaDto meta; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java index e6d2b4f9..69fe3e10 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java @@ -1,9 +1,11 @@ package org.sopt.makers.crew.main.meeting.v2.service; import java.util.List; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; @@ -21,4 +23,7 @@ MeetingV2GetAllMeetingByOrgUserDto getAllMeetingByOrgUser( MeetingV2ApplyMeetingResponseDto applyMeeting(MeetingV2ApplyMeetingDto requestBody, Integer userId); void applyMeetingCancel(Integer meetingId, Integer userId); + + MeetingGetApplyListResponseDto findApplyList(MeetingGetApplyListCommand queryCommand, Integer meetingId, + Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java index c237a714..71212608 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java @@ -37,15 +37,20 @@ import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper; import org.sopt.makers.crew.main.meeting.v2.dto.MeetingMapper; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetMeetingBannerResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetMeetingBannerResponseUserDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -178,6 +183,22 @@ public void applyMeetingCancel(Integer meetingId, Integer userId) { applyRepository.deleteByMeetingIdAndUserId(meetingId, userId); } + @Override + @Transactional(readOnly = true) + public MeetingGetApplyListResponseDto findApplyList(MeetingGetApplyListCommand queryCommand, + Integer meetingId, + Integer userId) { + Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, + PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), + meetingId, meeting.getUserId(), userId); + PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), queryCommand.getTake()); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, (int) applyInfoDtos.getTotalElements()); + + return MeetingGetApplyListResponseDto.of(applyInfoDtos.getContent(), pageMetaDto); + } + + private Boolean checkMeetingLeader(Meeting meeting, Integer userId) { return meeting.getUserId().equals(userId); } diff --git a/main/src/main/resources/application-test.yml b/main/src/main/resources/application-test.yml index 8415f784..443dddf8 100644 --- a/main/src/main/resources/application-test.yml +++ b/main/src/main/resources/application-test.yml @@ -17,12 +17,14 @@ spring: hibernate: naming: physical-strategy: org.sopt.makers.crew.main.common.config.CamelCaseNamingStrategy - ddl-auto: create + ddl-auto: create-drop properties: hibernate: show_sql: true format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect storage_engine: innodb + defer-datasource-initialization: true jwt: header: Authorization diff --git a/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java b/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java new file mode 100644 index 00000000..b8b89398 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java @@ -0,0 +1,18 @@ +package org.sopt.makers.crew.main.common.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory(){ + return new JPAQueryFactory(entityManager); + } +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserRepositoryTest.java b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserRepositoryTest.java index 1bd8ea9b..21d5a8b6 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserRepositoryTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserRepositoryTest.java @@ -2,14 +2,17 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.common.config.TestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; @DataJpaTest @ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import(TestConfig.class) public class UserRepositoryTest { @Autowired diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java new file mode 100644 index 00000000..b7098eb3 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java @@ -0,0 +1,276 @@ +package org.sopt.makers.crew.main.meeting.v2.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.common.config.TestConfig; +import org.sopt.makers.crew.main.entity.apply.ApplyRepository; +import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; +import org.springframework.test.context.jdbc.SqlGroup; + +@DataJpaTest +@ActiveProfiles("test") +@Import(TestConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@SqlGroup({ + @Sql(value = "/sql/apply-repository-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) + +}) +public class ApplyRepositoryTest { + + @Autowired + private ApplyRepository applyRepository; + + @Test + void 스터디장이_신청자_리스트_최신순으로_조회() { + // given + int page = 1; + int take = 12; + MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "desc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 1; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(3); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + + ApplyInfoDto applyInfoDto1 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto1) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(3, "전할말입니다3", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 3, 413489000)), EnApplyStatus.WAITING); + assertThat(applyInfoDto1.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(4, "이영지", 1004, "profile4.jpg", "010-5555-5555"); + assertThat(applyInfoDto1.getUser().getRecentActivity().getGeneration()).isEqualTo(32); + + + ApplyInfoDto applyInfoDto2 = applyInfoDtos.getContent().get(1); + assertThat(applyInfoDto2) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(2, "전할말입니다2", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 2, 413489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto2.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(3, "김철수", 1003, "profile3.jpg", "010-3333-4444"); + assertThat(applyInfoDto2.getUser().getRecentActivity().getGeneration()).isEqualTo(34); + + + ApplyInfoDto applyInfoDto3 = applyInfoDtos.getContent().get(2); + assertThat(applyInfoDto3) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(1, "전할말입니다1", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 0, 913489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto3.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(2, "홍길동", 1002, "profile2.jpg", "010-1111-2222"); + assertThat(applyInfoDto3.getUser().getRecentActivity().getGeneration()).isEqualTo(33); + } + + @Test + void 스터디장이_신청자_리스트_오래된순으로_조회() { + // given + int page = 1; + int take = 12; + MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "asc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 1; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(3); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + ApplyInfoDto applyInfoDto3 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto3) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(1, "전할말입니다1", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 0, 913489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto3.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(2, "홍길동", 1002, "profile2.jpg", "010-1111-2222"); + assertThat(applyInfoDto3.getUser().getRecentActivity().getGeneration()).isEqualTo(33); + + ApplyInfoDto applyInfoDto2 = applyInfoDtos.getContent().get(1); + assertThat(applyInfoDto2) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(2, "전할말입니다2", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 2, 413489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto2.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(3, "김철수", 1003, "profile3.jpg", "010-3333-4444"); + assertThat(applyInfoDto2.getUser().getRecentActivity().getGeneration()).isEqualTo(34); + + ApplyInfoDto applyInfoDto1 = applyInfoDtos.getContent().get(2); + assertThat(applyInfoDto1) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(3, "전할말입니다3", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 3, 413489000)), EnApplyStatus.WAITING); + assertThat(applyInfoDto1.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(4, "이영지", 1004, "profile4.jpg", "010-5555-5555"); + assertThat(applyInfoDto1.getUser().getRecentActivity().getGeneration()).isEqualTo(32); + + } + + @Test + void 스터디장이_신청자_리스트_대기상태만_오래된순으로_조회() { + // given + int page = 1; + int take = 12; + MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.WAITING), "asc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 1; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(1); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + ApplyInfoDto applyInfoDto1 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto1) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(3, "전할말입니다3", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 3, 413489000)), EnApplyStatus.WAITING); + assertThat(applyInfoDto1.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(4, "이영지", 1004, "profile4.jpg", "010-5555-5555"); + assertThat(applyInfoDto1.getUser().getRecentActivity().getGeneration()).isEqualTo(32); + + } + + @Test + void 스터디장이_신청자_리스트_승인상태만_오래된순으로_조회() { + // given + int page = 1; + int take = 12; + MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.APPROVE), "asc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 1; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(2); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + ApplyInfoDto applyInfoDto3 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto3) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(1, "전할말입니다1", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 0, 913489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto3.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(2, "홍길동", 1002, "profile2.jpg", "010-1111-2222"); + assertThat(applyInfoDto3.getUser().getRecentActivity().getGeneration()).isEqualTo(33); + + ApplyInfoDto applyInfoDto2 = applyInfoDtos.getContent().get(1); + assertThat(applyInfoDto2) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(2, "전할말입니다2", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 2, 413489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto2.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(3, "김철수", 1003, "profile3.jpg", "010-3333-4444"); + assertThat(applyInfoDto2.getUser().getRecentActivity().getGeneration()).isEqualTo(34); + + } + + + @Test + void 스터디장이_아닌_사람이_신청자_리스트_조회() { + // given + int page = 1; + int take = 12; + MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "desc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 2; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(3); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + ApplyInfoDto applyInfoDto1 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto1) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(3, "", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 3, 413489000)), EnApplyStatus.WAITING); + assertThat(applyInfoDto1.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(4, "이영지", 1004, "profile4.jpg", "010-5555-5555"); + assertThat(applyInfoDto1.getUser().getRecentActivity().getGeneration()).isEqualTo(32); + + + ApplyInfoDto applyInfoDto2 = applyInfoDtos.getContent().get(1); + assertThat(applyInfoDto2) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(2, "", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 2, 413489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto2.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(3, "김철수", 1003, "profile3.jpg", "010-3333-4444"); + assertThat(applyInfoDto2.getUser().getRecentActivity().getGeneration()).isEqualTo(34); + + + ApplyInfoDto applyInfoDto3 = applyInfoDtos.getContent().get(2); + assertThat(applyInfoDto3) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(1, "", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 0, 913489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto3.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(2, "홍길동", 1002, "profile2.jpg", "010-1111-2222"); + assertThat(applyInfoDto3.getUser().getRecentActivity().getGeneration()).isEqualTo(33); + } + + + +} diff --git a/main/src/test/resources/sql/apply-repository-test-data.sql b/main/src/test/resources/sql/apply-repository-test-data.sql new file mode 100644 index 00000000..3ddff246 --- /dev/null +++ b/main/src/test/resources/sql/apply-repository-test-data.sql @@ -0,0 +1,55 @@ +CREATE TYPE meeting_joinableparts_enum AS ENUM ('PM', 'DESIGN', 'IOS', 'ANDROID', 'SERVER', 'WEB'); + +INSERT INTO "user" (id, name, "orgId", activities, "profileImage", phone) +VALUES (1, '김삼순', 1001, + '[{"part": "서버", "generation": 33}, {"part": "iOS", "generation": 32}]', + 'profile1.jpg', '010-1234-5678'), + (2, '홍길동', 1002, + '[{"part": "기획", "generation": 32}, {"part": "기획", "generation": 29}, {"part": "기획", "generation": 33}, {"part": "기획", "generation": 30}]', + 'profile2.jpg', '010-1111-2222'), + (3, '김철수', 1003, + '[{"part": "웹", "generation": 34}]', + 'profile3.jpg', '010-3333-4444'), + (4, '이영지', 1004, + '[{"part": "iOS", "generation": 32}, {"part": "안드로이드", "generation": 29}]', + 'profile4.jpg', '010-5555-5555'); + +create table "meeting" ( + "canJoinOnlyActiveGeneration" boolean not null, + "capacity" integer not null, + "createdGeneration" integer not null, + "id" serial not null, + "isMentorNeeded" boolean not null, + "targetActiveGeneration" integer, + "userId" integer, + "endDate" TIMESTAMP not null, + "mEndDate" TIMESTAMP not null, + "mStartDate" TIMESTAMP not null, + "startDate" TIMESTAMP not null, + "category" varchar(255) not null, + "desc" varchar(255) not null, + "leaderDesc" varchar(255) not null, + "note" varchar(255), + "processDesc" varchar(255) not null, + "targetDesc" varchar(255) not null, + "title" varchar(255) not null, + "imageURL" jsonb, + "joinableParts" meeting_joinableparts_enum[], + primary key ("id") +); + +INSERT INTO meeting (id, "userId", title, category, "imageURL", "startDate", "endDate", capacity, + "desc", "processDesc", "mStartDate", "mEndDate", "leaderDesc", "targetDesc", + note, "isMentorNeeded", "canJoinOnlyActiveGeneration", "createdGeneration", + "targetActiveGeneration", "joinableParts") +VALUES (1, 1, '스터디 구합니다1', '행사', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-05-19 00:00:00.000000', '2024-05-24 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-29 00:00:00.000000', '2024-05-31 00:00:00.000000', '스터디장 설명입니다.', + '누구나 들어올 수 있어요.', '시간지키세요.', true, false, 34, 34, '{PM,DESIGN,WEB,ANDROID,IOS,SERVER}'); + +INSERT INTO apply (type, "meetingId", "userId", content, "appliedDate", status) +VALUES (0, 1, 2, '전할말입니다1', '2024-05-19 00:00:00.913489', 1), + (0, 1, 3, '전할말입니다2', '2024-05-19 00:00:02.413489', 1), + (0, 1, 4, '전할말입니다3', '2024-05-19 00:00:03.413489', 0); \ No newline at end of file diff --git a/main/src/test/resources/sql/delete-all-data.sql b/main/src/test/resources/sql/delete-all-data.sql new file mode 100644 index 00000000..637ccb04 --- /dev/null +++ b/main/src/test/resources/sql/delete-all-data.sql @@ -0,0 +1,30 @@ +-- 테이블의 데이터 삭제 +DELETE FROM "apply"; +DELETE FROM "comment"; +DELETE FROM "like"; +DELETE FROM "meeting"; +DELETE FROM "notice"; +DELETE FROM "post"; +DELETE FROM "user"; + +-- 시퀀스 초기화 +-- apply 테이블의 시퀀스 초기화 +ALTER SEQUENCE "apply_id_seq" RESTART WITH 1; + +-- comment 테이블의 시퀀스 초기화 +ALTER SEQUENCE "comment_id_seq" RESTART WITH 1; + +-- "like" 테이블의 시퀀스 초기화 +ALTER SEQUENCE "like_id_seq" RESTART WITH 1; + +-- meeting 테이블의 시퀀스 초기화 +ALTER SEQUENCE "meeting_id_seq" RESTART WITH 1; + +-- notice 테이블의 시퀀스 초기화 +ALTER SEQUENCE "notice_id_seq" RESTART WITH 1; + +-- post 테이블의 시퀀스 초기화 +ALTER SEQUENCE "post_id_seq" RESTART WITH 1; + +-- user 테이블의 시퀀스 초기화 +ALTER SEQUENCE "user_id_seq" RESTART WITH 1; \ No newline at end of file From bd2a192654f661c46f31abfccad32a142ed09781 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:03:33 +0900 Subject: [PATCH 04/35] =?UTF-8?q?[FIX]=20=EB=A9=94=EC=9D=B4=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=20=ED=8C=80=EC=9E=A5=20=EB=B2=84=EA=B7=B8=20&=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B7=9C=EC=B9=99=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FIX] 리드 -> 팀장 변경 * [FIX] 라우팅 규칙 수정 * [FIX] nest 리드 -> 팀장 변경 --- docker-compose.yml | 16 ++++++++++------ .../crew/main/entity/user/enums/UserPart.java | 2 +- server/src/entity/user/enum/user-part.enum.ts | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c97c242a..fbef6abe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -132,12 +132,14 @@ services: caddy.route_2.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_3: /user/v2/* caddy.route_3.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_4: /meeting/v2/* + caddy.route_4: /meeting/v2 caddy.route_4.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_5: /post/v2 + caddy.route_5: /meeting/v2/* caddy.route_5.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_6: /comment/v2 + caddy.route_6: /post/v2 caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_7: /comment/v2 + caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" nestjs-blue: image: makerscrew/server:latest @@ -221,12 +223,14 @@ services: caddy.route_2.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_3: /user/v2/* caddy.route_3.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_4: /meeting/v2/* + caddy.route_4: /meeting/v2 caddy.route_4.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_5: /post/v2 + caddy.route_5: /meeting/v2/* caddy.route_5.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_6: /comment/v2 + caddy.route_6: /post/v2 caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_7: /comment/v2 + caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" networks: caddy: diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/enums/UserPart.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/enums/UserPart.java index 068c951b..7afb2bbe 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/enums/UserPart.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/enums/UserPart.java @@ -28,7 +28,7 @@ public enum UserPart { GENERAL_AFFAIRS("총무"), OPERATION_LEADER("운영 팀장"), MEDIA_LEADER("미디어 팀장"), - MAKERS_LEADER("메이커스 리드"); + MAKERS_LEADER("메이커스 팀장"); private final String value; diff --git a/server/src/entity/user/enum/user-part.enum.ts b/server/src/entity/user/enum/user-part.enum.ts index f4b391d4..cdc3f91f 100644 --- a/server/src/entity/user/enum/user-part.enum.ts +++ b/server/src/entity/user/enum/user-part.enum.ts @@ -22,6 +22,6 @@ export enum UserPart { GENERAL_AFFAIRS = '총무', OPERATION_LEADER = '운영 팀장', MEDIA_LEADER = '미디어 팀장', - MAKERS_LEADER = '메이커스 리드', + MAKERS_LEADER = '메이커스 팀장', } From 7b01ce6462e2c6a4c778e7bd16f55c4fe12e328d Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:44:59 +0900 Subject: [PATCH 05/35] =?UTF-8?q?[FIX]=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#210)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index fbef6abe..e1791850 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,8 +64,6 @@ services: - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - AWS_REGION=${AWS_REGION} - JWT_SECRET=${JWT_SECRET} - depends_on: - - nestjs logging: driver: "json-file" options: From 869625db7523d728a7274c4f2571abe1d0a40a5b Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Sun, 23 Jun 2024 00:59:49 +0900 Subject: [PATCH 06/35] =?UTF-8?q?[FEAT]=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EA=B4=80=EB=A0=A8=20API=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20(#223)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [ADD] 공지사항 작성시 시크릿갑 추가 * [ADD] 라우팅 규칙 추가 * [ADD] Dto 추가 * [CHORE] int -> Integer 변경 * [FEAT] 공지사항 관련 컨트롤러 구현 * [FEAT] 공지사항 관련 서비스 로직 구현 * [FEAT] 공지사항 관련 레포지토리 구현 * [FIX] 예외 Enum 변경 --- docker-compose.yml | 4 ++ .../crew/main/entity/notice/Notice.java | 2 +- .../main/entity/notice/NoticeRepository.java | 10 ++++ .../makers/crew/main/notice/NoticeV2Api.java | 23 ++++++++ .../crew/main/notice/NoticeV2Controller.java | 35 ++++++++++++ .../dto/request/NoticeV2CreateRequestDto.java | 16 ++++++ .../dto/response/NoticeV2GetResponseDto.java | 14 +++++ .../main/notice/service/NoticeV2Service.java | 54 +++++++++++++++++++ main/src/main/resources/application-dev.yml | 5 +- main/src/main/resources/application-prod.yml | 3 ++ main/src/main/resources/application-test.yml | 5 +- 11 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/notice/NoticeRepository.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Api.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Controller.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/notice/dto/request/NoticeV2CreateRequestDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java diff --git a/docker-compose.yml b/docker-compose.yml index e1791850..8b117b64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -138,6 +138,8 @@ services: caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_7: /comment/v2 caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_8: /notice/v2 + caddy.route_8.reverse_proxy: "{{ upstreams 4000 }}" nestjs-blue: image: makerscrew/server:latest @@ -229,6 +231,8 @@ services: caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_7: /comment/v2 caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_8: /notice/v2 + caddy.route_8.reverse_proxy: "{{ upstreams 4000 }}" networks: caddy: diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/notice/Notice.java b/main/src/main/java/org/sopt/makers/crew/main/entity/notice/Notice.java index a3824a0d..43f7b61a 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/notice/Notice.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/notice/Notice.java @@ -26,7 +26,7 @@ public class Notice { */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; + private Integer id; /** * 공지사항 제목 diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/notice/NoticeRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/notice/NoticeRepository.java new file mode 100644 index 00000000..684654f0 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/notice/NoticeRepository.java @@ -0,0 +1,10 @@ +package org.sopt.makers.crew.main.entity.notice; + +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeRepository extends JpaRepository { + + List findByExposeStartDateBeforeAndExposeEndDateAfter(LocalDateTime now1, LocalDateTime now2); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Api.java new file mode 100644 index 00000000..6f0c5ad2 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Api.java @@ -0,0 +1,23 @@ +package org.sopt.makers.crew.main.notice; + +import io.swagger.v3.oas.annotations.Operation; +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 java.util.List; +import org.sopt.makers.crew.main.notice.dto.request.NoticeV2CreateRequestDto; +import org.sopt.makers.crew.main.notice.dto.response.NoticeV2GetResponseDto; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "공지사항") +public interface NoticeV2Api { + + @Operation(summary = "공지사항 조회") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "성공")}) + ResponseEntity> getNotices(); + + @Operation(summary = "공지사항 작성") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "성공")}) + ResponseEntity createNotice(@RequestBody NoticeV2CreateRequestDto requestDto); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Controller.java new file mode 100644 index 00000000..d8d4e26f --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Controller.java @@ -0,0 +1,35 @@ +package org.sopt.makers.crew.main.notice; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.notice.dto.request.NoticeV2CreateRequestDto; +import org.sopt.makers.crew.main.notice.dto.response.NoticeV2GetResponseDto; +import org.sopt.makers.crew.main.notice.service.NoticeV2Service; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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("/notice/v2") +@RequiredArgsConstructor +public class NoticeV2Controller implements NoticeV2Api { + private final NoticeV2Service noticeService; + + + @Override + @GetMapping + public ResponseEntity> getNotices() { + return ResponseEntity.ok(noticeService.getNotices()); + } + + @Override + @PostMapping + public ResponseEntity createNotice(@RequestBody NoticeV2CreateRequestDto requestDto) { + noticeService.createNotice(requestDto); + return ResponseEntity.ok(null); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/dto/request/NoticeV2CreateRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/request/NoticeV2CreateRequestDto.java new file mode 100644 index 00000000..15d95a3b --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/request/NoticeV2CreateRequestDto.java @@ -0,0 +1,16 @@ +package org.sopt.makers.crew.main.notice.dto.request; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class NoticeV2CreateRequestDto { + private final String title; + private final String subTitle; + private final String contents; + private final LocalDateTime exposeStartDate; + private final LocalDateTime exposeEndDate; + private final String noticeSecretKey; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java new file mode 100644 index 00000000..1d74f59c --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.notice.dto.response; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(staticName = "of") +public class NoticeV2GetResponseDto { + private final String title; + private final String subTitle; + private final String contents; + private final LocalDateTime createdDate; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java new file mode 100644 index 00000000..90d2bc46 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java @@ -0,0 +1,54 @@ +package org.sopt.makers.crew.main.notice.service; + +import static org.sopt.makers.crew.main.common.response.ErrorStatus.FORBIDDEN_EXCEPTION; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.common.exception.ForbiddenException; +import org.sopt.makers.crew.main.entity.notice.Notice; +import org.sopt.makers.crew.main.entity.notice.NoticeRepository; +import org.sopt.makers.crew.main.notice.dto.request.NoticeV2CreateRequestDto; +import org.sopt.makers.crew.main.notice.dto.response.NoticeV2GetResponseDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NoticeV2Service { + private final NoticeRepository noticeRepository; + + @Value("${notice.secret-key}") + private String noticeSecretKey; + + public List getNotices() { + // Time 클래스로 교체 필요. + List notices = noticeRepository.findByExposeStartDateBeforeAndExposeEndDateAfter(LocalDateTime.now(), + LocalDateTime.now()); + return notices.stream() + .map(notice -> NoticeV2GetResponseDto.of(notice.getTitle(), notice.getSubTitle(), notice.getContents(), + notice.getCreatedDate())) + .toList(); + } + + @Transactional + public void createNotice(NoticeV2CreateRequestDto requestDto) { + + if (!requestDto.getNoticeSecretKey().equals(noticeSecretKey)) { + throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); + } + + Notice notice = Notice.builder() + .title(requestDto.getTitle()) + .subTitle(requestDto.getSubTitle()) + .contents(requestDto.getContents()) + .exposeStartDate(requestDto.getExposeStartDate()) + .exposeEndDate(requestDto.getExposeEndDate()) + .build(); + + noticeRepository.save(notice); + } +} diff --git a/main/src/main/resources/application-dev.yml b/main/src/main/resources/application-dev.yml index ea96751d..b4013a68 100644 --- a/main/src/main/resources/application-dev.yml +++ b/main/src/main/resources/application-dev.yml @@ -51,4 +51,7 @@ push-notification: web-url: ${DEV_WEB_PAGE_URL} x-api-key: ${DEV_PUSH_API_KEY} service: ${PUSH_NOTIFICATION_SERVICE} - push-server-url: ${DEV_PUSH_SERVER_URL} \ No newline at end of file + push-server-url: ${DEV_PUSH_SERVER_URL} + +notice: + secret-key : ${NOTICE_SECRET_KEY} \ No newline at end of file diff --git a/main/src/main/resources/application-prod.yml b/main/src/main/resources/application-prod.yml index e46dfb7a..4110fa7b 100644 --- a/main/src/main/resources/application-prod.yml +++ b/main/src/main/resources/application-prod.yml @@ -51,3 +51,6 @@ push-notification: x-api-key: ${PROD_PUSH_API_KEY} service: ${PUSH_NOTIFICATION_SERVICE} push-server-url: ${PROD_PUSH_SERVER_URL} + +notice: + secret-key : ${NOTICE_SECRET_KEY} \ No newline at end of file diff --git a/main/src/main/resources/application-test.yml b/main/src/main/resources/application-test.yml index 443dddf8..d245c43c 100644 --- a/main/src/main/resources/application-test.yml +++ b/main/src/main/resources/application-test.yml @@ -54,4 +54,7 @@ push-notification: web-url: ${DEV_WEB_PAGE_URL} x-api-key: ${DEV_PUSH_API_KEY} service: ${PUSH_NOTIFICATION_SERVICE} - push-server-url: ${DEV_PUSH_SERVER_URL} \ No newline at end of file + push-server-url: ${DEV_PUSH_SERVER_URL} + +notice: + secret-key : ${NOTICE_SECRET_KEY} \ No newline at end of file From 3f9df903b50635a5138427a2014853d10b876f70 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Mon, 24 Jun 2024 00:43:28 +0900 Subject: [PATCH 07/35] =?UTF-8?q?[FIX]=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=ED=8E=B8=EC=A7=91=20(#228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 8b117b64..0aa9cefe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,6 +105,10 @@ services: caddy.route_13.reverse_proxy: "{{ upstreams 3000 }}" caddy.route_14: /users/* caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}" + caddy.route_15: /notice/v2 + caddy.route_15.reverse_proxy: "{{ upstreams 3000 }}" + caddy.route_16: /notice/v2/* + caddy.route_16.reverse_proxy: "{{ upstreams 3000 }}" spring-green: image: makerscrew/main:latest @@ -198,6 +202,10 @@ services: caddy.route_13.reverse_proxy: "{{ upstreams 3000 }}" caddy.route_14: /users/* caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}" + caddy.route_15: /notice/v2 + caddy.route_15.reverse_proxy: "{{ upstreams 3000 }}" + caddy.route_16: /notice/v2/* + caddy.route_16.reverse_proxy: "{{ upstreams 3000 }}" spring-blue: image: makerscrew/main:latest From 73f9d3fdf0075cc521261fddb8a40300a819185d Mon Sep 17 00:00:00 2001 From: Yeseul Jo <68415644+yeseul106@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:10:43 +0900 Subject: [PATCH 08/35] =?UTF-8?q?[FEAT]=20=EB=AA=A8=EC=9E=84=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?V2=20API=20=EA=B5=AC=ED=98=84=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [ADD] 게시글 목록 조회 API 관련 DTO 파일 추가 * [FEAT] 게시글 목록 조회 컨트롤러 관련 코드 구현 * [FEAT] querydsl을 사용한 게시글 목록 조회 구현 * [FEAT] 게시글 목록 조회 API service 로직 구현 * [ADD] querydsl의 transform을 사용하기 위해 JPAQueryFactory 설정 추가 * [TEST] 게시물 목록 조회 관련 postRepository 테스트 코드 작성 * [FIX] 테스트 코드 검증에 따른 쿼리 수정 * [FIX] Like 엔티티 내의 양방향 매핑 로직 오류 수정 * [FIX] 라우팅 규칙 변경 * [CHORE] 불필요한 공백 제거 * [FIX] Post, Comment, Like, User 간 불필요한 연관관계 제거 * [FIX] post 데이터 가져올 때 innerJoin 방식으로 수정 * [TEST] Post의 comment 수와 like 수 INSERT 시 세팅하도록 수정 * [FIX] PostDetail DTO 내부 update 메서드 제거 --- docker-compose.yml | 24 +- .../main/common/config/JpaAuditingConfig.java | 3 +- .../crew/main/entity/comment/Comment.java | 242 +++++++++-------- .../makers/crew/main/entity/like/Like.java | 42 +-- .../crew/main/entity/like/LikeRepository.java | 6 + .../makers/crew/main/entity/post/Post.java | 243 +++++++++--------- .../crew/main/entity/post/PostRepository.java | 12 +- .../entity/post/PostSearchRepository.java | 10 + .../entity/post/PostSearchRepositoryImpl.java | 129 ++++++++++ .../makers/crew/main/entity/user/User.java | 17 +- .../makers/crew/main/post/v2/PostV2Api.java | 37 +++ .../crew/main/post/v2/PostV2Controller.java | 26 +- .../v2/dto/query/PostGetPostsCommand.java | 19 ++ .../v2/dto/response/CommenterThumbnails.java | 11 + .../v2/dto/response/PostDetailBaseDto.java | 38 +++ .../dto/response/PostDetailResponseDto.java | 42 +++ .../post/v2/dto/response/PostMeetingDto.java | 19 ++ .../response/PostV2GetPostsResponseDto.java | 14 + .../v2/dto/response/PostWriterInfoDto.java | 20 ++ .../main/post/v2/service/PostV2Service.java | 6 +- .../post/v2/service/PostV2ServiceImpl.java | 135 +++++----- .../crew/main/common/config/TestConfig.java | 5 +- .../v2/repository/PostRepositoryTest.java | 131 ++++++++++ .../sql/post-repository-test-data.sql | 82 ++++++ server/src/post/v1/post-v1.controller.ts | 1 + 25 files changed, 914 insertions(+), 400 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepository.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepositoryImpl.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/CommenterThumbnails.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailBaseDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailResponseDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostMeetingDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostV2GetPostsResponseDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostWriterInfoDto.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/post/v2/repository/PostRepositoryTest.java create mode 100644 main/src/test/resources/sql/post-repository-test-data.sql diff --git a/docker-compose.yml b/docker-compose.yml index 0aa9cefe..445d7d69 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -97,18 +97,12 @@ services: caddy.route_9.reverse_proxy: "{{ upstreams 3000 }}" caddy.route_10: /notice/v1/* caddy.route_10.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_11: /post/v1 + caddy.route_11: /post/v1/* caddy.route_11.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_12: /post/v1/* + caddy.route_12: /users caddy.route_12.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_13: /users + caddy.route_13: /users/* caddy.route_13.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_14: /users/* - caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_15: /notice/v2 - caddy.route_15.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_16: /notice/v2/* - caddy.route_16.reverse_proxy: "{{ upstreams 3000 }}" spring-green: image: makerscrew/main:latest @@ -194,18 +188,12 @@ services: caddy.route_9.reverse_proxy: "{{ upstreams 3000 }}" caddy.route_10: /notice/v1/* caddy.route_10.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_11: /post/v1 + caddy.route_11: /post/v1/* caddy.route_11.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_12: /post/v1/* + caddy.route_12: /users caddy.route_12.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_13: /users + caddy.route_13: /users/* caddy.route_13.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_14: /users/* - caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_15: /notice/v2 - caddy.route_15.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_16: /notice/v2/* - caddy.route_16.reverse_proxy: "{{ upstreams 3000 }}" spring-blue: image: makerscrew/main:latest diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java b/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java index 5c83ce72..ef4a1927 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java @@ -1,5 +1,6 @@ package org.sopt.makers.crew.main.common.config; +import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -15,6 +16,6 @@ public class JpaAuditingConfig { @Bean public JPAQueryFactory queryFactory() { - return new JPAQueryFactory(entityManager); + return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager); } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index 48832145..b19d2ab7 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -13,13 +13,11 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.sopt.makers.crew.main.entity.like.Like; import org.sopt.makers.crew.main.entity.post.Post; import org.sopt.makers.crew.main.entity.report.Report; import org.sopt.makers.crew.main.entity.user.User; @@ -34,129 +32,119 @@ @Table(name = "comment") public class Comment { - /** - * 댓글의 고유 식별자 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; - - /** - * 댓글 내용 - */ - @Column(nullable = false) - private String contents; - - /** - * 댓글 깊이 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int depth; - - /** - * 댓글 순서 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int order; - - /** - * 작성일 - */ - @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") - @CreatedDate - private LocalDateTime createdDate; - - /** - * 수정일 - */ - @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") - @LastModifiedDate - private LocalDateTime updatedDate; - - /** - * 작성자 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 작성자의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int userId; - - /** - * 댓글이 속한 게시글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "postId", nullable = false) - private Post post; - - /** - * 댓글이 속한 게시글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int postId; - - /** - * 댓글에 대한 좋아요 목록 - */ - @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) - private List likes = new ArrayList<>(); - - /** - * 댓글에 대한 좋아요 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int likeCount; - - /** - * 부모 댓글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parentId") - private Comment parent; - - /** - * 부모 댓글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int parentId; - - /** - * 자식 댓글 목록 - */ - @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE) - private List children; - - /** - * 댓글에 대한 신고 목록 - */ - @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) - private List reports; - - @Builder - public Comment(String contents, User user, Post post, Comment parent) { - this.contents = contents; - this.user = user; - this.post = post; - this.parent = parent; - this.depth = 0; - this.order = 0; - this.likeCount = 0; - this.post.addComment(this); - } - - public void addLike(Like like) { - this.likes.add(like); - } - - public void addChildrenComment(Comment comment) { - this.children.add(comment); - } - - public void addReport(Report report) { - this.reports.add(report); - } + /** + * 댓글의 고유 식별자 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + /** + * 댓글 내용 + */ + @Column(nullable = false) + private String contents; + + /** + * 댓글 깊이 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int depth; + + /** + * 댓글 순서 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int order; + + /** + * 작성일 + */ + @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime createdDate; + + /** + * 수정일 + */ + @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") + @LastModifiedDate + private LocalDateTime updatedDate; + + /** + * 작성자 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + /** + * 작성자의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private int userId; + + /** + * 댓글이 속한 게시글 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "postId", nullable = false) + private Post post; + + /** + * 댓글이 속한 게시글의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private int postId; + + /** + * 댓글에 대한 좋아요 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int likeCount; + + /** + * 부모 댓글 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parentId") + private Comment parent; + + /** + * 부모 댓글의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private int parentId; + + /** + * 자식 댓글 목록 + */ + @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE) + private List children; + + /** + * 댓글에 대한 신고 목록 + */ + @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) + private List reports; + + @Builder + public Comment(String contents, User user, Post post, Comment parent) { + this.contents = contents; + this.user = user; + this.post = post; + this.parent = parent; + this.depth = 0; + this.order = 0; + this.likeCount = 0; + this.post.addComment(this); + } + + public void addChildrenComment(Comment comment) { + this.children.add(comment); + } + + public void addReport(Report report) { + this.reports.add(report); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/like/Like.java b/main/src/main/java/org/sopt/makers/crew/main/entity/like/Like.java index 984065b6..345f93ff 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/like/Like.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/like/Like.java @@ -3,21 +3,15 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.sopt.makers.crew.main.entity.comment.Comment; -import org.sopt.makers.crew.main.entity.post.Post; -import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -32,7 +26,7 @@ public class Like { */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; + private Integer id; /** * 좋아요 누른 날짜 @@ -41,53 +35,29 @@ public class Like { @CreatedDate private LocalDateTime createdDate; - /** - * 좋아요 누른사람 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - /** * 좋아요 누른사람 id */ @Column(insertable = false, updatable = false) - private int userId; - - /** - * 게시글 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "postId") - private Post post; + private Integer userId; /** * 게시글 id - 게시글 좋아요가 아닐 경우 null */ @Column(insertable = false, updatable = false) - private int postId; - - /** - * 댓글 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "commentId") - private Comment comment; + private Integer postId; /** * 댓글 id - 댓글 좋아요가 아닐 경우 null */ @Column(insertable = false, updatable = false) - private int commentId; + private Integer commentId; @Builder - public Like(User user, int userId, Post post, int postId, Comment comment, - int commentId) { - this.user = user; + public Like(Integer userId, Integer postId, Integer commentId) { this.userId = userId; - this.post = post; this.postId = postId; - this.comment = comment; this.commentId = commentId; } + } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java new file mode 100644 index 00000000..0870c8ba --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java @@ -0,0 +1,6 @@ +package org.sopt.makers.crew.main.entity.like; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LikeRepository extends JpaRepository { +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java index ef8bc778..aebe4b2a 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java @@ -22,7 +22,6 @@ import lombok.NoArgsConstructor; import org.hibernate.annotations.Type; import org.sopt.makers.crew.main.entity.comment.Comment; -import org.sopt.makers.crew.main.entity.like.Like; import org.sopt.makers.crew.main.entity.meeting.Meeting; import org.sopt.makers.crew.main.entity.report.Report; import org.sopt.makers.crew.main.entity.user.User; @@ -37,130 +36,120 @@ @Table(name = "post") public class Post { - /** - * 게시글의 고유 식별자 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - /** - * 게시글 제목 - */ - @Column(nullable = false) - private String title; - - /** - * 게시글 내용 - */ - @Column(nullable = false, columnDefinition = "TEXT") - private String contents; - - /** - * 게시글 작성일 - */ - @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") - @CreatedDate - private LocalDateTime createdDate; - - /** - * 게시글 수정일 - */ - @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") - @LastModifiedDate - private LocalDateTime updatedDate; - - /** - * 조회수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int viewCount; - - /** - * 이미지 리스트 - */ - @Column(name = "images", columnDefinition = "text[]") - @Type(StringArrayType.class) - private String[] images; - - /** - * 작성자 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 작성자의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer userId; - - /** - * 게시글이 속한 미팅 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "meetingId", nullable = false) - private Meeting meeting; - - /** - * 게시글이 속한 미팅의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer meetingId; - - /** - * 게시글에 달린 댓글 목록 - */ - @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) - private List comments = new ArrayList<>(); - - /** - * 게시글에 달린 댓글 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int commentCount; - - /** - * 게시글에 대한 좋아요 목록 - */ - @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) - private List likes = new ArrayList<>(); - - /** - * 게시글에 대한 좋아요 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int likeCount; - - /** - * 게시글에 대한 신고 목록 - */ - @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) - private List reports; - - @Builder - public Post(String title, String contents, String[] images, User user, Meeting meeting) { - this.title = title; - this.contents = contents; - this.viewCount = 0; - this.images = images; - this.user = user; - this.meeting = meeting; - this.commentCount = 0; - this.likeCount = 0; - } - - public void addLike(Like like) { - this.likes.add(like); - } - - public void addComment(Comment comment) { - this.comments.add(comment); - this.commentCount++; - } - - public void addReport(Report report) { - this.reports.add(report); - } + /** + * 게시글의 고유 식별자 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 게시글 제목 + */ + @Column(nullable = false) + private String title; + + /** + * 게시글 내용 + */ + @Column(nullable = false, columnDefinition = "TEXT") + private String contents; + + /** + * 게시글 작성일 + */ + @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime createdDate; + + /** + * 게시글 수정일 + */ + @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") + @LastModifiedDate + private LocalDateTime updatedDate; + + /** + * 조회수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int viewCount; + + /** + * 이미지 리스트 + */ + @Column(name = "images", columnDefinition = "text[]") + @Type(StringArrayType.class) + private String[] images; + + /** + * 작성자 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + /** + * 작성자의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer userId; + + /** + * 게시글이 속한 미팅 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "meetingId", nullable = false) + private Meeting meeting; + + /** + * 게시글이 속한 미팅의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer meetingId; + + /** + * 게시글에 달린 댓글 목록 + */ + @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) + private List comments = new ArrayList<>(); + + /** + * 게시글에 달린 댓글 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int commentCount; + + /** + * 게시글에 대한 좋아요 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int likeCount; + + /** + * 게시글에 대한 신고 목록 + */ + @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) + private List reports; + + @Builder + public Post(String title, String contents, String[] images, User user, Meeting meeting) { + this.title = title; + this.contents = contents; + this.viewCount = 0; + this.images = images; + this.user = user; + this.meeting = meeting; + this.commentCount = 0; + this.likeCount = 0; + } + + public void addComment(Comment comment) { + this.comments.add(comment); + this.commentCount++; + } + + public void addReport(Report report) { + this.reports.add(report); + } } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java index 816719c7..c6cab8ff 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java @@ -6,12 +6,12 @@ import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.springframework.data.jpa.repository.JpaRepository; -public interface PostRepository extends JpaRepository { +public interface PostRepository extends JpaRepository, PostSearchRepository { - Optional findById(Integer postId); + Optional findById(Integer postId); - default Post findByIdOrThrow(Integer postId) { - return findById(postId) - .orElseThrow(() -> new BadRequestException(NOT_FOUND_POST.getErrorCode())); - } + default Post findByIdOrThrow(Integer postId) { + return findById(postId) + .orElseThrow(() -> new BadRequestException(NOT_FOUND_POST.getErrorCode())); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepository.java new file mode 100644 index 00000000..9fbf50a6 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepository.java @@ -0,0 +1,10 @@ +package org.sopt.makers.crew.main.entity.post; + +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface PostSearchRepository { + Page findPostList(PostGetPostsCommand queryCommand, Pageable pageable, Integer userId); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepositoryImpl.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepositoryImpl.java new file mode 100644 index 00000000..befc4d88 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepositoryImpl.java @@ -0,0 +1,129 @@ +package org.sopt.makers.crew.main.entity.post; + +import static org.sopt.makers.crew.main.entity.comment.QComment.comment; +import static org.sopt.makers.crew.main.entity.like.QLike.like; +import static org.sopt.makers.crew.main.entity.meeting.QMeeting.meeting; +import static org.sopt.makers.crew.main.entity.post.QPost.post; +import static org.sopt.makers.crew.main.entity.user.QUser.user; + +import com.querydsl.core.group.GroupBy; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; +import org.sopt.makers.crew.main.post.v2.dto.response.CommenterThumbnails; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailBaseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.QPostDetailBaseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.QPostMeetingDto; +import org.sopt.makers.crew.main.post.v2.dto.response.QPostWriterInfoDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class PostSearchRepositoryImpl implements PostSearchRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Page findPostList(PostGetPostsCommand queryCommand, Pageable pageable, + Integer userId) { + Integer meetingId = queryCommand.getMeetingId().orElse(null); + + List content = getContent(pageable, meetingId, userId); + JPAQuery countQuery = getCount(meetingId); + + return PageableExecutionUtils.getPage(content, + PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()), countQuery::fetchFirst); + } + + private List getContent(Pageable pageable, Integer meetingId, + Integer userId) { + List responseDtos = new ArrayList<>(); + + List postDetailList = queryFactory + .select(new QPostDetailBaseDto( + post.id, + post.title, + post.contents, + post.createdDate, + post.images, + new QPostWriterInfoDto( + post.user.id, + post.user.orgId, + post.user.name, + post.user.profileImage + ), + post.likeCount, + ExpressionUtils.as( + JPAExpressions.selectFrom(like) + .where(like.postId.eq(post.id).and(like.userId.eq(userId))) + .exists() + , "isLiked" + ), + post.viewCount, + post.commentCount, + new QPostMeetingDto( + post.meeting.id, + post.meeting.title, + post.meeting.category + ) + )) + .from(post) + .innerJoin(post.user, user) + .innerJoin(post.meeting, meeting) + .where(meetingIdEq(meetingId)) + .orderBy(post.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // 모든 게시글 ID를 추출 + List postIds = postDetailList.stream() + .map(PostDetailBaseDto::getId) + .collect(Collectors.toList()); + + // 게시글 ID 리스트를 사용하여 한 번에 모든 댓글 작성자의 프로필 이미지를 조회 + Map> commenterThumbnailsMap = queryFactory + .select(comment.post.id, comment.user.profileImage) + .from(comment) + .where(comment.post.id.in(postIds)) + .groupBy(comment.post.id, comment.user.id, comment.user.profileImage) + .orderBy(comment.post.id.asc(), comment.user.id.asc()) + .limit(3) + .transform(GroupBy.groupBy(comment.post.id).as(GroupBy.list(comment.user.profileImage))); + + // 각 게시글별로 댓글 작성자의 프로필 이미지 리스트를 설정 + for (PostDetailBaseDto postDetail : postDetailList) { + CommenterThumbnails commenterThumbnails = new CommenterThumbnails( + commenterThumbnailsMap.getOrDefault(postDetail.getId(), + Collections.emptyList())); + responseDtos.add(PostDetailResponseDto.of(postDetail, commenterThumbnails)); + } + + return responseDtos; + } + + private JPAQuery getCount(Integer meetingId) { + return queryFactory + .select(post.count()) + .from(post) + .where(meetingIdEq(meetingId)); + } + + private BooleanExpression meetingIdEq(Integer meetingId) { + return meetingId == null ? null : post.meetingId.eq(meetingId); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java index e2b19fec..0e3780bb 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java @@ -19,7 +19,6 @@ import lombok.NoArgsConstructor; import org.hibernate.annotations.Type; import org.sopt.makers.crew.main.entity.apply.Apply; -import org.sopt.makers.crew.main.entity.like.Like; import org.sopt.makers.crew.main.entity.meeting.Meeting; import org.sopt.makers.crew.main.entity.post.Post; import org.sopt.makers.crew.main.entity.report.Report; @@ -54,7 +53,7 @@ public class User { /** * 활동 목록 */ - @Column(name = "activities",columnDefinition = "jsonb") + @Column(name = "activities", columnDefinition = "jsonb") @Type(JsonBinaryType.class) private List activities; @@ -88,12 +87,6 @@ public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) private List posts = new ArrayList<>(); - /** - * 좋아요 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private List likes = new ArrayList<>(); - /** * 신고 내역 */ @@ -118,13 +111,11 @@ public void addApply(Apply apply) { this.applies.add(apply); } - public void addLike(Like like) { - this.likes.add(like); - } - public void addReport(Report report) { this.reports.add(report); } - public void setUserIdForTest(Integer userId){ this.id = userId;} + public void setUserIdForTest(Integer userId) { + this.id = userId; + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java new file mode 100644 index 00000000..65a42dba --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java @@ -0,0 +1,37 @@ +package org.sopt.makers.crew.main.post.v2; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 java.security.Principal; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "게시글") +public interface PostV2Api { + + @Operation(summary = "모임 게시글 작성") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), + @ApiResponse(responseCode = "403", description = "권한이 없습니다.", content = @Content), + }) + ResponseEntity createPost( + @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal); + + @Operation(summary = "모임 게시글 목록 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), + }) + ResponseEntity getPosts( + @ModelAttribute PostGetPostsCommand queryCommand, Principal principal); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java index 8f075c2f..1e844725 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java @@ -1,19 +1,19 @@ package org.sopt.makers.crew.main.post.v2; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -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 java.security.Principal; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.util.UserUtil; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; import org.sopt.makers.crew.main.post.v2.service.PostV2Service; import org.springframework.http.HttpStatus; 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; @@ -24,21 +24,25 @@ @RequestMapping("/post/v2") @RequiredArgsConstructor @Tag(name = "게시글") -public class PostV2Controller { +public class PostV2Controller implements PostV2Api { private final PostV2Service postV2Service; - @Operation(summary = "모임 게시글 작성") + @Override @PostMapping() @ResponseStatus(HttpStatus.CREATED) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.", content = @Content), - }) public ResponseEntity createPost( @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal) { Integer userId = UserUtil.getUserId(principal); return ResponseEntity.ok(postV2Service.createPost(requestBody, userId)); } + + @Override + @GetMapping() + @ResponseStatus(HttpStatus.OK) + public ResponseEntity getPosts(@ModelAttribute PostGetPostsCommand queryCommand, + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(postV2Service.getPosts(queryCommand, userId)); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java new file mode 100644 index 00000000..7127ca6a --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.post.v2.dto.query; + +import java.util.Optional; +import lombok.Builder; +import lombok.Getter; +import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; + +@Getter +public class PostGetPostsCommand extends PageOptionsDto { + + private Optional meetingId; + + @Builder + public PostGetPostsCommand(Integer meetingId, Integer page, Integer take) { + super(page, take); + this.meetingId = Optional.ofNullable(meetingId); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/CommenterThumbnails.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/CommenterThumbnails.java new file mode 100644 index 00000000..bff64f31 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/CommenterThumbnails.java @@ -0,0 +1,11 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class CommenterThumbnails { + private final List commenterThumbnails; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailBaseDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailBaseDto.java new file mode 100644 index 00000000..927fe64b --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailBaseDto.java @@ -0,0 +1,38 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +public class PostDetailBaseDto { + private final Integer id; + private final String title; + private final String contents; + private final LocalDateTime createdDate; + private final String[] images; + private final PostWriterInfoDto user; + private final int likeCount; + //* 본인이 좋아요를 눌렀는지 여부 + private final Boolean isLiked; + private final int viewCount; + private final int commentCount; + private final PostMeetingDto meeting; + + @QueryProjection + public PostDetailBaseDto(Integer id, String title, String contents, LocalDateTime createdDate, String[] images, + PostWriterInfoDto user, int likeCount, boolean isLiked, int viewCount, int commentCount, + PostMeetingDto meeting) { + this.id = id; + this.title = title; + this.contents = contents; + this.createdDate = createdDate; + this.images = images; + this.user = user; + this.likeCount = likeCount; + this.isLiked = isLiked; + this.viewCount = viewCount; + this.commentCount = commentCount; + this.meeting = meeting; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailResponseDto.java new file mode 100644 index 00000000..21061cc1 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailResponseDto.java @@ -0,0 +1,42 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class PostDetailResponseDto { + private final Integer id; + private final String title; + private final String contents; + private final LocalDateTime createdDate; + private final String[] images; + private final PostWriterInfoDto user; + private final int likeCount; + private final Boolean isLiked; + private final int viewCount; + private final int commentCount; + private final PostMeetingDto meeting; + private final List commenterThumbnails; + + public static PostDetailResponseDto of(PostDetailBaseDto postDetail, + CommenterThumbnails postTopCommenterThumbnails) { + List thumbnails = postTopCommenterThumbnails.getCommenterThumbnails(); + return PostDetailResponseDto.of( + postDetail.getId(), + postDetail.getTitle(), + postDetail.getContents(), + postDetail.getCreatedDate(), + postDetail.getImages(), + postDetail.getUser(), + postDetail.getLikeCount(), + postDetail.getIsLiked(), + postDetail.getViewCount(), + postDetail.getCommentCount(), + postDetail.getMeeting(), + thumbnails + ); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostMeetingDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostMeetingDto.java new file mode 100644 index 00000000..d22cbd71 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostMeetingDto.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; + +@Getter +public class PostMeetingDto { + private final Integer id; + private final String title; + private final String category; + + @QueryProjection + public PostMeetingDto(Integer id, String title, MeetingCategory category) { + this.id = id; + this.title = title; + this.category = category.getValue(); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostV2GetPostsResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostV2GetPostsResponseDto.java new file mode 100644 index 00000000..e414007b --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostV2GetPostsResponseDto.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; + +@Getter +@AllArgsConstructor(staticName = "of") +public class PostV2GetPostsResponseDto { + + private final List posts; + private final PageMetaDto meta; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostWriterInfoDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostWriterInfoDto.java new file mode 100644 index 00000000..84a6834a --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostWriterInfoDto.java @@ -0,0 +1,20 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; + +@Getter +public class PostWriterInfoDto { + private final Integer id; + private final Integer orgId; + private final String name; + private final String profileImage; + + @QueryProjection + public PostWriterInfoDto(Integer id, Integer orgId, String name, String profileImage) { + this.id = id; + this.orgId = orgId; + this.name = name; + this.profileImage = profileImage; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java index e8047305..46bef28f 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java @@ -1,9 +1,13 @@ package org.sopt.makers.crew.main.post.v2.service; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; public interface PostV2Service { - PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, Integer userId); + PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, Integer userId); + + PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java index a00ae877..339b32b2 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java @@ -8,6 +8,8 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.exception.ForbiddenException; +import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; +import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; import org.sopt.makers.crew.main.entity.apply.ApplyRepository; import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; import org.sopt.makers.crew.main.entity.meeting.Meeting; @@ -18,9 +20,14 @@ import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.internal.notification.PushNotificationService; import org.sopt.makers.crew.main.internal.notification.dto.PushNotificationRequestDto; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,66 +36,78 @@ @Transactional(readOnly = true) public class PostV2ServiceImpl implements PostV2Service { - private final MeetingRepository meetingRepository; - private final UserRepository userRepository; - private final PostRepository postRepository; - private final ApplyRepository applyRepository; - private final PushNotificationService pushNotificationService; - - @Value("${push-notification.web-url}") - private String pushWebUrl; - - /** - * 모임 게시글 작성 - * - * @throws 403 모임에 속한 유저가 아닌 경우 - * @apiNote 모임에 속한 유저만 작성 가능 - */ - @Override - @Transactional - public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, - Integer userId) { - Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); - User user = userRepository.findByIdOrThrow(userId); - - boolean isInMeeting = meeting.getAppliedInfo().stream() - .anyMatch(apply -> apply.getUserId().equals(userId) - && apply.getStatus() == EnApplyStatus.APPROVE); - - boolean isMeetingCreator = meeting.getUserId().equals(userId); - - if (isInMeeting == false && isMeetingCreator == false) { - throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); + private final MeetingRepository meetingRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + private final ApplyRepository applyRepository; + private final PushNotificationService pushNotificationService; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + /** + * 모임 게시글 작성 + * + * @throws 403 모임에 속한 유저가 아닌 경우 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, + Integer userId) { + Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); + User user = userRepository.findByIdOrThrow(userId); + + boolean isInMeeting = meeting.getAppliedInfo().stream() + .anyMatch(apply -> apply.getUserId().equals(userId) + && apply.getStatus() == EnApplyStatus.APPROVE); + + boolean isMeetingCreator = meeting.getUserId().equals(userId); + + if (isInMeeting == false && isMeetingCreator == false) { + throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); + } + + Post post = Post.builder() + .title(requestBody.getTitle()) + .user(user) + .contents(requestBody.getContents()) + .images(requestBody.getImages()) + .meeting(meeting) + .build(); + + Post savedPost = postRepository.save(post); + + List userIdList = applyRepository.findAllByMeetingIdAndStatus(meeting.getId(), + EnApplyStatus.APPROVE) + .stream() + .map(apply -> String.valueOf(apply.getUser().getOrgId())) + .collect(toList()); + + String[] userIds = userIdList.toArray(new String[0]); + String pushNotificationContent = String.format("[%s의 새 글] : \"%s\"", + user.getName(), post.getTitle()); + String pushNotificationWeblink = pushWebUrl + "/detail?id=" + meeting.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_POST_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + + return PostV2CreatePostResponseDto.of(savedPost.getId()); } - Post post = Post.builder() - .title(requestBody.getTitle()) - .user(user) - .contents(requestBody.getContents()) - .images(requestBody.getImages()) - .meeting(meeting) - .build(); + @Override + @Transactional(readOnly = true) + public PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId) { + Page meetingPostListDtos = postRepository.findPostList(queryCommand, + PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), userId); - Post savedPost = postRepository.save(post); + PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), queryCommand.getTake()); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, (int) meetingPostListDtos.getTotalElements()); - List userIdList = applyRepository.findAllByMeetingIdAndStatus(meeting.getId(), - EnApplyStatus.APPROVE) - .stream() - .map(apply -> String.valueOf(apply.getUser().getOrgId())) - .collect(toList()); - - String[] userIds = userIdList.toArray(new String[0]); - String pushNotificationContent = String.format("[%s의 새 글] : \"%s\"", - user.getName(), post.getTitle()); - String pushNotificationWeblink = pushWebUrl + "/detail?id=" + meeting.getId(); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_POST_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); - - pushNotificationService.sendPushNotification(pushRequestDto); - - return PostV2CreatePostResponseDto.of(savedPost.getId()); - } + return PostV2GetPostsResponseDto.of(meetingPostListDtos.getContent(), pageMetaDto); + } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java b/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java index b8b89398..c8373182 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java +++ b/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java @@ -1,5 +1,6 @@ package org.sopt.makers.crew.main.common.config; +import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -12,7 +13,7 @@ public class TestConfig { private EntityManager entityManager; @Bean - public JPAQueryFactory jpaQueryFactory(){ - return new JPAQueryFactory(entityManager); + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager); } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/post/v2/repository/PostRepositoryTest.java b/main/src/test/java/org/sopt/makers/crew/main/post/v2/repository/PostRepositoryTest.java new file mode 100644 index 00000000..936d44a0 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/post/v2/repository/PostRepositoryTest.java @@ -0,0 +1,131 @@ +package org.sopt.makers.crew.main.post.v2.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.common.config.TestConfig; +import org.sopt.makers.crew.main.entity.post.PostRepository; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; +import org.springframework.test.context.jdbc.SqlGroup; + +@DataJpaTest +@ActiveProfiles("test") +@Import(TestConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@SqlGroup({ + @Sql(value = "/sql/post-repository-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) + +}) +public class PostRepositoryTest { + @Autowired + private PostRepository postRepository; + + @Test + void 모임_ID로_필터링해서_게시글_목록_조회() { + // given + int page = 1; + int take = 9; + Integer meetingId = 1; + PostGetPostsCommand queryCommand = new PostGetPostsCommand(meetingId, page, take); + Integer userId = 1; + + // when + Page postDetailJsonDtos = postRepository.findPostList(queryCommand, + PageRequest.of(page - 1, take), + userId); + + // then + assertThat(postDetailJsonDtos.getTotalElements()).isEqualTo(3); + assertThat(postDetailJsonDtos.getSize()).isEqualTo(take); + assertThat(postDetailJsonDtos.getNumber()).isEqualTo(page - 1); + assertThat(postDetailJsonDtos.getTotalPages()).isEqualTo(1); + + PostDetailResponseDto postDetailDto1 = postDetailJsonDtos.getContent().get(0); + assertThat(postDetailDto1) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(3, "제목3", "내용3", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 2, 0)), + 0, false, 0); + assertThat(postDetailDto1.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(1, "스터디 구합니다1", "행사"); + + PostDetailResponseDto postDetailDto2 = postDetailJsonDtos.getContent().get(1); + assertThat(postDetailDto2) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(2, "제목2", "내용2", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 1, 0)), + 0, false, 0); + assertThat(postDetailDto2.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(1, "스터디 구합니다1", "행사"); + + PostDetailResponseDto postDetailDto3 = postDetailJsonDtos.getContent().get(2); + assertThat(postDetailDto3) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(1, "제목1", "내용1", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 0, 0)), + 2, true, 5); + assertThat(postDetailDto3.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(1, "스터디 구합니다1", "행사"); + // 댓글 작성자 최대 3명 순서 검증 + assertThat(postDetailDto3.getCommenterThumbnails()) + .containsExactly("profile1.jpg", "profile2.jpg", "profile3.jpg"); + } + + @Test + void 게시글_목록_전체_조회() { + // given + int page = 1; + int take = 9; + PostGetPostsCommand queryCommand = new PostGetPostsCommand(null, page, take); + Integer userId = 1; + + // when + Page postDetailJsonDtos = postRepository.findPostList(queryCommand, + PageRequest.of(page - 1, take), + userId); + + // then + assertThat(postDetailJsonDtos.getTotalElements()).isEqualTo(5); + assertThat(postDetailJsonDtos.getSize()).isEqualTo(take); + assertThat(postDetailJsonDtos.getNumber()).isEqualTo(page - 1); + assertThat(postDetailJsonDtos.getTotalPages()).isEqualTo(1); + + PostDetailResponseDto postDetailDto1 = postDetailJsonDtos.getContent().get(0); + assertThat(postDetailDto1) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(5, "제목5", "내용5", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 4, 0)), + 0, false, 0); + assertThat(postDetailDto1.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(2, "스터디 구합니다2", "스터디"); + + PostDetailResponseDto postDetailDto2 = postDetailJsonDtos.getContent().get(4); + assertThat(postDetailDto2) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(1, "제목1", "내용1", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 0, 0)), + 2, true, 5); + assertThat(postDetailDto2.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(1, "스터디 구합니다1", "행사"); + } + +} diff --git a/main/src/test/resources/sql/post-repository-test-data.sql b/main/src/test/resources/sql/post-repository-test-data.sql new file mode 100644 index 00000000..e2676f94 --- /dev/null +++ b/main/src/test/resources/sql/post-repository-test-data.sql @@ -0,0 +1,82 @@ +CREATE TYPE meeting_joinableparts_enum AS ENUM ('PM', 'DESIGN', 'IOS', 'ANDROID', 'SERVER', 'WEB'); + +INSERT INTO "user" (id, name, "orgId", activities, "profileImage", phone) +VALUES (1, '김삼순', 1001, + '[{"part": "서버", "generation": 33}, {"part": "iOS", "generation": 32}]', + 'profile1.jpg', '010-1234-5678'), + (2, '홍길동', 1002, + '[{"part": "기획", "generation": 32}, {"part": "기획", "generation": 29}, {"part": "기획", "generation": 33}, {"part": "기획", "generation": 30}]', + 'profile2.jpg', '010-1111-2222'), + (3, '김철수', 1003, + '[{"part": "웹", "generation": 34}]', + 'profile3.jpg', '010-3333-4444'), + (4, '이영지', 1004, + '[{"part": "iOS", "generation": 32}, {"part": "안드로이드", "generation": 29}]', + 'profile4.jpg', '010-5555-5555'); + +create table "meeting" ( + "canJoinOnlyActiveGeneration" boolean not null, + "capacity" integer not null, + "createdGeneration" integer not null, + "id" serial not null, + "isMentorNeeded" boolean not null, + "targetActiveGeneration" integer, + "userId" integer, + "endDate" TIMESTAMP not null, + "mEndDate" TIMESTAMP not null, + "mStartDate" TIMESTAMP not null, + "startDate" TIMESTAMP not null, + "category" varchar(255) not null, + "desc" varchar(255) not null, + "leaderDesc" varchar(255) not null, + "note" varchar(255), + "processDesc" varchar(255) not null, + "targetDesc" varchar(255) not null, + "title" varchar(255) not null, + "imageURL" jsonb, + "joinableParts" meeting_joinableparts_enum[], + primary key ("id") +); + +INSERT INTO meeting (id, "userId", title, category, "imageURL", "startDate", "endDate", capacity, + "desc", "processDesc", "mStartDate", "mEndDate", "leaderDesc", "targetDesc", + note, "isMentorNeeded", "canJoinOnlyActiveGeneration", "createdGeneration", + "targetActiveGeneration", "joinableParts") +VALUES (1, 1, '스터디 구합니다1', '행사', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-05-19 00:00:00.000000', '2024-05-24 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-29 00:00:00.000000', '2024-05-31 00:00:00.000000', '스터디장 설명입니다.', + '누구나 들어올 수 있어요.', '시간지키세요.', true, false, 34, 34, '{PM,DESIGN,WEB,ANDROID,IOS,SERVER}'); + +INSERT INTO meeting (id, "userId", title, category, "imageURL", "startDate", "endDate", capacity, + "desc", "processDesc", "mStartDate", "mEndDate", "leaderDesc", "targetDesc", + note, "isMentorNeeded", "canJoinOnlyActiveGeneration", "createdGeneration", + "targetActiveGeneration", "joinableParts") +VALUES (2, 1, '스터디 구합니다2', '스터디', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-05-20 00:00:00.000000', '2024-05-25 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-30 00:00:00.000000', '2024-06-01 00:00:00.000000', '스터디장 설명입니다.', + '누구나 들어올 수 있어요.', '시간지키세요.', true, false, 34, 34, '{PM,DESIGN,WEB,ANDROID,IOS,SERVER}'); + +INSERT INTO post (id, title, contents, "createdDate", "updatedDate", "viewCount", images, "userId", "meetingId", "commentCount", "likeCount") +VALUES + (1, '제목1', '내용1', '2024-06-11 10:00:00', '2024-06-11 10:00:000000', 1, NULL, 1, 1, 5, 2), + (2, '제목2', '내용2', '2024-06-11 10:00:01', '2024-06-11 10:00:000001', 2, NULL, 2, 1, 0, 0), + (3, '제목3', '내용3', '2024-06-11 10:00:02', '2024-06-11 10:00:000002', 3, NULL, 3, 1, 0, 0), + (4, '제목4', '내용4', '2024-06-11 10:00:03', '2024-06-11 10:00:000003', 4, NULL, 2, 2, 0, 0), + (5, '제목5', '내용5', '2024-06-11 10:00:04', '2024-06-11 10:00:000004', 5, NULL, 4, 2, 0, 0); + +INSERT INTO comment (id, contents, depth, "order", "createdDate", "updatedDate", "userId", "postId", "likeCount", "parentId") +VALUES + (1, '첫 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:00', '2024-06-12 10:00:000000', 1, 1, 0, NULL), + (2, '두 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:01', '2024-06-12 10:00:000001', 2, 1, 0, NULL), + (3, '세 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:02', '2024-06-12 10:00:000002', 2, 1, 0, NULL), + (4, '네 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:03', '2024-06-12 10:00:000003', 3, 1, 0, NULL), + (5, '다섯 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:04', '2024-06-12 10:00:000004', 4, 1, 0, NULL); + +INSERT INTO "like" (id, "createdDate", "userId", "postId") +VALUES + (1, '2024-06-12 11:00:00', 1, 1), + (2, '2024-06-12 11:00:01', 2, 1); diff --git a/server/src/post/v1/post-v1.controller.ts b/server/src/post/v1/post-v1.controller.ts index 35d64bb1..dca431f6 100644 --- a/server/src/post/v1/post-v1.controller.ts +++ b/server/src/post/v1/post-v1.controller.ts @@ -63,6 +63,7 @@ export class PostV1Controller { @ApiOperation({ summary: '모임 게시글 목록 조회', + deprecated: true, }) @ApiOkResponseCommon(PostV1GetPostsResponseDto) @ApiResponse({ From 7c278a8eac6a8fe57ac0c1fa87a73bed145c6834 Mon Sep 17 00:00:00 2001 From: Yeseul Jo <68415644+yeseul106@users.noreply.github.com> Date: Wed, 26 Jun 2024 00:53:02 +0900 Subject: [PATCH 09/35] =?UTF-8?q?[FIX]=20Open=20API=20Spec=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=EC=84=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EA=B5=AC=EC=A1=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#232)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/makers/crew/main/post/v2/PostV2Api.java | 7 ++++++- .../sopt/makers/crew/main/post/v2/PostV2Controller.java | 6 ++++-- .../crew/main/post/v2/dto/query/PostGetPostsCommand.java | 2 -- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java index 65a42dba..93b47572 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java @@ -1,6 +1,8 @@ package org.sopt.makers.crew.main.post.v2; 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.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -32,6 +34,9 @@ ResponseEntity createPost( @ApiResponse(responseCode = "200", description = "성공"), @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), }) + @Parameters({@Parameter(name = "page", description = "페이지, default = 1", example = "1"), + @Parameter(name = "take", description = "가져올 데이터 개수, default = 12", example = "50"), + @Parameter(name = "meetingId", description = "모임 id", example = "0")}) ResponseEntity getPosts( - @ModelAttribute PostGetPostsCommand queryCommand, Principal principal); + @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, Principal principal); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java index 1e844725..b3c9ebbe 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java @@ -1,5 +1,6 @@ package org.sopt.makers.crew.main.post.v2; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.security.Principal; @@ -40,8 +41,9 @@ public ResponseEntity createPost( @Override @GetMapping() @ResponseStatus(HttpStatus.OK) - public ResponseEntity getPosts(@ModelAttribute PostGetPostsCommand queryCommand, - Principal principal) { + public ResponseEntity getPosts( + @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, + Principal principal) { Integer userId = UserUtil.getUserId(principal); return ResponseEntity.ok(postV2Service.getPosts(queryCommand, userId)); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java index 7127ca6a..e84ef927 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java @@ -1,7 +1,6 @@ package org.sopt.makers.crew.main.post.v2.dto.query; import java.util.Optional; -import lombok.Builder; import lombok.Getter; import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; @@ -10,7 +9,6 @@ public class PostGetPostsCommand extends PageOptionsDto { private Optional meetingId; - @Builder public PostGetPostsCommand(Integer meetingId, Integer page, Integer take) { super(page, take); this.meetingId = Optional.ofNullable(meetingId); From eb8d5e7c75248c4bf0ed144b094766b209a68641 Mon Sep 17 00:00:00 2001 From: Yeseul Jo <68415644+yeseul106@users.noreply.github.com> Date: Wed, 26 Jun 2024 01:22:06 +0900 Subject: [PATCH 10/35] =?UTF-8?q?[FIX]=20Open=20API=20Spec=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=EC=84=9C=20=EC=BF=BC=EB=A6=AC=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=ED=83=80=EC=9E=85=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=20(#234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/makers/crew/main/post/v2/PostV2Api.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java index 93b47572..6a4c2da2 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -34,9 +35,10 @@ ResponseEntity createPost( @ApiResponse(responseCode = "200", description = "성공"), @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), }) - @Parameters({@Parameter(name = "page", description = "페이지, default = 1", example = "1"), - @Parameter(name = "take", description = "가져올 데이터 개수, default = 12", example = "50"), - @Parameter(name = "meetingId", description = "모임 id", example = "0")}) + @Parameters({ + @Parameter(name = "page", description = "페이지, default = 1", example = "1", schema = @Schema(type = "integer", format = "int32")), + @Parameter(name = "take", description = "가져올 데이터 개수, default = 12", example = "50", schema = @Schema(type = "integer", format = "int32")), + @Parameter(name = "meetingId", description = "모임 id", example = "0", schema = @Schema(type = "integer", format = "int32"))}) ResponseEntity getPosts( @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, Principal principal); } From bb5592993c61be54d284c7e6d820e296ed64ca33 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Sat, 29 Jun 2024 19:33:24 +0900 Subject: [PATCH 11/35] =?UTF-8?q?[CHORE]=20id=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/main/notice/dto/response/NoticeV2GetResponseDto.java | 1 + .../sopt/makers/crew/main/notice/service/NoticeV2Service.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java index 1d74f59c..05a1615c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java @@ -7,6 +7,7 @@ @Getter @RequiredArgsConstructor(staticName = "of") public class NoticeV2GetResponseDto { + private final Integer id; private final String title; private final String subTitle; private final String contents; diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java index 90d2bc46..96cc326b 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java @@ -29,7 +29,7 @@ public List getNotices() { List notices = noticeRepository.findByExposeStartDateBeforeAndExposeEndDateAfter(LocalDateTime.now(), LocalDateTime.now()); return notices.stream() - .map(notice -> NoticeV2GetResponseDto.of(notice.getTitle(), notice.getSubTitle(), notice.getContents(), + .map(notice -> NoticeV2GetResponseDto.of(notice.getId(), notice.getTitle(), notice.getSubTitle(), notice.getContents(), notice.getCreatedDate())) .toList(); } From dbd50432b7dd04475e3dbc2c9fd893f7bb24265c Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Sun, 30 Jun 2024 20:19:03 +0900 Subject: [PATCH 12/35] =?UTF-8?q?[FIX]=20Comment=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C=20=EB=93=B1=EB=A1=9D=EB=90=9C=20Exception=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8D=98=EC=A7=80=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 미등록된 예외 대신, 정의된 예외 사용 - 401 코드 리턴 대신 의미가 명확한 403 코드 리턴 - javadoc spec에 맞게 주석 변경 ref. https://github.com/sopt-makers/sopt-crew-backend/pull/218#discussion_r1646810560 --- .../main/comment/v2/CommentV2Controller.java | 18 +++++ .../comment/v2/service/CommentV2Service.java | 2 + .../v2/service/CommentV2ServiceImpl.java | 22 +++++- .../crew/main/entity/comment/Comment.java | 22 +++--- .../entity/comment/CommentRepository.java | 4 + .../makers/crew/main/entity/post/Post.java | 10 ++- .../v2/service/CommentV2ServiceTest.java | 75 +++++++++++++++++++ .../src/comment/v1/comment-v1.controller.ts | 4 + 8 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java index e40c5b27..1dbd1347 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java @@ -13,6 +13,8 @@ import org.sopt.makers.crew.main.common.util.UserUtil; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +41,20 @@ public ResponseEntity createComment( return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); } + @Operation(summary = "모임 게시글 댓글 삭제") + @DeleteMapping("/{commentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "성공"), + }) + public ResponseEntity deleteComment( + Principal principal, + @PathVariable Integer commentId) { + Integer userId = UserUtil.getUserId(principal); + + commentV2Service.deleteComment(commentId, userId); + + return ResponseEntity.noContent().build(); + } + } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index c2a7e78a..b1fed6b4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -7,4 +7,6 @@ public interface CommentV2Service { CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, Integer userId); + + void deleteComment(Integer commentId, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index dbbb5d7a..29571303 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -39,7 +39,6 @@ public class CommentV2ServiceImpl implements CommentV2Service { */ @Override @Transactional - public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, Integer userId) { Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); @@ -69,4 +68,25 @@ public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBod return CommentV2CreateCommentResponseDto.of(savedComment.getId()); } + + /** + * 모임 게시글 댓글 삭제 + * + * @throws 400 댓글 작성자가 아닐 때 + * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 + */ + @Override + @Transactional + public void deleteComment(Integer commentId, Integer userId) { + Comment comment = commentRepository.findByIdOrThrow(commentId); + + if (!comment.getUser().getId().equals(userId)) { + throw new SecurityException("댓글 작성자만 삭제할 수 있습니다."); + } + + Post post = comment.getPost(); + + post.decreaseCommentCount(); + commentRepository.delete(comment); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index b19d2ab7..6f98cf26 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -32,12 +32,12 @@ @Table(name = "comment") public class Comment { - /** - * 댓글의 고유 식별자 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; + /** + * 댓글의 고유 식별자 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; /** * 댓글 내용 @@ -110,11 +110,11 @@ public class Comment { @JoinColumn(name = "parentId") private Comment parent; - /** - * 부모 댓글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int parentId; + /** + * 부모 댓글의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer parentId; /** * 자식 댓글 목록 diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java index 45e789e3..8926bf88 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java @@ -4,4 +4,8 @@ public interface CommentRepository extends JpaRepository { + default Comment findByIdOrThrow(Integer commentId) { + return findById(commentId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 댓글입니다.")); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java index aebe4b2a..0a8f3d5c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java @@ -149,7 +149,11 @@ public void addComment(Comment comment) { this.commentCount++; } - public void addReport(Report report) { - this.reports.add(report); - } + public void addReport(Report report) { + this.reports.add(report); + } + + public void decreaseCommentCount() { + this.commentCount--; + } } \ No newline at end of file diff --git a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java new file mode 100644 index 00000000..efb4d8c2 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java @@ -0,0 +1,75 @@ +package org.sopt.makers.crew.main.comment.v2.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; + +import java.util.Optional; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.entity.comment.Comment; +import org.sopt.makers.crew.main.entity.comment.CommentRepository; +import org.sopt.makers.crew.main.entity.post.Post; +import org.sopt.makers.crew.main.entity.post.PostRepository; +import org.sopt.makers.crew.main.entity.user.User; +import org.sopt.makers.crew.main.entity.user.UserFixture; +import org.sopt.makers.crew.main.entity.user.UserRepository; + +@ExtendWith(MockitoExtension.class) +public class CommentV2ServiceTest { + @InjectMocks + private CommentV2ServiceImpl commentV2Service; + @Mock + private CommentRepository commentRepository; + @Mock + private PostRepository postRepository; + + private Comment comment; + + private Post post; + + private User user; + + @BeforeEach + void init() { + user = UserFixture.createStaticUser(); + user.setUserIdForTest(1); + + String[] images = {"image1", "image2", "image3"}; + this.post = Post.builder().user(user).title("title").contents("contents").images(images).build(); + this.comment = Comment.builder().contents("contents").post(post).user(user).build(); + post.addComment(this.comment); + } + + @Test + void 댓글_삭제_성공() { + // given + int initialCommentCount = post.getCommentCount(); + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + + // when + commentV2Service.deleteComment(comment.getId(), comment.getUser().getId()); + + // then + Assertions.assertThat(commentRepository.findById(comment.getId())).isEqualTo(Optional.empty()); + Assertions.assertThat(post.getCommentCount()).isEqualTo(initialCommentCount - 1); + } + + @Test + void 댓글_삭제_실패_본인_작성_댓글_아님() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + + // when & then + assertThrows(SecurityException.class, () -> { + commentV2Service.deleteComment(comment.getId(), comment.getUser().getId() + 1); + }); + } +} diff --git a/server/src/comment/v1/comment-v1.controller.ts b/server/src/comment/v1/comment-v1.controller.ts index 38a1e3ab..47d06e9a 100644 --- a/server/src/comment/v1/comment-v1.controller.ts +++ b/server/src/comment/v1/comment-v1.controller.ts @@ -137,8 +137,12 @@ export class CommentV1Controller { }); } + /** + * @deprecated + */ @ApiOperation({ summary: '모임 게시글 댓글 삭제', + deprecated: true, }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, From f8d84127324a34a2eed80d0e91fca3b421ccdce6 Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Sat, 22 Jun 2024 13:43:49 +0900 Subject: [PATCH 13/35] =?UTF-8?q?[FIX]=20Comment=20UserId=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타입을 맞추기 위해 entity의 타입을 int를 Integer로 변경 - foreign constraint를 제거하기로 한 결정을 통해 간접참조 형식으로 변경 ref. https://github.com/sopt-makers/sopt-crew-backend/pull/218#discussion_r1646806690 --- .../main/comment/v2/service/CommentV2ServiceImpl.java | 2 +- .../sopt/makers/crew/main/entity/comment/Comment.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index 29571303..b73de4ab 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -80,7 +80,7 @@ public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBod public void deleteComment(Integer commentId, Integer userId) { Comment comment = commentRepository.findByIdOrThrow(commentId); - if (!comment.getUser().getId().equals(userId)) { + if (!comment.getUserId().equals(userId)) { throw new SecurityException("댓글 작성자만 삭제할 수 있습니다."); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index 6f98cf26..3561f86c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -78,11 +78,11 @@ public class Comment { @JoinColumn(name = "userId", nullable = false) private User user; - /** - * 작성자의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int userId; + /** + * 작성자의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer userId; /** * 댓글이 속한 게시글 정보 From 4aa45cb2348e631c7add6d914a421785d3c119f8 Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Sat, 22 Jun 2024 14:00:08 +0900 Subject: [PATCH 14/35] =?UTF-8?q?[FIX]=20Comment=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C=20=EB=93=B1=EB=A1=9D=EB=90=9C=20Exception=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8D=98=EC=A7=80=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 미등록된 예외 대신, 정의된 예외 사용 - 401 코드 리턴 대신 의미가 명확한 403 코드 리턴 - javadoc spec에 맞게 주석 변경 ref. https://github.com/sopt-makers/sopt-crew-backend/pull/218#discussion_r1646810560 --- .../crew/main/comment/v2/service/CommentV2Service.java | 3 ++- .../main/comment/v2/service/CommentV2ServiceImpl.java | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index b1fed6b4..9a4051ba 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -2,11 +2,12 @@ import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.common.exception.ForbiddenException; public interface CommentV2Service { CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, Integer userId); - void deleteComment(Integer commentId, Integer userId); + void deleteComment(Integer commentId, Integer userId) throws ForbiddenException; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index b73de4ab..659f8d95 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.common.exception.ForbiddenException; +import org.sopt.makers.crew.main.common.response.ErrorStatus; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.post.Post; @@ -72,16 +74,16 @@ public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBod /** * 모임 게시글 댓글 삭제 * - * @throws 400 댓글 작성자가 아닐 때 + * @exception ForbiddenException 댓글 작성자가 아닐 때 * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 */ @Override @Transactional - public void deleteComment(Integer commentId, Integer userId) { + public void deleteComment(Integer commentId, Integer userId) throws ForbiddenException { Comment comment = commentRepository.findByIdOrThrow(commentId); if (!comment.getUserId().equals(userId)) { - throw new SecurityException("댓글 작성자만 삭제할 수 있습니다."); + throw new ForbiddenException(); } Post post = comment.getPost(); From 6623f66311d440ed564d412453676a1d59035851 Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Mon, 1 Jul 2024 01:29:06 +0900 Subject: [PATCH 15/35] =?UTF-8?q?[ENV]=20`post/v1`=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=9B=EC=A7=80=20=EB=AA=BB=ED=95=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 445d7d69..8b117b64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -97,12 +97,14 @@ services: caddy.route_9.reverse_proxy: "{{ upstreams 3000 }}" caddy.route_10: /notice/v1/* caddy.route_10.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_11: /post/v1/* + caddy.route_11: /post/v1 caddy.route_11.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_12: /users + caddy.route_12: /post/v1/* caddy.route_12.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_13: /users/* + caddy.route_13: /users caddy.route_13.reverse_proxy: "{{ upstreams 3000 }}" + caddy.route_14: /users/* + caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}" spring-green: image: makerscrew/main:latest @@ -188,12 +190,14 @@ services: caddy.route_9.reverse_proxy: "{{ upstreams 3000 }}" caddy.route_10: /notice/v1/* caddy.route_10.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_11: /post/v1/* + caddy.route_11: /post/v1 caddy.route_11.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_12: /users + caddy.route_12: /post/v1/* caddy.route_12.reverse_proxy: "{{ upstreams 3000 }}" - caddy.route_13: /users/* + caddy.route_13: /users caddy.route_13.reverse_proxy: "{{ upstreams 3000 }}" + caddy.route_14: /users/* + caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}" spring-blue: image: makerscrew/main:latest From 3d9c9283552ea0c87ee948bc3b9dcd2051d0b6a1 Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Mon, 1 Jul 2024 01:36:44 +0900 Subject: [PATCH 16/35] =?UTF-8?q?[REFACTOR]=20=EB=8C=93=EA=B8=80=EC=9D=84?= =?UTF-8?q?=20=EC=B0=BE=EC=A7=80=20=EB=AA=BB=ED=96=88=EC=9D=84=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=EC=9D=98=20=EC=97=90=EB=9F=AC=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "존재하지 않는 댓글입니다." 메시지를 ErrorStatus 내 등록 후 사용 - Error 객체 수정 ref. https://github.com/sopt-makers/sopt-crew-backend/pull/218#discussion_r1646811527 --- .../sopt/makers/crew/main/common/response/ErrorStatus.java | 1 + .../makers/crew/main/entity/comment/CommentRepository.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java index 8cb8a178..49fa8222 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java @@ -19,6 +19,7 @@ public enum ErrorStatus { VALIDATION_REQUEST_MISSING_EXCEPTION("요청값이 입력되지 않았습니다."), NOT_FOUND_MEETING("모임이 없습니다."), NOT_FOUND_POST("존재하지 않는 게시글입니다."), + NOT_FOUND_COMMENT("존재하지 않는 댓글입니다."), FULL_MEETING_CAPACITY("정원이 꽉 찼습니다."), ALREADY_APPLIED_MEETING("이미 지원한 모임입니다."), NOT_IN_APPLY_PERIOD("모임 지원 기간이 아닙니다."), diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java index 8926bf88..bfc3b910 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java @@ -1,11 +1,13 @@ package org.sopt.makers.crew.main.entity.comment; +import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.common.response.ErrorStatus; import org.springframework.data.jpa.repository.JpaRepository; public interface CommentRepository extends JpaRepository { default Comment findByIdOrThrow(Integer commentId) { return findById(commentId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 댓글입니다.")); + .orElseThrow(() -> new BadRequestException(ErrorStatus.NOT_FOUND_COMMENT.getErrorCode())); } } From 2d10f3076795fdfb755c33374dc2329e5de67193 Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Mon, 1 Jul 2024 01:50:04 +0900 Subject: [PATCH 17/35] =?UTF-8?q?[FIX]=20Comment=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=EC=97=90=20`userId`=EB=8F=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/makers/crew/main/entity/comment/Comment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index 3561f86c..f04312af 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -132,6 +132,7 @@ public class Comment { public Comment(String contents, User user, Post post, Comment parent) { this.contents = contents; this.user = user; + this.userId = user.getId(); this.post = post; this.parent = parent; this.depth = 0; From 5d3f7e75c3755b8a0909dfdbfbca4a47b50858a1 Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Mon, 1 Jul 2024 01:50:42 +0900 Subject: [PATCH 18/35] =?UTF-8?q?[TEST]=20=EC=8B=A4=ED=8C=A8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=8C=93=EA=B8=80=20=EC=82=AD=EC=A0=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/main/comment/v2/service/CommentV2ServiceTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java index efb4d8c2..af3003e4 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java @@ -14,6 +14,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.post.Post; @@ -55,7 +56,7 @@ void init() { doReturn(comment).when(commentRepository).findByIdOrThrow(any()); // when - commentV2Service.deleteComment(comment.getId(), comment.getUser().getId()); + commentV2Service.deleteComment(comment.getId(), user.getId()); // then Assertions.assertThat(commentRepository.findById(comment.getId())).isEqualTo(Optional.empty()); @@ -68,7 +69,7 @@ void init() { doReturn(comment).when(commentRepository).findByIdOrThrow(any()); // when & then - assertThrows(SecurityException.class, () -> { + assertThrows(ForbiddenException.class, () -> { commentV2Service.deleteComment(comment.getId(), comment.getUser().getId() + 1); }); } From 8c18f375d5066ccb505f6b9a1647b264084ac597 Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Sun, 16 Jun 2024 05:01:40 +0900 Subject: [PATCH 19/35] =?UTF-8?q?[FEAT]=20=EB=8C=93=EA=B8=80=20=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=ED=95=98=EA=B8=B0=20V2=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spring 프로젝트에 댓글 신고하기 V2 API 구현 - 사용하지 않는 댓글 신고하기 V1 API에 deprecated 처리 - Spring에 잘못 매핑되어있던 신고 테이블 정보 수정 --- .../main/comment/v2/CommentV2Controller.java | 13 ++++++++ .../CommentV2ReportCommentResponseDto.java | 14 ++++++++ .../comment/v2/service/CommentV2Service.java | 3 ++ .../v2/service/CommentV2ServiceImpl.java | 32 +++++++++++++++++++ .../crew/main/entity/report/Report.java | 4 +-- .../main/entity/report/ReportRepository.java | 11 +++++++ .../src/comment/v1/comment-v1.controller.ts | 4 +++ 7 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2ReportCommentResponseDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/report/ReportRepository.java diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java index 1dbd1347..2e9e9661 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java @@ -9,10 +9,12 @@ import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.service.CommentV2Service; import org.sopt.makers.crew.main.common.util.UserUtil; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -41,6 +43,17 @@ public ResponseEntity createComment( return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); } + @Operation(summary = "댓글 신고하기") + @PostMapping("/{commentId}/report") + @ResponseStatus(HttpStatus.CREATED) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + }) + public ResponseEntity reportComment( + @PathVariable Integer commentId, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(commentV2Service.reportComment(commentId, userId)); + } @Operation(summary = "모임 게시글 댓글 삭제") @DeleteMapping("/{commentId}") @ResponseStatus(HttpStatus.NO_CONTENT) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2ReportCommentResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2ReportCommentResponseDto.java new file mode 100644 index 00000000..03340a2c --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2ReportCommentResponseDto.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class CommentV2ReportCommentResponseDto { + + /** + * 생성된 신고 id + */ + private Integer reportId; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index 9a4051ba..a2ecc858 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -2,6 +2,7 @@ import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.common.exception.ForbiddenException; public interface CommentV2Service { @@ -9,5 +10,7 @@ public interface CommentV2Service { CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, Integer userId); + CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId); + void deleteComment(Integer commentId, Integer userId) throws ForbiddenException; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index 659f8d95..47e7f3d5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -8,10 +8,13 @@ import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.common.response.ErrorStatus; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.post.Post; import org.sopt.makers.crew.main.entity.post.PostRepository; +import org.sopt.makers.crew.main.entity.report.Report; +import org.sopt.makers.crew.main.entity.report.ReportRepository; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.internal.notification.PushNotificationService; @@ -28,6 +31,7 @@ public class CommentV2ServiceImpl implements CommentV2Service { private final PostRepository postRepository; private final UserRepository userRepository; private final CommentRepository commentRepository; + private final ReportRepository reportRepository; private final PushNotificationService pushNotificationService; @Value("${push-notification.web-url}") @@ -71,6 +75,34 @@ public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBod return CommentV2CreateCommentResponseDto.of(savedComment.getId()); } + /** + * 댓글 신고하기 + * @param commentId 댓글 신고할 댓글 id + * @param userId 신고하는 유저 id + * @return 신고 ID + * @apiNote 댓글 신고는 한 댓글당 한번만 가능 + * @throws 400 이미 신고한 댓글일 때 + */ + @Override + @Transactional + public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) { + Comment comment = commentRepository.findByIdOrThrow(commentId); + User user = userRepository.findByIdOrThrow(userId); + + reportRepository.findByCommentAndUser(comment, user).ifPresent(report -> { + throw new IllegalArgumentException("이미 신고한 댓글입니다."); + }); + + Report report = Report.builder() + .comment(comment) + .user(user) + .build(); + + Report savedReport = reportRepository.save(report); + + return CommentV2ReportCommentResponseDto.of(savedReport.getId()); + } + /** * 모임 게시글 댓글 삭제 * diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/report/Report.java b/main/src/main/java/org/sopt/makers/crew/main/entity/report/Report.java index 9ae43979..2827aee4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/report/Report.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/report/Report.java @@ -24,7 +24,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "like") +@Table(name = "report") @EntityListeners(AuditingEntityListener.class) public class Report { /** @@ -65,7 +65,7 @@ public class Report { * 게시글 id - 게시글 좋아요가 아닐 경우 null */ @Column(insertable = false, updatable = false) - private int postId; + private Integer postId; /** * 댓글 diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/report/ReportRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/report/ReportRepository.java new file mode 100644 index 00000000..56071d7d --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/report/ReportRepository.java @@ -0,0 +1,11 @@ +package org.sopt.makers.crew.main.entity.report; + +import java.util.Optional; +import org.sopt.makers.crew.main.entity.comment.Comment; +import org.sopt.makers.crew.main.entity.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { + + Optional findByCommentAndUser(Comment comment, User user); +} diff --git a/server/src/comment/v1/comment-v1.controller.ts b/server/src/comment/v1/comment-v1.controller.ts index 47d06e9a..39ee85b7 100644 --- a/server/src/comment/v1/comment-v1.controller.ts +++ b/server/src/comment/v1/comment-v1.controller.ts @@ -74,8 +74,12 @@ export class CommentV1Controller { return this.commentV1Service.switchCommentLike({ param, user }); } + /** + * @deprecated + */ @ApiOperation({ summary: '댓글 신고', + deprecated: true, }) @ApiOkResponseCommon(CommentV1ReportCommentResponseDto) @ApiResponse({ From 0e77040a6716465ce914cbcc0f94bcccb081a54c Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Mon, 1 Jul 2024 02:10:15 +0900 Subject: [PATCH 20/35] =?UTF-8?q?[FIX]=20=EC=97=90=EB=9F=AC=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20ErrorStatus=20=EA=B0=92=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - javadoc 문법에 맞도록 주석 수정 - throw 하는 예외 객체 명시 - 예외 객체 종류 변경 --- .../main/comment/v2/service/CommentV2Service.java | 3 ++- .../comment/v2/service/CommentV2ServiceImpl.java | 13 ++++++++----- .../crew/main/common/response/ErrorStatus.java | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index a2ecc858..47e7c549 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -3,6 +3,7 @@ import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; public interface CommentV2Service { @@ -10,7 +11,7 @@ public interface CommentV2Service { CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, Integer userId); - CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId); + CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) throws BadRequestException; void deleteComment(Integer commentId, Integer userId) throws ForbiddenException; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index 47e7f3d5..ce5caa7d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.common.response.ErrorStatus; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; @@ -77,20 +78,22 @@ public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBod /** * 댓글 신고하기 + * * @param commentId 댓글 신고할 댓글 id - * @param userId 신고하는 유저 id + * @param userId 신고하는 유저 id * @return 신고 ID + * @throws BadRequestException 이미 신고한 댓글일 때 * @apiNote 댓글 신고는 한 댓글당 한번만 가능 - * @throws 400 이미 신고한 댓글일 때 */ @Override @Transactional - public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) { + public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException { Comment comment = commentRepository.findByIdOrThrow(commentId); User user = userRepository.findByIdOrThrow(userId); reportRepository.findByCommentAndUser(comment, user).ifPresent(report -> { - throw new IllegalArgumentException("이미 신고한 댓글입니다."); + throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); }); Report report = Report.builder() @@ -106,7 +109,7 @@ public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Intege /** * 모임 게시글 댓글 삭제 * - * @exception ForbiddenException 댓글 작성자가 아닐 때 + * @throws ForbiddenException 댓글 작성자가 아닐 때 * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 */ @Override diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java index 49fa8222..c4d041e2 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java @@ -22,6 +22,7 @@ public enum ErrorStatus { NOT_FOUND_COMMENT("존재하지 않는 댓글입니다."), FULL_MEETING_CAPACITY("정원이 꽉 찼습니다."), ALREADY_APPLIED_MEETING("이미 지원한 모임입니다."), + ALREADY_REPORTED_COMMENT("이미 신고한 댓글입니다."), NOT_IN_APPLY_PERIOD("모임 지원 기간이 아닙니다."), MISSING_GENERATION_PART("내 프로필에서 기수/파트 정보를 입력해주세요."), NOT_ACTIVE_GENERATION("활동 기수가 아닙니다."), From ebf006b8490a733a5a24b3617bf1bba8e0fce677 Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Mon, 1 Jul 2024 02:32:37 +0900 Subject: [PATCH 21/35] =?UTF-8?q?[REFACTOR]=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/service/CommentV2ServiceImpl.java | 7 +- .../v2/service/CommentV2ServiceTest.java | 138 +++++++++++------- 2 files changed, 93 insertions(+), 52 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index ce5caa7d..c55b3022 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -3,6 +3,7 @@ import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_PUSH_NOTIFICATION_TITLE; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; @@ -92,9 +93,11 @@ public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Intege Comment comment = commentRepository.findByIdOrThrow(commentId); User user = userRepository.findByIdOrThrow(userId); - reportRepository.findByCommentAndUser(comment, user).ifPresent(report -> { + Optional existingReport = reportRepository.findByCommentAndUser(comment, user); + + if (existingReport.isPresent()) { throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); - }); + } Report report = Report.builder() .comment(comment) diff --git a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java index af3003e4..83cd386b 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java @@ -1,6 +1,5 @@ package org.sopt.makers.crew.main.comment.v2.service; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; @@ -13,64 +12,103 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.post.Post; -import org.sopt.makers.crew.main.entity.post.PostRepository; +import org.sopt.makers.crew.main.entity.report.Report; +import org.sopt.makers.crew.main.entity.report.ReportRepository; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserFixture; import org.sopt.makers.crew.main.entity.user.UserRepository; @ExtendWith(MockitoExtension.class) public class CommentV2ServiceTest { - @InjectMocks - private CommentV2ServiceImpl commentV2Service; - @Mock - private CommentRepository commentRepository; - @Mock - private PostRepository postRepository; - - private Comment comment; - - private Post post; - - private User user; - - @BeforeEach - void init() { - user = UserFixture.createStaticUser(); - user.setUserIdForTest(1); - - String[] images = {"image1", "image2", "image3"}; - this.post = Post.builder().user(user).title("title").contents("contents").images(images).build(); - this.comment = Comment.builder().contents("contents").post(post).user(user).build(); - post.addComment(this.comment); - } - - @Test - void 댓글_삭제_성공() { - // given - int initialCommentCount = post.getCommentCount(); - doReturn(comment).when(commentRepository).findByIdOrThrow(any()); - - // when - commentV2Service.deleteComment(comment.getId(), user.getId()); - - // then - Assertions.assertThat(commentRepository.findById(comment.getId())).isEqualTo(Optional.empty()); - Assertions.assertThat(post.getCommentCount()).isEqualTo(initialCommentCount - 1); - } - - @Test - void 댓글_삭제_실패_본인_작성_댓글_아님() { - // given - doReturn(comment).when(commentRepository).findByIdOrThrow(any()); - - // when & then - assertThrows(ForbiddenException.class, () -> { - commentV2Service.deleteComment(comment.getId(), comment.getUser().getId() + 1); - }); - } + + @InjectMocks + private CommentV2ServiceImpl commentV2Service; + @Mock + private CommentRepository commentRepository; + @Mock + private ReportRepository reportRepository; + @Mock + private UserRepository userRepository; + + private Comment comment; + + private Post post; + + private User user; + + private Report report; + + @BeforeEach + void init() { + user = UserFixture.createStaticUser(); + user.setUserIdForTest(1); + + String[] images = {"image1", "image2", "image3"}; + this.post = Post.builder().user(user).title("title").contents("contents").images(images) + .build(); + this.comment = Comment.builder().contents("contents").post(post).user(user).build(); + post.addComment(this.comment); + + this.report = Report.builder().comment(comment).user(user).build(); + } + + @Test + void 댓글_삭제_성공() { + // given + int initialCommentCount = post.getCommentCount(); + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + + // when + commentV2Service.deleteComment(comment.getId(), user.getId()); + + // then + Assertions.assertThat(commentRepository.findById(comment.getId())).isEqualTo(Optional.empty()); + Assertions.assertThat(post.getCommentCount()).isEqualTo(initialCommentCount - 1); + } + + @Test + void 댓글_삭제_실패_본인_작성_댓글_아님() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + + // when & then + assertThrows(ForbiddenException.class, () -> { + commentV2Service.deleteComment(comment.getId(), comment.getUser().getId() + 1); + }); + } + + @Test + void 댓글_신고_성공() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(user).when(userRepository).findByIdOrThrow(any()); + doReturn(Optional.empty()).when(reportRepository).findByCommentAndUser(any(), any()); + doReturn(report).when(reportRepository).save(any()); + + // when + CommentV2ReportCommentResponseDto result = commentV2Service.reportComment(comment.getId(), + user.getId()); + + // then + Assertions.assertThat(result.getReportId()).isEqualTo(report.getId()); + } + + @Test + void 댓글_신고_실패_이미_신고한_댓글() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(user).when(userRepository).findByIdOrThrow(any()); + doReturn(Optional.of(report)).when(reportRepository).findByCommentAndUser(any(), any()); + + // when & then + assertThrows(BadRequestException.class, () -> { + commentV2Service.reportComment(comment.getId(), user.getId()); + }); + } } From 11d94954408e4dcc54e2bc0206582c5bd32370c0 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Tue, 2 Jul 2024 22:10:11 +0900 Subject: [PATCH 22/35] =?UTF-8?q?[FEAT]=20=EB=A9=98=EC=85=98=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [DEL] 스웨거 관련 삭제 * [ADD] 서버 에러 추가 * [ADD] 유저 관련 스웨거 추가 * [FEAT] 멘션 사용자 조회 로직 구현 * [ADD] 멘션 사용자 관련 Dto 추가 * [FEAT] 멘션 사용자 조회 컨트롤러 로직 구현 * [FEAT] 최근 기수 조회 로직 구현 * [TEST] 유저 엔티티 테스트 코드 작성 * [CHORE] 변수명 변경 * [CHORE] 파라미터 변경 * [ADD] 통합테스트 전용 어노테이션 추가 * [TEST] 멘션 사용자 조회 기능 테스트 코드 작성 --- .../common/exception/ServerException.java | 14 ++ .../makers/crew/main/entity/user/User.java | 12 ++ .../crew/main/post/v2/PostV2Controller.java | 1 - .../makers/crew/main/user/v2/UserApi.java | 28 ++++ .../crew/main/user/v2/UserV2Controller.java | 37 +++--- .../response/UserV2GetAllMentionUserDto.java | 15 +++ .../main/user/v2/service/UserV2Service.java | 2 + .../user/v2/service/UserV2ServiceImpl.java | 68 ++++++---- .../common/annotation/IntegratedTest.java | 21 +++ .../crew/main/entity/user/UserEntityTest.java | 51 ++++++++ .../crew/main/user/v2/UserServiceTest.java | 123 ++++++++++++++++++ 11 files changed, 324 insertions(+), 48 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/common/exception/ServerException.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/user/v2/UserApi.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/common/annotation/IntegratedTest.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/exception/ServerException.java b/main/src/main/java/org/sopt/makers/crew/main/common/exception/ServerException.java new file mode 100644 index 00000000..2ab8faa2 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/common/exception/ServerException.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.common.exception; + +import org.springframework.http.HttpStatus; + +public class ServerException extends BaseException { + public ServerException(HttpStatus httpStatus) { + super(httpStatus); + } + + public ServerException(String responseMessage) { + super(HttpStatus.INTERNAL_SERVER_ERROR, responseMessage); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java index 0e3780bb..1ffd454e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java @@ -1,5 +1,7 @@ package org.sopt.makers.crew.main.entity.user; +import static org.sopt.makers.crew.main.common.response.ErrorStatus.*; + import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; @@ -10,6 +12,7 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -18,6 +21,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Type; +import org.sopt.makers.crew.main.common.exception.ServerException; +import org.sopt.makers.crew.main.common.response.ErrorStatus; import org.sopt.makers.crew.main.entity.apply.Apply; import org.sopt.makers.crew.main.entity.meeting.Meeting; import org.sopt.makers.crew.main.entity.post.Post; @@ -118,4 +123,11 @@ public void addReport(Report report) { public void setUserIdForTest(Integer userId) { this.id = userId; } + + public UserActivityVO getRecentActivityVO(){ + return activities.stream() + .filter(userActivityVO -> userActivityVO.getPart() != null) + .max(Comparator.comparingInt(UserActivityVO::getGeneration)) + .orElseThrow(() -> new ServerException(INTERNAL_SERVER_ERROR.getErrorCode())); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java index b3c9ebbe..b0b85e42 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java @@ -24,7 +24,6 @@ @RestController @RequestMapping("/post/v2") @RequiredArgsConstructor -@Tag(name = "게시글") public class PostV2Controller implements PostV2Api { private final PostV2Service postV2Service; diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserApi.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserApi.java new file mode 100644 index 00000000..5d17ecc3 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserApi.java @@ -0,0 +1,28 @@ +package org.sopt.makers.crew.main.user.v2; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 java.security.Principal; +import java.util.List; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.springframework.http.ResponseEntity; + +@Tag(name = "사용자") +public interface UserApi { + + @Operation(summary = "내가 속한 모임 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "204", description = "내가 속한 모임 리스트가 없는 경우", content = @Content), + }) + ResponseEntity> getAllMeetingByUser(Principal principal); + + @Operation(summary = "멘션 사용자 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공")}) + ResponseEntity> getAllMentionUser(Principal principal); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java index c8aa632a..61f90ef1 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java @@ -1,15 +1,11 @@ package org.sopt.makers.crew.main.user.v2; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -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 java.security.Principal; import java.util.List; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.util.UserUtil; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; import org.sopt.makers.crew.main.user.v2.service.UserV2Service; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -21,21 +17,22 @@ @RestController @RequestMapping("/user/v2") @RequiredArgsConstructor -@Tag(name = "사용자") -public class UserV2Controller { +public class UserV2Controller implements UserApi { - private final UserV2Service userV2Service; + private final UserV2Service userV2Service; - @Operation(summary = "내가 속한 모임 조회") - @GetMapping("/meeting/all") - @ResponseStatus(HttpStatus.OK) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "204", description = "내가 속한 모임 리스트가 없는 경우", content = @Content), - }) - public ResponseEntity> getAllMeetingByUser( - Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(userV2Service.getAllMeetingByUser(userId)); - } + @GetMapping("/meeting/all") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> getAllMeetingByUser( + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(userV2Service.getAllMeetingByUser(userId)); + } + + @GetMapping("/mention") + public ResponseEntity> getAllMentionUser( + Principal principal) { + UserUtil.getUserId(principal); + return ResponseEntity.ok(userV2Service.getAllMentionUser()); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java new file mode 100644 index 00000000..3b81677c --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java @@ -0,0 +1,15 @@ +package org.sopt.makers.crew.main.user.v2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class UserV2GetAllMentionUserDto { + private Integer userId; + private String userName; + private String recentPart; + private int recentGeneration; + private String profileImageUrl; +} + diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java index ecfbe87e..a6710c9d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java @@ -2,9 +2,11 @@ import java.util.List; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; public interface UserV2Service { List getAllMeetingByUser(Integer userId); + List getAllMentionUser(); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java index 804e5f5b..e5129374 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java @@ -11,6 +11,7 @@ import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,32 +21,45 @@ @Transactional(readOnly = true) public class UserV2ServiceImpl implements UserV2Service { - private final UserRepository userRepository; - private final ApplyRepository applyRepository; - - @Override - public List getAllMeetingByUser(Integer userId) { - User user = userRepository.findByIdOrThrow(userId); - - List userJoinedList = Stream.concat( - user.getMeetings().stream(), - applyRepository.findAllByUserIdAndStatus(userId, EnApplyStatus.APPROVE) - .stream() - .map(apply -> apply.getMeeting()) - ) - .map(meeting -> UserV2GetAllMeetingByUserMeetingDto.of( - meeting.getId(), - meeting.getTitle(), - meeting.getDesc(), - meeting.getImageURL().get(0).getUrl(), - meeting.getCategory().getValue() - )) - .sorted(Comparator.comparing(UserV2GetAllMeetingByUserMeetingDto::getId).reversed()) - .collect(Collectors.toList()); - - if (userJoinedList.isEmpty()) { - throw new BaseException(HttpStatus.NO_CONTENT); + private final UserRepository userRepository; + private final ApplyRepository applyRepository; + + @Override + public List getAllMeetingByUser(Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + + List userJoinedList = Stream.concat( + user.getMeetings().stream(), + applyRepository.findAllByUserIdAndStatus(userId, EnApplyStatus.APPROVE) + .stream() + .map(apply -> apply.getMeeting()) + ) + .map(meeting -> UserV2GetAllMeetingByUserMeetingDto.of( + meeting.getId(), + meeting.getTitle(), + meeting.getDesc(), + meeting.getImageURL().get(0).getUrl(), + meeting.getCategory().getValue() + )) + .sorted(Comparator.comparing(UserV2GetAllMeetingByUserMeetingDto::getId).reversed()) + .collect(Collectors.toList()); + + if (userJoinedList.isEmpty()) { + throw new BaseException(HttpStatus.NO_CONTENT); + } + return userJoinedList; + } + + @Override + public List getAllMentionUser() { + + List users = userRepository.findAll(); + + return users.stream() + .filter(user -> user.getActivities() != null) + .map(user -> UserV2GetAllMentionUserDto.of(user.getId(), user.getName(), + user.getRecentActivityVO().getPart(), user.getRecentActivityVO().getGeneration(), + user.getProfileImage())) + .toList(); } - return userJoinedList; - } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/common/annotation/IntegratedTest.java b/main/src/test/java/org/sopt/makers/crew/main/common/annotation/IntegratedTest.java new file mode 100644 index 00000000..4af63b6d --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/common/annotation/IntegratedTest.java @@ -0,0 +1,21 @@ +package org.sopt.makers.crew.main.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@SpringBootTest +@Transactional +@Testcontainers +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public @interface IntegratedTest { +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java new file mode 100644 index 00000000..a88f3907 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java @@ -0,0 +1,51 @@ +package org.sopt.makers.crew.main.entity.user; + +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; + +public class UserEntityTest { + + + @Test + void 최근_기수_조회(){ + // given + User user = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url") + .phone("010-1234-5678") + .build(); + + // when + UserActivityVO recentActivityVO = user.getRecentActivityVO(); + + // then + + Assertions.assertThat(recentActivityVO.getGeneration()).isEqualTo(34); + Assertions.assertThat(recentActivityVO.getPart()).isEqualTo("iOS"); + + } + + @Test + void 최근_기수_조회_잘못된_데이터가_저장된_경우_해당_데이터는_무시한다(){ + // given + User user = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO(null, 34), new UserActivityVO("서버", 33))) + .profileImage("image-url") + .phone("010-1234-5678") + .build(); + + // when + UserActivityVO recentActivityVO = user.getRecentActivityVO(); + + // then + + Assertions.assertThat(recentActivityVO.getGeneration()).isEqualTo(33); + Assertions.assertThat(recentActivityVO.getPart()).isEqualTo("서버"); + } +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java new file mode 100644 index 00000000..548a2779 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java @@ -0,0 +1,123 @@ +package org.sopt.makers.crew.main.user.v2; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.common.annotation.IntegratedTest; +import org.sopt.makers.crew.main.entity.user.User; +import org.sopt.makers.crew.main.entity.user.UserRepository; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.sopt.makers.crew.main.user.v2.service.UserV2Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +@IntegratedTest +public class UserServiceTest { + + @Autowired + private UserV2Service userV2Service; + + @Autowired + private UserRepository userRepository; + + + @Test + void 멘션_사용자_조회(){ + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO("iOS", 30), new UserActivityVO("안드로이드", 33))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllMentionUser(); + + // then + assertThat(allMentionUsers).hasSize(2); + assertThat(allMentionUsers.get(0)) + .extracting( "userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly( "홍길동", "iOS", 34, "image-url1"); + assertThat(allMentionUsers.get(1)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "안드로이드", 33, "image-url2"); + + } + + @Test + void 멘션_사용자_조회시_db에_null_저장된_경우(){ + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(null) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO("iOS", 30), new UserActivityVO("안드로이드", 33))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllMentionUser(); + + // then + assertThat(allMentionUsers).hasSize(1); + assertThat(allMentionUsers.get(0)) + .extracting( "userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "안드로이드", 33, "image-url2"); + } + + @Test + void 멘션_사용자_조회시_db에_올바르지_않은_데이터_저장된_경우(){ + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO(null, 30), new UserActivityVO("", 34))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllMentionUser(); + + // then + assertThat(allMentionUsers).hasSize(2); + assertThat(allMentionUsers.get(0)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("홍길동", "iOS", 34, "image-url1"); + assertThat(allMentionUsers.get(1)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "", 34, "image-url2"); + } +} From b0a1aa91772c02d847a09005c27aa83e35619cf4 Mon Sep 17 00:00:00 2001 From: JiHwan <62228195+sgh002400@users.noreply.github.com> Date: Sun, 7 Jul 2024 21:57:53 +0900 Subject: [PATCH 23/35] =?UTF-8?q?[FEAT]=20=EB=AA=A8=EC=9E=84=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=AC=BC=20&=20=EB=8C=93=EA=B8=80=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=A9=98=EC=85=98=20V2=20API=20=EA=B5=AC=ED=98=84=20(#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/main/comment/v2/CommentV2Api.java | 51 ++++ .../main/comment/v2/CommentV2Controller.java | 78 +++--- ...mmentV2MentionUserInCommentRequestDto.java | 25 ++ .../comment/v2/service/CommentV2Service.java | 12 +- .../v2/service/CommentV2ServiceImpl.java | 222 ++++++++++-------- .../crew/main/entity/user/UserRepository.java | 21 +- .../notification/PushNotificationEnums.java | 15 +- .../makers/crew/main/post/v2/PostV2Api.java | 30 ++- .../crew/main/post/v2/PostV2Controller.java | 19 +- .../PostV2MentionUserInPostRequestDto.java | 25 ++ .../main/post/v2/service/PostV2Service.java | 3 + .../post/v2/service/PostV2ServiceImpl.java | 69 ++++-- 12 files changed, 379 insertions(+), 191 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java new file mode 100644 index 00000000..eceef123 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java @@ -0,0 +1,51 @@ +package org.sopt.makers.crew.main.comment.v2; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import java.security.Principal; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +public interface CommentV2Api { + + @Operation(summary = "모임 게시글 댓글 작성") + @ResponseStatus(HttpStatus.CREATED) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + }) + public ResponseEntity createComment( + @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal); + + @Operation(summary = "댓글 신고하기") + @ResponseStatus(HttpStatus.CREATED) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + }) + public ResponseEntity reportComment( + @PathVariable Integer commentId, Principal principal); + + @Operation(summary = "모임 게시글 댓글 삭제") + @ResponseStatus(HttpStatus.NO_CONTENT) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "성공"), + }) + public ResponseEntity deleteComment(Principal principal, @PathVariable Integer commentId); + + @Operation(summary = "댓글에서 유저 멘션") + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + public ResponseEntity mentionUserInComment( + @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, + Principal principal); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java index 2e9e9661..1c189c33 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java @@ -1,73 +1,67 @@ package org.sopt.makers.crew.main.comment.v2; -import io.swagger.v3.oas.annotations.Operation; -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 java.security.Principal; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.service.CommentV2Service; import org.sopt.makers.crew.main.common.util.UserUtil; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/comment/v2") @RequiredArgsConstructor @Tag(name = "댓글/대댓글") -public class CommentV2Controller { +public class CommentV2Controller implements CommentV2Api { + + private final CommentV2Service commentV2Service; - private final CommentV2Service commentV2Service; + @Override + @PostMapping() + public ResponseEntity createComment( + @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); + } - @Operation(summary = "모임 게시글 댓글 작성") - @PostMapping() - @ResponseStatus(HttpStatus.CREATED) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - }) - public ResponseEntity createComment( - @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); - } + @Override + @PostMapping("/{commentId}/report") + public ResponseEntity reportComment( + @PathVariable Integer commentId, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(commentV2Service.reportComment(commentId, userId)); + } - @Operation(summary = "댓글 신고하기") - @PostMapping("/{commentId}/report") - @ResponseStatus(HttpStatus.CREATED) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - }) - public ResponseEntity reportComment( - @PathVariable Integer commentId, Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(commentV2Service.reportComment(commentId, userId)); - } - @Operation(summary = "모임 게시글 댓글 삭제") - @DeleteMapping("/{commentId}") - @ResponseStatus(HttpStatus.NO_CONTENT) - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "성공"), - }) - public ResponseEntity deleteComment( - Principal principal, - @PathVariable Integer commentId) { - Integer userId = UserUtil.getUserId(principal); + @Override + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment( + Principal principal, + @PathVariable Integer commentId) { + Integer userId = UserUtil.getUserId(principal); - commentV2Service.deleteComment(commentId, userId); + commentV2Service.deleteComment(commentId, userId); - return ResponseEntity.noContent().build(); - } + return ResponseEntity.noContent().build(); + } + @Override + @PostMapping("/mention") + public ResponseEntity mentionUserInComment( + @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + commentV2Service.mentionUserInComment(requestBody, userId); + return ResponseEntity.status(HttpStatus.OK).build(); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java new file mode 100644 index 00000000..48db818d --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java @@ -0,0 +1,25 @@ +package org.sopt.makers.crew.main.comment.v2.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "댓글에서 유저 언급 request body dto") +public class CommentV2MentionUserInCommentRequestDto { + + @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") + @NotEmpty + private List userIds; + + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private Integer postId; + + @Schema(example = "멘션내용~~", required = true, description = "멘션 내용") + private String content; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index 47e7c549..c370dcf5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -1,6 +1,7 @@ package org.sopt.makers.crew.main.comment.v2.service; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; @@ -8,10 +9,13 @@ public interface CommentV2Service { - CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, - Integer userId); + CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, + Integer userId); - CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) throws BadRequestException; + CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException; - void deleteComment(Integer commentId, Integer userId) throws ForbiddenException; + void deleteComment(Integer commentId, Integer userId) throws ForbiddenException; + + void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index c55b3022..a22a2ad8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -1,16 +1,18 @@ package org.sopt.makers.crew.main.comment.v2.service; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_PUSH_NOTIFICATION_TITLE; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.common.response.ErrorStatus; -import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.post.Post; @@ -30,103 +32,133 @@ @Transactional(readOnly = true) public class CommentV2ServiceImpl implements CommentV2Service { - private final PostRepository postRepository; - private final UserRepository userRepository; - private final CommentRepository commentRepository; - private final ReportRepository reportRepository; - private final PushNotificationService pushNotificationService; - - @Value("${push-notification.web-url}") - private String pushWebUrl; - - /** - * 모임 게시글 댓글 작성 - * - * @throws 400 존재하지 않는 게시글일 떄 - * @apiNote 모임에 속한 유저만 작성 가능 - */ - @Override - @Transactional - public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, - Integer userId) { - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - User user = userRepository.findByIdOrThrow(userId); - - Comment comment = Comment.builder() - .contents(requestBody.getContents()) - .user(user) - .post(post) - .build(); - - Comment savedComment = commentRepository.save(comment); - - User PostWriter = post.getUser(); - String[] userIds = {String.valueOf(PostWriter.getOrgId())}; - - String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", - user.getName(), requestBody.getContents()); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); - - pushNotificationService.sendPushNotification(pushRequestDto); - - return CommentV2CreateCommentResponseDto.of(savedComment.getId()); - } - - /** - * 댓글 신고하기 - * - * @param commentId 댓글 신고할 댓글 id - * @param userId 신고하는 유저 id - * @return 신고 ID - * @throws BadRequestException 이미 신고한 댓글일 때 - * @apiNote 댓글 신고는 한 댓글당 한번만 가능 - */ - @Override - @Transactional - public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) - throws BadRequestException { - Comment comment = commentRepository.findByIdOrThrow(commentId); - User user = userRepository.findByIdOrThrow(userId); - - Optional existingReport = reportRepository.findByCommentAndUser(comment, user); - - if (existingReport.isPresent()) { - throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); + private final PostRepository postRepository; + private final UserRepository userRepository; + private final CommentRepository commentRepository; + private final ReportRepository reportRepository; + private final PushNotificationService pushNotificationService; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + /** + * 모임 게시글 댓글 작성 + * + * @throws 400 존재하지 않는 게시글일 떄 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + public CommentV2CreateCommentResponseDto createComment( + CommentV2CreateCommentBodyDto requestBody, + Integer userId) { + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + User user = userRepository.findByIdOrThrow(userId); + + Comment comment = Comment.builder() + .contents(requestBody.getContents()) + .user(user) + .post(post) + .build(); + + Comment savedComment = commentRepository.save(comment); + + User PostWriter = post.getUser(); + String[] userIds = {String.valueOf(PostWriter.getOrgId())}; + + String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", + user.getName(), requestBody.getContents()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + + return CommentV2CreateCommentResponseDto.of(savedComment.getId()); } - Report report = Report.builder() - .comment(comment) - .user(user) - .build(); - - Report savedReport = reportRepository.save(report); - - return CommentV2ReportCommentResponseDto.of(savedReport.getId()); - } - - /** - * 모임 게시글 댓글 삭제 - * - * @throws ForbiddenException 댓글 작성자가 아닐 때 - * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 - */ - @Override - @Transactional - public void deleteComment(Integer commentId, Integer userId) throws ForbiddenException { - Comment comment = commentRepository.findByIdOrThrow(commentId); - - if (!comment.getUserId().equals(userId)) { - throw new ForbiddenException(); + /** + * 댓글 신고하기 + * + * @param commentId 댓글 신고할 댓글 id + * @param userId 신고하는 유저 id + * @return 신고 ID + * @throws BadRequestException 이미 신고한 댓글일 때 + * @apiNote 댓글 신고는 한 댓글당 한번만 가능 + */ + @Override + @Transactional + public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException { + Comment comment = commentRepository.findByIdOrThrow(commentId); + User user = userRepository.findByIdOrThrow(userId); + + Optional existingReport = reportRepository.findByCommentAndUser(comment, user); + + if (existingReport.isPresent()) { + throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); + } + + Report report = Report.builder() + .comment(comment) + .user(user) + .build(); + + Report savedReport = reportRepository.save(report); + + return CommentV2ReportCommentResponseDto.of(savedReport.getId()); } - Post post = comment.getPost(); + /** + * 모임 게시글 댓글 삭제 + * + * @throws ForbiddenException 댓글 작성자가 아닐 때 + * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 + */ + @Override + @Transactional + public void deleteComment(Integer commentId, Integer userId) throws ForbiddenException { + Comment comment = commentRepository.findByIdOrThrow(commentId); + + if (!comment.getUserId().equals(userId)) { + throw new ForbiddenException(); + } + + Post post = comment.getPost(); + + post.decreaseCommentCount(); + commentRepository.delete(comment); + } - post.decreaseCommentCount(); - commentRepository.delete(comment); - } + @Override + public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, + Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + + String pushNotificationContent = String.format("[%s님이 회원님을 언급했어요.] : \"%s\"", + user.getName(), requestBody.getContent()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + String[] userOrgIds = userRepository.findByIdIn(requestBody.getUserIds()) + .stream() + .map(mentionedUser -> String.valueOf(mentionedUser.getOrgId())) + .toArray(String[]::new); + + String newCommentMentionPushNotificationTitle = String.format( + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), user.getName()); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( + userOrgIds, + newCommentMentionPushNotificationTitle, + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), + pushNotificationWeblink + ); + + pushNotificationService.sendPushNotification(pushRequestDto); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java index 8cd6a8ea..54ff351d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java @@ -2,6 +2,7 @@ import static org.sopt.makers.crew.main.common.response.ErrorStatus.NO_CONTENT_EXCEPTION; +import java.util.List; import java.util.Optional; import org.sopt.makers.crew.main.common.exception.NoContentException; import org.sopt.makers.crew.main.common.exception.UnAuthorizedException; @@ -9,15 +10,17 @@ public interface UserRepository extends JpaRepository { - Optional findByOrgId(Integer orgId); + Optional findByOrgId(Integer orgId); - default User findByIdOrThrow(Integer userId) { - return findById(userId).orElseThrow(() -> new UnAuthorizedException()); - } + default User findByIdOrThrow(Integer userId) { + return findById(userId).orElseThrow(() -> new UnAuthorizedException()); + } - default User findByOrgIdOrThrow(Integer orgUserId) { - return findByOrgId(orgUserId).orElseThrow( - () -> new NoContentException( - NO_CONTENT_EXCEPTION.getErrorCode())); //유저가 아직 모임 서비스를 이용 전이기 때문에 - } + default User findByOrgIdOrThrow(Integer orgUserId) { + return findByOrgId(orgUserId).orElseThrow( + () -> new NoContentException( + NO_CONTENT_EXCEPTION.getErrorCode())); //유저가 아직 모임 서비스를 이용 전이기 때문에 + } + + List findByIdIn(List userIds); } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java index 83096c9c..b7d36d65 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java +++ b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java @@ -7,13 +7,14 @@ @Getter @RequiredArgsConstructor(access = AccessLevel.PROTECTED) public enum PushNotificationEnums { - PUSH_NOTIFICATION_ACTION("send"), + PUSH_NOTIFICATION_ACTION("send"), - PUSH_NOTIFICATION_CATEGORY("NEWS"), + PUSH_NOTIFICATION_CATEGORY("NEWS"), - NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."), - NEW_COMMENT_PUSH_NOTIFICATION_TITLE("📢내가 작성한 모임 피드에 새로운 댓글이 달렸어요."), - ; - - private final String value; + NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."), + NEW_COMMENT_PUSH_NOTIFICATION_TITLE("📢내가 작성한 모임 피드에 새로운 댓글이 달렸어요."), + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE("💬%s님이 회원님을 언급했어요."), + NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE("✏️모임 피드에서 회원님이 언급됐어요."), + ; + private final String value; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java index 6a4c2da2..d1eeffc6 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java @@ -12,6 +12,7 @@ import java.security.Principal; import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; import org.springframework.http.ResponseEntity; @@ -23,22 +24,31 @@ public interface PostV2Api { @Operation(summary = "모임 게시글 작성") @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.", content = @Content), + @ApiResponse(responseCode = "201", description = "성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), + @ApiResponse(responseCode = "403", description = "권한이 없습니다.", content = @Content), }) ResponseEntity createPost( - @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal); + @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal); @Operation(summary = "모임 게시글 목록 조회") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), }) @Parameters({ - @Parameter(name = "page", description = "페이지, default = 1", example = "1", schema = @Schema(type = "integer", format = "int32")), - @Parameter(name = "take", description = "가져올 데이터 개수, default = 12", example = "50", schema = @Schema(type = "integer", format = "int32")), - @Parameter(name = "meetingId", description = "모임 id", example = "0", schema = @Schema(type = "integer", format = "int32"))}) + @Parameter(name = "page", description = "페이지, default = 1", example = "1", schema = @Schema(type = "integer", format = "int32")), + @Parameter(name = "take", description = "가져올 데이터 개수, default = 12", example = "50", schema = @Schema(type = "integer", format = "int32")), + @Parameter(name = "meetingId", description = "모임 id", example = "0", schema = @Schema(type = "integer", format = "int32"))}) ResponseEntity getPosts( - @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, Principal principal); + @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, + Principal principal); + + @Operation(summary = "게시글에서 멘션하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity mentionUserInPost( + @Valid @RequestBody PostV2MentionUserInPostRequestDto requestBody, Principal principal); + } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java index b0b85e42..e394ca93 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java @@ -1,13 +1,13 @@ package org.sopt.makers.crew.main.post.v2; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.security.Principal; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.util.UserUtil; import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; import org.sopt.makers.crew.main.post.v2.service.PostV2Service; @@ -32,7 +32,7 @@ public class PostV2Controller implements PostV2Api { @PostMapping() @ResponseStatus(HttpStatus.CREATED) public ResponseEntity createPost( - @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal) { + @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal) { Integer userId = UserUtil.getUserId(principal); return ResponseEntity.ok(postV2Service.createPost(requestBody, userId)); } @@ -41,9 +41,20 @@ public ResponseEntity createPost( @GetMapping() @ResponseStatus(HttpStatus.OK) public ResponseEntity getPosts( - @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, - Principal principal) { + @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, + Principal principal) { Integer userId = UserUtil.getUserId(principal); return ResponseEntity.ok(postV2Service.getPosts(queryCommand, userId)); } + + + @Override + @PostMapping("/mention") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity mentionUserInPost( + @Valid @RequestBody PostV2MentionUserInPostRequestDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + postV2Service.mentionUserInPost(requestBody, userId); + return ResponseEntity.status(HttpStatus.OK).build(); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java new file mode 100644 index 00000000..e12f6fc1 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java @@ -0,0 +1,25 @@ +package org.sopt.makers.crew.main.post.v2.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "모임 게시글에서 유저 언급 request body dto") +public class PostV2MentionUserInPostRequestDto { + + @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") + @NotEmpty + private final List userIds; + + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private final Integer postId; + + @Schema(example = "멘션내용~~", required = true, description = "멘션 내용") + private final String content; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java index 46bef28f..e73f56c5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java @@ -2,6 +2,7 @@ import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; @@ -10,4 +11,6 @@ public interface PostV2Service { PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, Integer userId); PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId); + + void mentionUserInPost(PostV2MentionUserInPostRequestDto requestBody, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java index 339b32b2..8e37bd8c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java @@ -2,6 +2,7 @@ import static java.util.stream.Collectors.toList; import static org.sopt.makers.crew.main.common.response.ErrorStatus.FORBIDDEN_EXCEPTION; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_POST_PUSH_NOTIFICATION_TITLE; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; @@ -22,6 +23,7 @@ import org.sopt.makers.crew.main.internal.notification.dto.PushNotificationRequestDto; import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; @@ -54,13 +56,13 @@ public class PostV2ServiceImpl implements PostV2Service { @Override @Transactional public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, - Integer userId) { + Integer userId) { Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); User user = userRepository.findByIdOrThrow(userId); boolean isInMeeting = meeting.getAppliedInfo().stream() - .anyMatch(apply -> apply.getUserId().equals(userId) - && apply.getStatus() == EnApplyStatus.APPROVE); + .anyMatch(apply -> apply.getUserId().equals(userId) + && apply.getStatus() == EnApplyStatus.APPROVE); boolean isMeetingCreator = meeting.getUserId().equals(userId); @@ -69,30 +71,30 @@ public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBod } Post post = Post.builder() - .title(requestBody.getTitle()) - .user(user) - .contents(requestBody.getContents()) - .images(requestBody.getImages()) - .meeting(meeting) - .build(); + .title(requestBody.getTitle()) + .user(user) + .contents(requestBody.getContents()) + .images(requestBody.getImages()) + .meeting(meeting) + .build(); Post savedPost = postRepository.save(post); List userIdList = applyRepository.findAllByMeetingIdAndStatus(meeting.getId(), - EnApplyStatus.APPROVE) - .stream() - .map(apply -> String.valueOf(apply.getUser().getOrgId())) - .collect(toList()); + EnApplyStatus.APPROVE) + .stream() + .map(apply -> String.valueOf(apply.getUser().getOrgId())) + .collect(toList()); String[] userIds = userIdList.toArray(new String[0]); String pushNotificationContent = String.format("[%s의 새 글] : \"%s\"", - user.getName(), post.getTitle()); + user.getName(), post.getTitle()); String pushNotificationWeblink = pushWebUrl + "/detail?id=" + meeting.getId(); PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_POST_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + NEW_POST_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); pushNotificationService.sendPushNotification(pushRequestDto); @@ -103,11 +105,38 @@ public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBod @Transactional(readOnly = true) public PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId) { Page meetingPostListDtos = postRepository.findPostList(queryCommand, - PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), userId); + PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), userId); - PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), queryCommand.getTake()); - PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, (int) meetingPostListDtos.getTotalElements()); + PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), + queryCommand.getTake()); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, + (int) meetingPostListDtos.getTotalElements()); return PostV2GetPostsResponseDto.of(meetingPostListDtos.getContent(), pageMetaDto); } + + @Override + public void mentionUserInPost(PostV2MentionUserInPostRequestDto requestBody, Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + + String pushNotificationContent = String.format("[%s의 글] : \"%s\"", + user.getName(), post.getTitle()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + String[] userOrgIds = userRepository.findByIdIn(requestBody.getUserIds()) + .stream() + .map(mentionedUser -> String.valueOf(mentionedUser.getOrgId())) + .toArray(String[]::new); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( + userOrgIds, + NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), + pushNotificationWeblink + ); + + pushNotificationService.sendPushNotification(pushRequestDto); + } } From 118eb1ced642341e6bc9efa52ae209a9bf7db9aa Mon Sep 17 00:00:00 2001 From: YeongWoooo Date: Tue, 9 Jul 2024 14:41:16 +0900 Subject: [PATCH 24/35] =?UTF-8?q?[feature/#214]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20V2=20API=20=EA=B5=AC=ED=98=84=20(#243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [REFACTOR] 테스트코드 그룹화 * [FEAT] 댓글 수정 V2 API 구현 * [CHORE] 댓글 수정 V1 API deprecated 처리 * [BUILD] 댓글 관련 API forward pattern 변경 * [REFACTOR] Time 객체 추가 * [CHORE] 작성자 검증 로직 Comment 내부에 구현 * [TEST] StubTime 추가 및 테스트 코드 수정 --------- Co-authored-by: mikekks --- docker-compose.yml | 8 +- .../crew/main/comment/v2/CommentV2Api.java | 76 +++-- .../main/comment/v2/CommentV2Controller.java | 86 +++-- ...mmentV2MentionUserInCommentRequestDto.java | 18 +- .../CommentV2UpdateCommentBodyDto.java | 19 ++ .../CommentV2UpdateCommentResponseDto.java | 24 ++ .../comment/v2/service/CommentV2Service.java | 16 +- .../v2/service/CommentV2ServiceImpl.java | 295 ++++++++++-------- .../crew/main/common/util/RealTime.java | 15 + .../makers/crew/main/common/util/Time.java | 8 + .../crew/main/entity/comment/Comment.java | 186 ++++++----- .../crew/main/entity/user/UserRepository.java | 21 +- .../notification/PushNotificationEnums.java | 16 +- .../makers/crew/main/post/v2/PostV2Api.java | 58 ++-- .../crew/main/post/v2/PostV2Controller.java | 64 ++-- .../PostV2MentionUserInPostRequestDto.java | 18 +- .../main/post/v2/service/PostV2Service.java | 6 +- .../post/v2/service/PostV2ServiceImpl.java | 204 ++++++------ .../v2/service/CommentV2ServiceTest.java | 222 ++++++++----- .../crew/main/common/util/StubTime.java | 15 + .../src/comment/v1/comment-v1.controller.ts | 4 + 21 files changed, 796 insertions(+), 583 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2UpdateCommentBodyDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2UpdateCommentResponseDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/common/util/RealTime.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/common/util/Time.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/common/util/StubTime.java diff --git a/docker-compose.yml b/docker-compose.yml index 8b117b64..9da67955 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -138,8 +138,10 @@ services: caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_7: /comment/v2 caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_8: /notice/v2 + caddy.route_8: /comment/v2/* caddy.route_8.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_9: /notice/v2 + caddy.route_9.reverse_proxy: "{{ upstreams 4000 }}" nestjs-blue: image: makerscrew/server:latest @@ -231,8 +233,10 @@ services: caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_7: /comment/v2 caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_8: /notice/v2 + caddy.route_8: /comment/v2/* caddy.route_8.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_9: /notice/v2 + caddy.route_9.reverse_proxy: "{{ upstreams 4000 }}" networks: caddy: diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java index eceef123..69e649d3 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java @@ -4,11 +4,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; + import java.security.Principal; + import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2UpdateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -17,35 +21,45 @@ public interface CommentV2Api { - @Operation(summary = "모임 게시글 댓글 작성") - @ResponseStatus(HttpStatus.CREATED) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - }) - public ResponseEntity createComment( - @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal); - - @Operation(summary = "댓글 신고하기") - @ResponseStatus(HttpStatus.CREATED) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - }) - public ResponseEntity reportComment( - @PathVariable Integer commentId, Principal principal); - - @Operation(summary = "모임 게시글 댓글 삭제") - @ResponseStatus(HttpStatus.NO_CONTENT) - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "성공"), - }) - public ResponseEntity deleteComment(Principal principal, @PathVariable Integer commentId); - - @Operation(summary = "댓글에서 유저 멘션") - @ResponseStatus(HttpStatus.OK) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - }) - public ResponseEntity mentionUserInComment( - @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, - Principal principal); + @Operation(summary = "모임 게시글 댓글 작성") + @ResponseStatus(HttpStatus.CREATED) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + }) + ResponseEntity createComment( + @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal); + + @Operation(summary = "모임 게시글 댓글 수정") + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity updateComment( + @PathVariable Integer commentId, + @Valid @RequestBody CommentV2UpdateCommentBodyDto requestBody, + Principal principal); + + @Operation(summary = "댓글 신고하기") + @ResponseStatus(HttpStatus.CREATED) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + }) + ResponseEntity reportComment( + @PathVariable Integer commentId, Principal principal); + + @Operation(summary = "모임 게시글 댓글 삭제") + @ResponseStatus(HttpStatus.NO_CONTENT) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "성공"), + }) + ResponseEntity deleteComment(Principal principal, @PathVariable Integer commentId); + + @Operation(summary = "댓글에서 유저 멘션") + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity mentionUserInComment( + @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, + Principal principal); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java index 1c189c33..be554fa6 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java @@ -2,19 +2,26 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; + import java.security.Principal; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2UpdateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.service.CommentV2Service; import org.sopt.makers.crew.main.common.util.UserUtil; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -25,43 +32,54 @@ @Tag(name = "댓글/대댓글") public class CommentV2Controller implements CommentV2Api { - private final CommentV2Service commentV2Service; + private final CommentV2Service commentV2Service; + + @Override + @PostMapping() + public ResponseEntity createComment( + @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); + } - @Override - @PostMapping() - public ResponseEntity createComment( - @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); - } + @Override + @PutMapping("/{commentId}") + public ResponseEntity updateComment( + @PathVariable Integer commentId, + @Valid @RequestBody CommentV2UpdateCommentBodyDto requestBody, + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok( + commentV2Service.updateComment(commentId, requestBody.getContents(), userId)); + } - @Override - @PostMapping("/{commentId}/report") - public ResponseEntity reportComment( - @PathVariable Integer commentId, Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(commentV2Service.reportComment(commentId, userId)); - } + @Override + @PostMapping("/{commentId}/report") + public ResponseEntity reportComment( + @PathVariable Integer commentId, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(commentV2Service.reportComment(commentId, userId)); + } - @Override - @DeleteMapping("/{commentId}") - public ResponseEntity deleteComment( - Principal principal, - @PathVariable Integer commentId) { - Integer userId = UserUtil.getUserId(principal); + @Override + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment( + Principal principal, + @PathVariable Integer commentId) { + Integer userId = UserUtil.getUserId(principal); - commentV2Service.deleteComment(commentId, userId); + commentV2Service.deleteComment(commentId, userId); - return ResponseEntity.noContent().build(); - } + return ResponseEntity.noContent().build(); + } - @Override - @PostMapping("/mention") - public ResponseEntity mentionUserInComment( - @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, - Principal principal) { - Integer userId = UserUtil.getUserId(principal); - commentV2Service.mentionUserInComment(requestBody, userId); - return ResponseEntity.status(HttpStatus.OK).build(); - } + @Override + @PostMapping("/mention") + public ResponseEntity mentionUserInComment( + @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + commentV2Service.mentionUserInComment(requestBody, userId); + return ResponseEntity.status(HttpStatus.OK).build(); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java index 48db818d..5d0c14e0 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java @@ -3,7 +3,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; + import java.util.List; + import lombok.AllArgsConstructor; import lombok.Getter; @@ -12,14 +14,14 @@ @Schema(description = "댓글에서 유저 언급 request body dto") public class CommentV2MentionUserInCommentRequestDto { - @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") - @NotEmpty - private List userIds; + @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") + @NotEmpty + private List userIds; - @Schema(example = "1", required = true, description = "게시글 ID") - @NotNull - private Integer postId; + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private Integer postId; - @Schema(example = "멘션내용~~", required = true, description = "멘션 내용") - private String content; + @Schema(example = "멘션내용~~", required = true, description = "멘션 내용") + private String content; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2UpdateCommentBodyDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2UpdateCommentBodyDto.java new file mode 100644 index 00000000..f702ed16 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2UpdateCommentBodyDto.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.comment.v2.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "댓글 업데이트 request body dto") +public class CommentV2UpdateCommentBodyDto { + + @Schema(example = "알고보면 쓸데있는 개발 프로세스", description = "댓글 내용") + @NotEmpty + private String contents; + +} \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2UpdateCommentResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2UpdateCommentResponseDto.java new file mode 100644 index 00000000..fe7de072 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2UpdateCommentResponseDto.java @@ -0,0 +1,24 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class CommentV2UpdateCommentResponseDto { + + /** + * 생성된 댓글 id + */ + private Integer id; + + /** + * 댓글 내용 + */ + private String contents; + + /** + * 업데이트 시각 + */ + private String updateDate; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index c370dcf5..cf67bc50 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -4,18 +4,22 @@ import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; public interface CommentV2Service { - CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, - Integer userId); + CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, + Integer userId); - CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) - throws BadRequestException; + CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException; - void deleteComment(Integer commentId, Integer userId) throws ForbiddenException; + void deleteComment(Integer commentId, Integer userId) throws ForbiddenException; - void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, Integer userId); + void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, Integer userId); + + CommentV2UpdateCommentResponseDto updateComment(Integer commentId, String contents, + Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index a22a2ad8..3e966983 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -1,18 +1,20 @@ package org.sopt.makers.crew.main.comment.v2.service; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_PUSH_NOTIFICATION_TITLE; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.*; import java.util.Optional; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; -import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.common.response.ErrorStatus; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.common.util.Time; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.post.Post; @@ -32,133 +34,160 @@ @Transactional(readOnly = true) public class CommentV2ServiceImpl implements CommentV2Service { - private final PostRepository postRepository; - private final UserRepository userRepository; - private final CommentRepository commentRepository; - private final ReportRepository reportRepository; - private final PushNotificationService pushNotificationService; - - @Value("${push-notification.web-url}") - private String pushWebUrl; - - /** - * 모임 게시글 댓글 작성 - * - * @throws 400 존재하지 않는 게시글일 떄 - * @apiNote 모임에 속한 유저만 작성 가능 - */ - @Override - @Transactional - public CommentV2CreateCommentResponseDto createComment( - CommentV2CreateCommentBodyDto requestBody, - Integer userId) { - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - User user = userRepository.findByIdOrThrow(userId); - - Comment comment = Comment.builder() - .contents(requestBody.getContents()) - .user(user) - .post(post) - .build(); - - Comment savedComment = commentRepository.save(comment); - - User PostWriter = post.getUser(); - String[] userIds = {String.valueOf(PostWriter.getOrgId())}; - - String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", - user.getName(), requestBody.getContents()); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); - - pushNotificationService.sendPushNotification(pushRequestDto); - - return CommentV2CreateCommentResponseDto.of(savedComment.getId()); - } - - /** - * 댓글 신고하기 - * - * @param commentId 댓글 신고할 댓글 id - * @param userId 신고하는 유저 id - * @return 신고 ID - * @throws BadRequestException 이미 신고한 댓글일 때 - * @apiNote 댓글 신고는 한 댓글당 한번만 가능 - */ - @Override - @Transactional - public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) - throws BadRequestException { - Comment comment = commentRepository.findByIdOrThrow(commentId); - User user = userRepository.findByIdOrThrow(userId); - - Optional existingReport = reportRepository.findByCommentAndUser(comment, user); - - if (existingReport.isPresent()) { - throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); - } - - Report report = Report.builder() - .comment(comment) - .user(user) - .build(); - - Report savedReport = reportRepository.save(report); - - return CommentV2ReportCommentResponseDto.of(savedReport.getId()); - } - - /** - * 모임 게시글 댓글 삭제 - * - * @throws ForbiddenException 댓글 작성자가 아닐 때 - * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 - */ - @Override - @Transactional - public void deleteComment(Integer commentId, Integer userId) throws ForbiddenException { - Comment comment = commentRepository.findByIdOrThrow(commentId); - - if (!comment.getUserId().equals(userId)) { - throw new ForbiddenException(); - } - - Post post = comment.getPost(); - - post.decreaseCommentCount(); - commentRepository.delete(comment); - } - - @Override - public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, - Integer userId) { - User user = userRepository.findByIdOrThrow(userId); - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - - String pushNotificationContent = String.format("[%s님이 회원님을 언급했어요.] : \"%s\"", - user.getName(), requestBody.getContent()); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - - String[] userOrgIds = userRepository.findByIdIn(requestBody.getUserIds()) - .stream() - .map(mentionedUser -> String.valueOf(mentionedUser.getOrgId())) - .toArray(String[]::new); - - String newCommentMentionPushNotificationTitle = String.format( - NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), user.getName()); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( - userOrgIds, - newCommentMentionPushNotificationTitle, - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), - pushNotificationWeblink - ); - - pushNotificationService.sendPushNotification(pushRequestDto); - } + private final PostRepository postRepository; + private final UserRepository userRepository; + private final CommentRepository commentRepository; + private final ReportRepository reportRepository; + private final PushNotificationService pushNotificationService; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + private final Time time; + + /** + * 모임 게시글 댓글 작성 + * + * @throws 400 존재하지 않는 게시글일 떄 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, + Integer userId) { + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + User user = userRepository.findByIdOrThrow(userId); + + Comment comment = Comment.builder() + .contents(requestBody.getContents()) + .user(user) + .post(post) + .build(); + + Comment savedComment = commentRepository.save(comment); + + User PostWriter = post.getUser(); + String[] userIds = {String.valueOf(PostWriter.getOrgId())}; + + String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", + user.getName(), requestBody.getContents()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + + return CommentV2CreateCommentResponseDto.of(savedComment.getId()); + } + + /** + * 모임 게시글 댓글 수정 + * + * @param commentId 수정할 댓글 ID + * @param contents 수정할 내용 + * @param userId 수정하는 유저 ID + * @return 수정된 댓글 정보 + */ + @Override + @Transactional + public CommentV2UpdateCommentResponseDto updateComment(Integer commentId, + String contents, Integer userId) { + // 1. id를 기반으로 comment를 찾는다. + Comment comment = commentRepository.findByIdOrThrow(commentId); + + // 2. comment의 user_id와 userId가 같은지 확인한다. + comment.isWriter(userId); + + // 3. comment의 contents를 수정한다. + comment.updateContents(contents, time.now()); + + // 4. 수정된 comment의 id, contents, updatedDate를 반환한다. + return CommentV2UpdateCommentResponseDto.of(comment.getId(), comment.getContents(), + String.valueOf(comment.getUpdatedDate())); + } + + /** + * 댓글 신고하기 + * + * @param commentId 댓글 신고할 댓글 id + * @param userId 신고하는 유저 id + * @return 신고 ID + * @throws BadRequestException 이미 신고한 댓글일 때 + * @apiNote 댓글 신고는 한 댓글당 한번만 가능 + */ + @Override + @Transactional + public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException { + Comment comment = commentRepository.findByIdOrThrow(commentId); + User user = userRepository.findByIdOrThrow(userId); + + Optional existingReport = reportRepository.findByCommentAndUser(comment, user); + + if (existingReport.isPresent()) { + throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); + } + + Report report = Report.builder() + .comment(comment) + .user(user) + .build(); + + Report savedReport = reportRepository.save(report); + + return CommentV2ReportCommentResponseDto.of(savedReport.getId()); + } + + /** + * 모임 게시글 댓글 삭제 + * + * @throws ForbiddenException 댓글 작성자가 아닐 때 + * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 + */ + @Override + @Transactional + public void deleteComment(Integer commentId, Integer userId) throws ForbiddenException { + Comment comment = commentRepository.findByIdOrThrow(commentId); + + if (!comment.getUserId().equals(userId)) { + throw new ForbiddenException(); + } + + Post post = comment.getPost(); + + post.decreaseCommentCount(); + commentRepository.delete(comment); + } + + @Override + public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, + Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + + String pushNotificationContent = String.format("[%s님이 회원님을 언급했어요.] : \"%s\"", + user.getName(), requestBody.getContent()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + String[] userOrgIds = userRepository.findByIdIn(requestBody.getUserIds()) + .stream() + .map(mentionedUser -> String.valueOf(mentionedUser.getOrgId())) + .toArray(String[]::new); + + String newCommentMentionPushNotificationTitle = String.format( + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), user.getName()); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( + userOrgIds, + newCommentMentionPushNotificationTitle, + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), + pushNotificationWeblink + ); + + pushNotificationService.sendPushNotification(pushRequestDto); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/RealTime.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/RealTime.java new file mode 100644 index 00000000..e10bac2d --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/RealTime.java @@ -0,0 +1,15 @@ +package org.sopt.makers.crew.main.common.util; + +import java.time.LocalDateTime; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile({"local", "dev", "prod"}) +public class RealTime implements Time { + @Override + public LocalDateTime now() { + return LocalDateTime.now(); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/Time.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/Time.java new file mode 100644 index 00000000..115a50b2 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/Time.java @@ -0,0 +1,8 @@ +package org.sopt.makers.crew.main.common.util; + +import java.time.LocalDateTime; + +public interface Time { + LocalDateTime now(); + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index f04312af..59e9ffd2 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -1,6 +1,7 @@ package org.sopt.makers.crew.main.entity.comment; -import jakarta.persistence.CascadeType; +import static org.sopt.makers.crew.main.common.response.ErrorStatus.*; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -10,16 +11,15 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDateTime; -import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; + +import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.entity.post.Post; -import org.sopt.makers.crew.main.entity.report.Report; import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; @@ -39,44 +39,44 @@ public class Comment { @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; - /** - * 댓글 내용 - */ - @Column(nullable = false) - private String contents; - - /** - * 댓글 깊이 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int depth; - - /** - * 댓글 순서 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int order; - - /** - * 작성일 - */ - @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") - @CreatedDate - private LocalDateTime createdDate; - - /** - * 수정일 - */ - @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") - @LastModifiedDate - private LocalDateTime updatedDate; - - /** - * 작성자 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; + /** + * 댓글 내용 + */ + @Column(nullable = false) + private String contents; + + /** + * 댓글 깊이 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int depth; + + /** + * 댓글 순서 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int order; + + /** + * 작성일 + */ + @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime createdDate; + + /** + * 수정일 + */ + @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") + @LastModifiedDate + private LocalDateTime updatedDate; + + /** + * 작성자 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; /** * 작성자의 고유 식별자 @@ -84,31 +84,31 @@ public class Comment { @Column(insertable = false, updatable = false) private Integer userId; - /** - * 댓글이 속한 게시글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "postId", nullable = false) - private Post post; - - /** - * 댓글이 속한 게시글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int postId; - - /** - * 댓글에 대한 좋아요 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int likeCount; - - /** - * 부모 댓글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parentId") - private Comment parent; + /** + * 댓글이 속한 게시글 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "postId", nullable = false) + private Post post; + + /** + * 댓글이 속한 게시글의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer postId; + + /** + * 댓글에 대한 좋아요 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int likeCount; + + /** + * 부모 댓글 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parentId") + private Comment parent; /** * 부모 댓글의 고유 식별자 @@ -116,36 +116,30 @@ public class Comment { @Column(insertable = false, updatable = false) private Integer parentId; - /** - * 자식 댓글 목록 - */ - @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE) - private List children; - - /** - * 댓글에 대한 신고 목록 - */ - @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) - private List reports; - - @Builder - public Comment(String contents, User user, Post post, Comment parent) { - this.contents = contents; - this.user = user; - this.userId = user.getId(); - this.post = post; - this.parent = parent; - this.depth = 0; - this.order = 0; - this.likeCount = 0; - this.post.addComment(this); - } - public void addChildrenComment(Comment comment) { - this.children.add(comment); + @Builder + public Comment(String contents, User user, Post post, Comment parent) { + this.contents = contents; + this.user = user; + this.userId = user.getId(); + this.post = post; + this.parent = parent; + this.depth = 0; + this.order = 0; + this.likeCount = 0; + this.post.addComment(this); + } + + public void updateContents(String contents,LocalDateTime updatedDate) { + this.contents = contents; + this.updatedDate = updatedDate; + } + + public void isWriter(Integer userId){ + boolean isWriter = this.userId.equals(userId); + if (!isWriter) { + throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); } + } - public void addReport(Report report) { - this.reports.add(report); - } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java index 54ff351d..0f390f1c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java @@ -4,23 +4,24 @@ import java.util.List; import java.util.Optional; + import org.sopt.makers.crew.main.common.exception.NoContentException; import org.sopt.makers.crew.main.common.exception.UnAuthorizedException; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { - Optional findByOrgId(Integer orgId); + Optional findByOrgId(Integer orgId); - default User findByIdOrThrow(Integer userId) { - return findById(userId).orElseThrow(() -> new UnAuthorizedException()); - } + default User findByIdOrThrow(Integer userId) { + return findById(userId).orElseThrow(() -> new UnAuthorizedException()); + } - default User findByOrgIdOrThrow(Integer orgUserId) { - return findByOrgId(orgUserId).orElseThrow( - () -> new NoContentException( - NO_CONTENT_EXCEPTION.getErrorCode())); //유저가 아직 모임 서비스를 이용 전이기 때문에 - } + default User findByOrgIdOrThrow(Integer orgUserId) { + return findByOrgId(orgUserId).orElseThrow( + () -> new NoContentException( + NO_CONTENT_EXCEPTION.getErrorCode())); //유저가 아직 모임 서비스를 이용 전이기 때문에 + } - List findByIdIn(List userIds); + List findByIdIn(List userIds); } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java index b7d36d65..999ff836 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java +++ b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java @@ -7,14 +7,14 @@ @Getter @RequiredArgsConstructor(access = AccessLevel.PROTECTED) public enum PushNotificationEnums { - PUSH_NOTIFICATION_ACTION("send"), + PUSH_NOTIFICATION_ACTION("send"), - PUSH_NOTIFICATION_CATEGORY("NEWS"), + PUSH_NOTIFICATION_CATEGORY("NEWS"), - NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."), - NEW_COMMENT_PUSH_NOTIFICATION_TITLE("📢내가 작성한 모임 피드에 새로운 댓글이 달렸어요."), - NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE("💬%s님이 회원님을 언급했어요."), - NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE("✏️모임 피드에서 회원님이 언급됐어요."), - ; - private final String value; + NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."), + NEW_COMMENT_PUSH_NOTIFICATION_TITLE("📢내가 작성한 모임 피드에 새로운 댓글이 달렸어요."), + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE("💬%s님이 회원님을 언급했어요."), + NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE("✏️모임 피드에서 회원님이 언급됐어요."), + ; + private final String value; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java index d1eeffc6..b300d1f8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java @@ -9,7 +9,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; + import java.security.Principal; + import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; @@ -22,33 +24,33 @@ @Tag(name = "게시글") public interface PostV2Api { - @Operation(summary = "모임 게시글 작성") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.", content = @Content), - }) - ResponseEntity createPost( - @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal); - - @Operation(summary = "모임 게시글 목록 조회") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), - }) - @Parameters({ - @Parameter(name = "page", description = "페이지, default = 1", example = "1", schema = @Schema(type = "integer", format = "int32")), - @Parameter(name = "take", description = "가져올 데이터 개수, default = 12", example = "50", schema = @Schema(type = "integer", format = "int32")), - @Parameter(name = "meetingId", description = "모임 id", example = "0", schema = @Schema(type = "integer", format = "int32"))}) - ResponseEntity getPosts( - @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, - Principal principal); - - @Operation(summary = "게시글에서 멘션하기") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - }) - ResponseEntity mentionUserInPost( - @Valid @RequestBody PostV2MentionUserInPostRequestDto requestBody, Principal principal); + @Operation(summary = "모임 게시글 작성") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), + @ApiResponse(responseCode = "403", description = "권한이 없습니다.", content = @Content), + }) + ResponseEntity createPost( + @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal); + + @Operation(summary = "모임 게시글 목록 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), + }) + @Parameters({ + @Parameter(name = "page", description = "페이지, default = 1", example = "1", schema = @Schema(type = "integer", format = "int32")), + @Parameter(name = "take", description = "가져올 데이터 개수, default = 12", example = "50", schema = @Schema(type = "integer", format = "int32")), + @Parameter(name = "meetingId", description = "모임 id", example = "0", schema = @Schema(type = "integer", format = "int32"))}) + ResponseEntity getPosts( + @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, + Principal principal); + + @Operation(summary = "게시글에서 멘션하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity mentionUserInPost( + @Valid @RequestBody PostV2MentionUserInPostRequestDto requestBody, Principal principal); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java index e394ca93..b35a7947 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java @@ -2,8 +2,11 @@ import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; + import java.security.Principal; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.common.util.UserUtil; import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; @@ -26,35 +29,34 @@ @RequiredArgsConstructor public class PostV2Controller implements PostV2Api { - private final PostV2Service postV2Service; - - @Override - @PostMapping() - @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity createPost( - @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(postV2Service.createPost(requestBody, userId)); - } - - @Override - @GetMapping() - @ResponseStatus(HttpStatus.OK) - public ResponseEntity getPosts( - @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, - Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(postV2Service.getPosts(queryCommand, userId)); - } - - - @Override - @PostMapping("/mention") - @ResponseStatus(HttpStatus.OK) - public ResponseEntity mentionUserInPost( - @Valid @RequestBody PostV2MentionUserInPostRequestDto requestBody, Principal principal) { - Integer userId = UserUtil.getUserId(principal); - postV2Service.mentionUserInPost(requestBody, userId); - return ResponseEntity.status(HttpStatus.OK).build(); - } + private final PostV2Service postV2Service; + + @Override + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createPost( + @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(postV2Service.createPost(requestBody, userId)); + } + + @Override + @GetMapping() + @ResponseStatus(HttpStatus.OK) + public ResponseEntity getPosts( + @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(postV2Service.getPosts(queryCommand, userId)); + } + + @Override + @PostMapping("/mention") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity mentionUserInPost( + @Valid @RequestBody PostV2MentionUserInPostRequestDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + postV2Service.mentionUserInPost(requestBody, userId); + return ResponseEntity.status(HttpStatus.OK).build(); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java index e12f6fc1..5e04e711 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java @@ -3,7 +3,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; + import java.util.List; + import lombok.AllArgsConstructor; import lombok.Getter; @@ -12,14 +14,14 @@ @Schema(description = "모임 게시글에서 유저 언급 request body dto") public class PostV2MentionUserInPostRequestDto { - @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") - @NotEmpty - private final List userIds; + @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") + @NotEmpty + private final List userIds; - @Schema(example = "1", required = true, description = "게시글 ID") - @NotNull - private final Integer postId; + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private final Integer postId; - @Schema(example = "멘션내용~~", required = true, description = "멘션 내용") - private final String content; + @Schema(example = "멘션내용~~", required = true, description = "멘션 내용") + private final String content; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java index e73f56c5..3a9ff793 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java @@ -8,9 +8,9 @@ public interface PostV2Service { - PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, Integer userId); + PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, Integer userId); - PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId); + PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId); - void mentionUserInPost(PostV2MentionUserInPostRequestDto requestBody, Integer userId); + void mentionUserInPost(PostV2MentionUserInPostRequestDto requestBody, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java index 8e37bd8c..ecea9dc7 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java @@ -7,7 +7,9 @@ import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; import java.util.List; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; @@ -38,105 +40,105 @@ @Transactional(readOnly = true) public class PostV2ServiceImpl implements PostV2Service { - private final MeetingRepository meetingRepository; - private final UserRepository userRepository; - private final PostRepository postRepository; - private final ApplyRepository applyRepository; - private final PushNotificationService pushNotificationService; - - @Value("${push-notification.web-url}") - private String pushWebUrl; - - /** - * 모임 게시글 작성 - * - * @throws 403 모임에 속한 유저가 아닌 경우 - * @apiNote 모임에 속한 유저만 작성 가능 - */ - @Override - @Transactional - public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, - Integer userId) { - Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); - User user = userRepository.findByIdOrThrow(userId); - - boolean isInMeeting = meeting.getAppliedInfo().stream() - .anyMatch(apply -> apply.getUserId().equals(userId) - && apply.getStatus() == EnApplyStatus.APPROVE); - - boolean isMeetingCreator = meeting.getUserId().equals(userId); - - if (isInMeeting == false && isMeetingCreator == false) { - throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); - } - - Post post = Post.builder() - .title(requestBody.getTitle()) - .user(user) - .contents(requestBody.getContents()) - .images(requestBody.getImages()) - .meeting(meeting) - .build(); - - Post savedPost = postRepository.save(post); - - List userIdList = applyRepository.findAllByMeetingIdAndStatus(meeting.getId(), - EnApplyStatus.APPROVE) - .stream() - .map(apply -> String.valueOf(apply.getUser().getOrgId())) - .collect(toList()); - - String[] userIds = userIdList.toArray(new String[0]); - String pushNotificationContent = String.format("[%s의 새 글] : \"%s\"", - user.getName(), post.getTitle()); - String pushNotificationWeblink = pushWebUrl + "/detail?id=" + meeting.getId(); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_POST_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); - - pushNotificationService.sendPushNotification(pushRequestDto); - - return PostV2CreatePostResponseDto.of(savedPost.getId()); - } - - @Override - @Transactional(readOnly = true) - public PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId) { - Page meetingPostListDtos = postRepository.findPostList(queryCommand, - PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), userId); - - PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), - queryCommand.getTake()); - PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, - (int) meetingPostListDtos.getTotalElements()); - - return PostV2GetPostsResponseDto.of(meetingPostListDtos.getContent(), pageMetaDto); - } - - @Override - public void mentionUserInPost(PostV2MentionUserInPostRequestDto requestBody, Integer userId) { - User user = userRepository.findByIdOrThrow(userId); - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - - String pushNotificationContent = String.format("[%s의 글] : \"%s\"", - user.getName(), post.getTitle()); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - - String[] userOrgIds = userRepository.findByIdIn(requestBody.getUserIds()) - .stream() - .map(mentionedUser -> String.valueOf(mentionedUser.getOrgId())) - .toArray(String[]::new); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( - userOrgIds, - NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), - pushNotificationWeblink - ); - - pushNotificationService.sendPushNotification(pushRequestDto); - } + private final MeetingRepository meetingRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + private final ApplyRepository applyRepository; + private final PushNotificationService pushNotificationService; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + /** + * 모임 게시글 작성 + * + * @throws 403 모임에 속한 유저가 아닌 경우 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, + Integer userId) { + Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); + User user = userRepository.findByIdOrThrow(userId); + + boolean isInMeeting = meeting.getAppliedInfo().stream() + .anyMatch(apply -> apply.getUserId().equals(userId) + && apply.getStatus() == EnApplyStatus.APPROVE); + + boolean isMeetingCreator = meeting.getUserId().equals(userId); + + if (isInMeeting == false && isMeetingCreator == false) { + throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); + } + + Post post = Post.builder() + .title(requestBody.getTitle()) + .user(user) + .contents(requestBody.getContents()) + .images(requestBody.getImages()) + .meeting(meeting) + .build(); + + Post savedPost = postRepository.save(post); + + List userIdList = applyRepository.findAllByMeetingIdAndStatus(meeting.getId(), + EnApplyStatus.APPROVE) + .stream() + .map(apply -> String.valueOf(apply.getUser().getOrgId())) + .collect(toList()); + + String[] userIds = userIdList.toArray(new String[0]); + String pushNotificationContent = String.format("[%s의 새 글] : \"%s\"", + user.getName(), post.getTitle()); + String pushNotificationWeblink = pushWebUrl + "/detail?id=" + meeting.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_POST_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + + return PostV2CreatePostResponseDto.of(savedPost.getId()); + } + + @Override + @Transactional(readOnly = true) + public PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId) { + Page meetingPostListDtos = postRepository.findPostList(queryCommand, + PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), userId); + + PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), + queryCommand.getTake()); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, + (int)meetingPostListDtos.getTotalElements()); + + return PostV2GetPostsResponseDto.of(meetingPostListDtos.getContent(), pageMetaDto); + } + + @Override + public void mentionUserInPost(PostV2MentionUserInPostRequestDto requestBody, Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + + String pushNotificationContent = String.format("[%s의 글] : \"%s\"", + user.getName(), post.getTitle()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + String[] userOrgIds = userRepository.findByIdIn(requestBody.getUserIds()) + .stream() + .map(mentionedUser -> String.valueOf(mentionedUser.getOrgId())) + .toArray(String[]::new); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( + userOrgIds, + NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), + pushNotificationWeblink + ); + + pushNotificationService.sendPushNotification(pushRequestDto); + } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java index 83cd386b..f9b1cfac 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java @@ -4,17 +4,24 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.Optional; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; +import org.sopt.makers.crew.main.common.util.Time; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.post.Post; @@ -27,88 +34,135 @@ @ExtendWith(MockitoExtension.class) public class CommentV2ServiceTest { - @InjectMocks - private CommentV2ServiceImpl commentV2Service; - @Mock - private CommentRepository commentRepository; - @Mock - private ReportRepository reportRepository; - @Mock - private UserRepository userRepository; - - private Comment comment; - - private Post post; - - private User user; - - private Report report; - - @BeforeEach - void init() { - user = UserFixture.createStaticUser(); - user.setUserIdForTest(1); - - String[] images = {"image1", "image2", "image3"}; - this.post = Post.builder().user(user).title("title").contents("contents").images(images) - .build(); - this.comment = Comment.builder().contents("contents").post(post).user(user).build(); - post.addComment(this.comment); - - this.report = Report.builder().comment(comment).user(user).build(); - } - - @Test - void 댓글_삭제_성공() { - // given - int initialCommentCount = post.getCommentCount(); - doReturn(comment).when(commentRepository).findByIdOrThrow(any()); - - // when - commentV2Service.deleteComment(comment.getId(), user.getId()); - - // then - Assertions.assertThat(commentRepository.findById(comment.getId())).isEqualTo(Optional.empty()); - Assertions.assertThat(post.getCommentCount()).isEqualTo(initialCommentCount - 1); - } - - @Test - void 댓글_삭제_실패_본인_작성_댓글_아님() { - // given - doReturn(comment).when(commentRepository).findByIdOrThrow(any()); - - // when & then - assertThrows(ForbiddenException.class, () -> { - commentV2Service.deleteComment(comment.getId(), comment.getUser().getId() + 1); - }); - } - - @Test - void 댓글_신고_성공() { - // given - doReturn(comment).when(commentRepository).findByIdOrThrow(any()); - doReturn(user).when(userRepository).findByIdOrThrow(any()); - doReturn(Optional.empty()).when(reportRepository).findByCommentAndUser(any(), any()); - doReturn(report).when(reportRepository).save(any()); - - // when - CommentV2ReportCommentResponseDto result = commentV2Service.reportComment(comment.getId(), - user.getId()); - - // then - Assertions.assertThat(result.getReportId()).isEqualTo(report.getId()); - } - - @Test - void 댓글_신고_실패_이미_신고한_댓글() { - // given - doReturn(comment).when(commentRepository).findByIdOrThrow(any()); - doReturn(user).when(userRepository).findByIdOrThrow(any()); - doReturn(Optional.of(report)).when(reportRepository).findByCommentAndUser(any(), any()); - - // when & then - assertThrows(BadRequestException.class, () -> { - commentV2Service.reportComment(comment.getId(), user.getId()); - }); - } + @InjectMocks + private CommentV2ServiceImpl commentV2Service; + @Mock + private CommentRepository commentRepository; + @Mock + private ReportRepository reportRepository; + @Mock + private UserRepository userRepository; + @Mock + private Time time; + + private Comment comment; + + private Post post; + + private User user; + + private Report report; + + @BeforeEach + void init() { + user = UserFixture.createStaticUser(); + user.setUserIdForTest(1); + + String[] images = {"image1", "image2", "image3"}; + this.post = Post.builder().user(user).title("title").contents("contents").images(images) + .build(); + this.comment = Comment.builder().contents("contents").post(post).user(user).build(); + post.addComment(this.comment); + + this.report = Report.builder().comment(comment).user(user).build(); + } + + @Nested + class 댓글_수정 { + + @Test + void 성공() { + // given + String updatedContents = "updatedContents"; + LocalDateTime expectedUpdatedDate = time.now(); + + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(LocalDateTime.of(2024, 4, 24, 23, 59)).when(time).now(); + + // when + CommentV2UpdateCommentResponseDto result = commentV2Service.updateComment(comment.getId(), + updatedContents, user.getId()); + + // then + Assertions.assertThat(result.getId()).isEqualTo(comment.getId()); + Assertions.assertThat(result.getContents()).isEqualTo(updatedContents); + Assertions.assertThat(LocalDateTime.parse(result.getUpdateDate())) + .isEqualTo(LocalDateTime.of(2024, 4, 24, 23, 59)); + } + + @Test + void 실패_본인_작성_댓글_아님() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + + // when & then + assertThrows(ForbiddenException.class, () -> { + commentV2Service.updateComment(comment.getId(), "updatedContents", + comment.getUser().getId() + 1); + }); + } + } + + @Nested + class 댓글_삭제 { + + @Test + void 성공() { + // given + int initialCommentCount = post.getCommentCount(); + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + + // when + commentV2Service.deleteComment(comment.getId(), user.getId()); + + // then + Assertions.assertThat(commentRepository.findById(comment.getId())) + .isEqualTo(Optional.empty()); + Assertions.assertThat(post.getCommentCount()).isEqualTo(initialCommentCount - 1); + } + + @Test + void 실패_본인_작성_댓글_아님() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + + // when & then + assertThrows(ForbiddenException.class, () -> { + commentV2Service.deleteComment(comment.getId(), comment.getUser().getId() + 1); + }); + } + } + + @Nested + class 댓글_신고 { + + @Test + void 댓글_신고_성공() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(user).when(userRepository).findByIdOrThrow(any()); + doReturn(Optional.empty()).when(reportRepository).findByCommentAndUser(any(), any()); + doReturn(report).when(reportRepository).save(any()); + + // when + CommentV2ReportCommentResponseDto result = commentV2Service.reportComment(comment.getId(), + user.getId()); + + // then + Assertions.assertThat(result.getReportId()).isEqualTo(report.getId()); + } + + @Test + void 댓글_신고_실패_이미_신고한_댓글() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(user).when(userRepository).findByIdOrThrow(any()); + doReturn(Optional.of(report)).when(reportRepository).findByCommentAndUser(any(), any()); + + // when & then + assertThrows(BadRequestException.class, () -> { + commentV2Service.reportComment(comment.getId(), user.getId()); + }); + } + } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/common/util/StubTime.java b/main/src/test/java/org/sopt/makers/crew/main/common/util/StubTime.java new file mode 100644 index 00000000..827894b6 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/common/util/StubTime.java @@ -0,0 +1,15 @@ +package org.sopt.makers.crew.main.common.util; + +import java.time.LocalDateTime; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("test") +public class StubTime implements Time { + @Override + public LocalDateTime now() { + return LocalDateTime.of(2024, 4, 24, 23, 59); + } +} diff --git a/server/src/comment/v1/comment-v1.controller.ts b/server/src/comment/v1/comment-v1.controller.ts index 39ee85b7..8b78afc9 100644 --- a/server/src/comment/v1/comment-v1.controller.ts +++ b/server/src/comment/v1/comment-v1.controller.ts @@ -117,8 +117,12 @@ export class CommentV1Controller { return this.commentV1Service.createPostComment({ body, user }); } + /** + * @deprecated + */ @ApiOperation({ summary: '모임 게시글 댓글 수정', + deprecated: true, }) @ApiOkResponseCommon(CommentV1CreateCommentResponseDto) @ApiResponse({ From 400b9faca285c759bc7b60cb9e0a1dc1da497198 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:59:36 +0900 Subject: [PATCH 25/35] =?UTF-8?q?[FIX]=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=ED=8E=B8=EC=A7=91=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9da67955..73adf8a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,12 +136,14 @@ services: caddy.route_5.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_6: /post/v2 caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_7: /comment/v2 + caddy.route_7: /post/v2/* caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_8: /comment/v2/* + caddy.route_8: /comment/v2 caddy.route_8.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_9: /notice/v2 + caddy.route_9: /comment/v2/* caddy.route_9.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_10: /notice/v2 + caddy.route_10.reverse_proxy: "{{ upstreams 4000 }}" nestjs-blue: image: makerscrew/server:latest @@ -231,12 +233,14 @@ services: caddy.route_5.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_6: /post/v2 caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_7: /comment/v2 + caddy.route_7: /post/v2/* caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_8: /comment/v2/* + caddy.route_8: /comment/v2 caddy.route_8.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_9: /notice/v2 + caddy.route_9: /comment/v2/* caddy.route_9.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_10: /notice/v2 + caddy.route_10.reverse_proxy: "{{ upstreams 4000 }}" networks: caddy: From b4c0040f1c0175d5ba9912408f438c364f33e7a3 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:56:28 +0900 Subject: [PATCH 26/35] =?UTF-8?q?[FEAT]=20=EB=8C=80=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [CHORE] 포맷 변경 * [CHORE] Apply 연관관계 제거 * [CHORE] Apply 연관관계 제거로 인한 메서드 추가 * [CHORE] 주석 및 빌더 일부 수정 * [CHORE] apply 연관관계 제거 * [CHORE] apply 연관관계 제거로 인한 로직 수정 * [CHORE] comment 및 report 객체 연관관계 제거 * [CHORE] 로직 단순화 * [CHORE] 연관관계 제거로 인한 로직 수정 * [CHORE] 연관관계 제거로 인한 로직 수정 * [FEAT] 대댓글 구현을 위한 댓글 생성 로직 구현 * [CHORE] 대댓글 로직 추가로 인한 댓글 삭제 경우 로직 수정 * [CHORE] Dto 이름 컨벤션에 맞게 변경 * [CHORE] 일부 컬럼 수정 및 작성자 검증 로직 수정 * [ADD] 댓글 Dto 생성 * [FEAT] 대댓글 조회를 위한 repo 로직 구현 * [FEAT] 대댓글 조회 관련 스웨거 및 컨트롤러 로직 구현 * [FIX] 오타수정 * [ADD] 요청, 응답 Dto 정의 * [FEAT] 대댓글 조회 기능 구현 * [FEAT] 좋아요 여부 판단을 위한 일급 컬렉션 구현 및 repo 로직 추가 * [ADD] 댓글 작성자 관련 Dto 추가 * [FIX] 컴파일 에러 해결 * [CHORE] 기존 API deprecated 처리 * [CHORE] 대댓글 여부 확인 로직 추가 * [CHORE] 댓글 수 증가 로직 추가 * [TEST] test 코드 수정 * [CHORE] 조회 쿼리 변경 및 댓글일 경우 parentId 처리 로직 추가, 응답값 수정 * [CHORE] 코드리뷰: 매퍼 사용 * [CHORE] 페이지네이션 -> 전체 조회로 변경 * [CHORE] 알림 서버 예외 처리 --- .../crew/main/comment/v2/CommentV2Api.java | 10 + .../main/comment/v2/CommentV2Controller.java | 18 +- .../main/comment/v2/dto/CommentMapper.java | 18 + .../query/CommentV2GetCommentsQueryDto.java | 20 + .../CommentV2CreateCommentBodyDto.java | 18 +- .../comment/v2/dto/response/CommentDto.java | 44 ++ .../CommentV2GetCommentsResponseDto.java | 12 + .../v2/dto/response/CommentWriterDto.java | 19 + .../comment/v2/service/CommentV2Service.java | 3 + .../v2/service/CommentV2ServiceImpl.java | 92 +++- .../main/common/response/ErrorStatus.java | 1 + .../makers/crew/main/entity/apply/Apply.java | 1 - .../main/entity/apply/ApplyRepository.java | 2 + .../entity/apply/ApplySearchRepository.java | 4 +- .../apply/ApplySearchRepositoryImpl.java | 8 +- .../crew/main/entity/comment/Comment.java | 240 ++++----- .../entity/comment/CommentRepository.java | 24 +- .../comment/CommentSearchRepository.java | 9 + .../comment/CommentSearchRepositoryImpl.java | 50 ++ .../crew/main/entity/like/LikeRepository.java | 4 + .../makers/crew/main/entity/like/MyLikes.java | 17 + .../crew/main/entity/meeting/Meeting.java | 377 +++++++------- .../makers/crew/main/entity/post/Post.java | 230 ++++----- .../crew/main/entity/post/PostRepository.java | 2 + .../makers/crew/main/entity/user/User.java | 48 +- .../notification/PushNotificationService.java | 29 +- .../crew/main/meeting/v2/MeetingV2Api.java | 4 +- .../main/meeting/v2/MeetingV2Controller.java | 5 +- ...nd.java => MeetingGetAppliesQueryDto.java} | 4 +- .../MeetingV2GetMeetingBannerResponseDto.java | 2 +- .../meeting/v2/service/MeetingV2Service.java | 4 +- .../v2/service/MeetingV2ServiceImpl.java | 467 +++++++++--------- .../post/v2/service/PostV2ServiceImpl.java | 5 +- .../user/v2/service/UserV2ServiceImpl.java | 77 +-- .../v2/service/CommentV2ServiceTest.java | 16 +- .../v2/repository/ApplyRepositoryTest.java | 12 +- .../v2/service/MeetingV2ServiceTest.java | 8 +- .../src/comment/v1/comment-v1.controller.ts | 1 + 38 files changed, 1094 insertions(+), 811 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/query/CommentV2GetCommentsQueryDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2GetCommentsResponseDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepository.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepositoryImpl.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/like/MyLikes.java rename main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/{MeetingGetApplyListCommand.java => MeetingGetAppliesQueryDto.java} (79%) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java index 69e649d3..a5b798f3 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java @@ -7,14 +7,17 @@ import java.security.Principal; +import org.sopt.makers.crew.main.comment.v2.dto.query.CommentV2GetCommentsQueryDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2UpdateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseStatus; @@ -62,4 +65,11 @@ ResponseEntity reportComment( ResponseEntity mentionUserInComment( @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, Principal principal); + + @Operation(summary = "모임 게시글 댓글 리스트 조회") + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity getComments(@Valid @ModelAttribute CommentV2GetCommentsQueryDto requestBody, Principal principal); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java index be554fa6..06c0e59d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java @@ -7,17 +7,20 @@ import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.comment.v2.dto.query.CommentV2GetCommentsQueryDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2UpdateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; -import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.service.CommentV2Service; import org.sopt.makers.crew.main.common.util.UserUtil; import org.springframework.http.HttpStatus; 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.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -82,4 +85,17 @@ public ResponseEntity mentionUserInComment( commentV2Service.mentionUserInComment(requestBody, userId); return ResponseEntity.status(HttpStatus.OK).build(); } + + @Override + @GetMapping + public ResponseEntity getComments( + @Valid @ModelAttribute CommentV2GetCommentsQueryDto requestBody, + Principal principal) { + + Integer userId = UserUtil.getUserId(principal); + CommentV2GetCommentsResponseDto commentDtos = commentV2Service.getComments(requestBody.getPostId(), + requestBody.getPage(), requestBody.getTake(), userId); + + return ResponseEntity.status(HttpStatus.OK).body(commentDtos); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java new file mode 100644 index 00000000..b8367b78 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java @@ -0,0 +1,18 @@ +package org.sopt.makers.crew.main.comment.v2.dto; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.entity.comment.Comment; +import org.sopt.makers.crew.main.entity.post.Post; +import org.sopt.makers.crew.main.entity.user.User; + +@Mapper(componentModel = "spring") +public interface CommentMapper { + @Mapping(source = "post", target = "post") + @Mapping(source = "requestBody.contents", target = "contents") + @Mapping(source = "user", target = "user") + @Mapping(source = "user.id", target = "userId") + Comment toComment(CommentV2CreateCommentBodyDto requestBody, Post post, User user, int depth, int order, + Integer parentId); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/query/CommentV2GetCommentsQueryDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/query/CommentV2GetCommentsQueryDto.java new file mode 100644 index 00000000..a5602c51 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/query/CommentV2GetCommentsQueryDto.java @@ -0,0 +1,20 @@ +package org.sopt.makers.crew.main.comment.v2.dto.query; + +import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CommentV2GetCommentsQueryDto extends PageOptionsDto { + + @NotNull + private final Integer postId; + + @Builder + public CommentV2GetCommentsQueryDto(Integer page, Integer take, Integer postId) { + super(page, take); + this.postId = postId; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java index c7df385f..bdc26c45 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java @@ -11,12 +11,18 @@ @Schema(description = "댓글 생성 request body dto") public class CommentV2CreateCommentBodyDto { - @Schema(example = "1", required = true, description = "게시글 ID") - @NotNull - private Integer postId; + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private Integer postId; - @Schema(example = "알고보면 쓸데있는 개발 프로세스", required = true, description = "댓글 내용") - @NotEmpty - private String contents; + @Schema(example = "알고보면 쓸데있는 개발 프로세스", required = true, description = "댓글 내용") + @NotEmpty + private String contents; + + @Schema(example = "댓글/대댓글 여부", required = true, description = "true") + private boolean isParent; + + @Schema(example = "대댓글인 경우, 댓글의 id", required = true, description = "1") + private Integer parentCommentId; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java new file mode 100644 index 00000000..caa2c8bb --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java @@ -0,0 +1,44 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +import org.sopt.makers.crew.main.entity.comment.Comment; + +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; + +@Getter +public class CommentDto { + private final Integer id; + private final String contents; + private final CommentWriterDto user; + private final LocalDateTime updatedDate; + private final int likeCount; + private final boolean isLiked; + private final boolean isWriter; + private final int order; + private final List replies; + + @QueryProjection + public CommentDto(Integer id, String contents, CommentWriterDto user, LocalDateTime updatedDate, int likeCount, + boolean isLiked, boolean isWriter, int order, List replies) { + this.id = id; + this.contents = contents; + this.user = user; + this.updatedDate = updatedDate; + this.likeCount = likeCount; + this.isLiked = isLiked; + this.isWriter = isWriter; + this.order = order; + this.replies = replies; + } + + public static CommentDto of(Comment comment, boolean isLiked, boolean isWriter, List replies){ + return new CommentDto(comment.getId(), comment.getContents(), + new CommentWriterDto(comment.getUser().getId(), comment.getUser().getName(), + comment.getUser().getProfileImage()), comment.getUpdatedDate(), comment.getLikeCount(), + isLiked, isWriter, comment.getOrder(), replies); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2GetCommentsResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2GetCommentsResponseDto.java new file mode 100644 index 00000000..de6c9dfb --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2GetCommentsResponseDto.java @@ -0,0 +1,12 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class CommentV2GetCommentsResponseDto { + private final List comments; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java new file mode 100644 index 00000000..875a7e32 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; + +@Getter +public class CommentWriterDto { + private final Integer id; + private final String name; + private final String profileImage; + + @QueryProjection + public CommentWriterDto(Integer id, String name, String profileImage) { + this.id = id; + this.name = name; + this.profileImage = profileImage; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index cf67bc50..fe18ffb7 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -3,6 +3,7 @@ import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; @@ -22,4 +23,6 @@ CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userI CommentV2UpdateCommentResponseDto updateComment(Integer commentId, String contents, Integer userId); + + CommentV2GetCommentsResponseDto getComments(Integer postId, Integer page, Integer take, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index 3e966983..6121fe6c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -2,13 +2,20 @@ import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.comment.v2.dto.CommentMapper; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; @@ -17,6 +24,8 @@ import org.sopt.makers.crew.main.common.util.Time; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; +import org.sopt.makers.crew.main.entity.like.LikeRepository; +import org.sopt.makers.crew.main.entity.like.MyLikes; import org.sopt.makers.crew.main.entity.post.Post; import org.sopt.makers.crew.main.entity.post.PostRepository; import org.sopt.makers.crew.main.entity.report.Report; @@ -33,13 +42,20 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class CommentV2ServiceImpl implements CommentV2Service { + private static final int IS_PARENT_COMMENT = 0; + private static final int IS_REPLY_COMMENT = 1; + private static final String DELETE_COMMENT_CONTENT = "삭제된 댓글입니다."; private final PostRepository postRepository; private final UserRepository userRepository; private final CommentRepository commentRepository; private final ReportRepository reportRepository; + private final LikeRepository likeRepository; + private final PushNotificationService pushNotificationService; + private final CommentMapper commentMapper; + @Value("${push-notification.web-url}") private String pushWebUrl; @@ -56,16 +72,31 @@ public class CommentV2ServiceImpl implements CommentV2Service { public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, Integer userId) { Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - User user = userRepository.findByIdOrThrow(userId); + User writer = userRepository.findByIdOrThrow(userId); + + int depth = 0; + int order = 0; + Integer parentId = 0; + + boolean isReplyComment = !requestBody.isParent(); + if (isReplyComment) { + validateParentCommentId(requestBody); + depth = 1; + parentId = requestBody.getParentCommentId(); + order = getOrder(parentId); + } - Comment comment = Comment.builder() - .contents(requestBody.getContents()) - .user(user) - .post(post) - .build(); + Comment comment = commentMapper.toComment(requestBody, post, writer, depth, order, parentId); Comment savedComment = commentRepository.save(comment); + post.increaseCommentCount(); + + sendPushNotification(requestBody, post, writer); + return CommentV2CreateCommentResponseDto.of(savedComment.getId()); + } + + private void sendPushNotification(CommentV2CreateCommentBodyDto requestBody, Post post, User user) { User PostWriter = post.getUser(); String[] userIds = {String.valueOf(PostWriter.getOrgId())}; @@ -79,8 +110,16 @@ public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBod PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); pushNotificationService.sendPushNotification(pushRequestDto); + } - return CommentV2CreateCommentResponseDto.of(savedComment.getId()); + private void validateParentCommentId(CommentV2CreateCommentBodyDto requestBody) { + commentRepository.findByIdAndPostIdOrThrow(requestBody.getParentCommentId(), requestBody.getPostId()); + } + + private int getOrder(Integer parentId) { + Optional recentComment = commentRepository.findFirstByParentIdOrderByOrderDesc( + parentId); + return recentComment.map(comment -> comment.getOrder() + 1).orElse(1); } /** @@ -99,7 +138,7 @@ public CommentV2UpdateCommentResponseDto updateComment(Integer commentId, Comment comment = commentRepository.findByIdOrThrow(commentId); // 2. comment의 user_id와 userId가 같은지 확인한다. - comment.isWriter(userId); + comment.validateWriter(userId); // 3. comment의 contents를 수정한다. comment.updateContents(contents, time.now()); @@ -109,6 +148,29 @@ public CommentV2UpdateCommentResponseDto updateComment(Integer commentId, String.valueOf(comment.getUpdatedDate())); } + @Override + public CommentV2GetCommentsResponseDto getComments(Integer postId, Integer page, Integer take, Integer userId) { + // TODO : 페이지네이션 구현 + + List comments = commentRepository.findAllByPostIdOrderByCreatedDate(postId); + + MyLikes myLikes = new MyLikes(likeRepository.findAllByUserIdAndPostIdNotNull(userId)); + + Map> replyMap = new HashMap<>(); + comments.stream() + .filter(comment -> !comment.isParentComment()) + .forEach(comment -> replyMap.computeIfAbsent(comment.getParentId(), k -> new ArrayList<>()) + .add(CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), comment.isWriter(userId), null))); + + List commentDtos = comments.stream() + .filter(Comment::isParentComment) + .map(comment -> CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), comment.isWriter(userId), + replyMap.get(comment.getId()))) + .toList(); + + return CommentV2GetCommentsResponseDto.of(commentDtos); + } + /** * 댓글 신고하기 * @@ -156,10 +218,18 @@ public void deleteComment(Integer commentId, Integer userId) throws ForbiddenExc throw new ForbiddenException(); } - Post post = comment.getPost(); - + Post post = postRepository.findByIdOrThrow(comment.getPostId()); post.decreaseCommentCount(); - commentRepository.delete(comment); + + Optional childComment = commentRepository.findFirstByParentIdOrderByOrderDesc( + comment.getId()); + + if (comment.getDepth() == IS_REPLY_COMMENT || childComment.isEmpty()) { + commentRepository.delete(comment); + return; + } + + comment.deleteParentComment(DELETE_COMMENT_CONTENT, null, null); } @Override diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java index c4d041e2..130043d8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java @@ -42,6 +42,7 @@ public enum ErrorStatus { /** * 500 SERVER_ERROR */ + NOTIFICATION_SERVER_ERROR("알림 서버에 에러가 발생했습니다."), INTERNAL_SERVER_ERROR("예상치 못한 서버 에러가 발생했습니다."); private final String errorCode; diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java index ee9925f7..92fe4795 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java @@ -104,7 +104,6 @@ public Apply(EnApplyType type, Meeting meeting, Integer meetingId, User user, In this.content = content; this.appliedDate = LocalDateTime.now(); this.status = EnApplyStatus.WAITING; - this.meeting.addApply(this); } public void updateApplyStatus(EnApplyStatus status) { diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java index 07a9448d..26ca1b56 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java @@ -20,6 +20,8 @@ List findAllByUserIdAndStatus(@Param("userId") Integer userId, List findAllByMeetingIdAndStatus(Integer meetingId, EnApplyStatus statusValue); + List findAllByMeetingId(Integer meetingId); + boolean existsByMeetingIdAndUserId(Integer meetingId, Integer userId); @Transactional diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java index c8309198..c6ac5a97 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java @@ -1,12 +1,12 @@ package org.sopt.makers.crew.main.entity.apply; -import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface ApplySearchRepository { - Page findApplyList(MeetingGetApplyListCommand queryCommand, Pageable pageable, Integer meetingId, + Page findApplyList(MeetingGetAppliesQueryDto queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java index 915e7c34..f47765ae 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; -import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.QApplicantDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.QApplyInfoDto; @@ -25,7 +25,7 @@ public class ApplySearchRepositoryImpl implements ApplySearchRepository { private final JPAQueryFactory queryFactory; @Override - public Page findApplyList(MeetingGetApplyListCommand queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId) { + public Page findApplyList(MeetingGetAppliesQueryDto queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId) { List content = getContent(queryCommand, pageable, meetingId, meetingCreatorId, userId); JPAQuery countQuery = getCount(queryCommand, meetingId); @@ -33,7 +33,7 @@ public Page findApplyList(MeetingGetApplyListCommand queryCommand, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()), countQuery::fetchFirst); } - private List getContent(MeetingGetApplyListCommand queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId) { + private List getContent(MeetingGetAppliesQueryDto queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId) { boolean isStudyCreator = Objects.equals(meetingCreatorId, userId); return queryFactory .select(new QApplyInfoDto( @@ -53,7 +53,7 @@ private List getContent(MeetingGetApplyListCommand queryCommand, P .fetch(); } - private JPAQuery getCount(MeetingGetApplyListCommand queryCommand, Integer meetingId) { + private JPAQuery getCount(MeetingGetAppliesQueryDto queryCommand, Integer meetingId) { return queryFactory .select(apply.count()) .from(apply) diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index 59e9ffd2..daef6b01 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -11,8 +11,11 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.PostPersist; import jakarta.persistence.Table; + import java.time.LocalDateTime; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -28,118 +31,133 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EntityListeners(AuditingEntityListener.class) +@EntityListeners({AuditingEntityListener.class, Comment.CommentListener.class}) @Table(name = "comment") public class Comment { - /** - * 댓글의 고유 식별자 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - /** - * 댓글 내용 - */ - @Column(nullable = false) - private String contents; - - /** - * 댓글 깊이 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int depth; - - /** - * 댓글 순서 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int order; - - /** - * 작성일 - */ - @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") - @CreatedDate - private LocalDateTime createdDate; - - /** - * 수정일 - */ - @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") - @LastModifiedDate - private LocalDateTime updatedDate; - - /** - * 작성자 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 작성자의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer userId; - - /** - * 댓글이 속한 게시글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "postId", nullable = false) - private Post post; - - /** - * 댓글이 속한 게시글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer postId; - - /** - * 댓글에 대한 좋아요 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int likeCount; - - /** - * 부모 댓글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parentId") - private Comment parent; - - /** - * 부모 댓글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer parentId; - - - @Builder - public Comment(String contents, User user, Post post, Comment parent) { - this.contents = contents; - this.user = user; - this.userId = user.getId(); - this.post = post; - this.parent = parent; - this.depth = 0; - this.order = 0; - this.likeCount = 0; - this.post.addComment(this); - } - - public void updateContents(String contents,LocalDateTime updatedDate) { - this.contents = contents; - this.updatedDate = updatedDate; - } - - public void isWriter(Integer userId){ - boolean isWriter = this.userId.equals(userId); - if (!isWriter) { - throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); - } - } - + private static final int PARENT_COMMENT = 0; + + /** + * 댓글의 고유 식별자 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 댓글 내용 + */ + @Column(nullable = false) + private String contents; + + /** + * 댓글/대댓글 구분자 (0 = 댓글, 1 = 대댓글) + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int depth; + + /** + * 댓글 순서 (댓글일 경우 0, 대댓글은 1부터 시작) + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int order; + + /** + * 작성일 + */ + @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime createdDate; + + /** + * 수정일 + */ + @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") + @LastModifiedDate + private LocalDateTime updatedDate; + + /** + * 작성자 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId") + private User user; + + /** + * 작성자의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer userId; + + /** + * 댓글이 속한 게시글 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "postId", nullable = false) + private Post post; + + /** + * 댓글이 속한 게시글의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer postId; + + /** + * 댓글에 대한 좋아요 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int likeCount; + + /** + * 부모 댓글의 고유 식별자 + */ + private Integer parentId; + + public static class CommentListener { + @PostPersist + public void setParentId(Comment comment) { + if (comment.depth == PARENT_COMMENT) { // 댓글일 경우 + comment.parentId = comment.id; + } + } + } + + @Builder + public Comment(String contents, int depth, int order, User user, Integer userId, Post post, Integer postId, + int likeCount, Integer parentId) { + this.contents = contents; + this.depth = depth; + this.order = order; + this.user = user; + this.userId = userId; + this.post = post; + this.postId = postId; + this.likeCount = likeCount; + this.parentId = parentId; + } + + public void updateContents(String contents, LocalDateTime updatedDate) { + this.contents = contents; + this.updatedDate = updatedDate; + } + + public void deleteParentComment(String contents, User user, Integer userId) { + this.contents = contents; + this.user = user; + this.userId = userId; + } + + public void validateWriter(Integer userId) { + if (!isWriter(userId)) { + throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); + } + } + + public boolean isWriter(Integer userId) { + return this.userId.equals(userId); + } + + public boolean isParentComment() { + return this.depth == PARENT_COMMENT; + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java index bfc3b910..e3faa2b0 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java @@ -1,13 +1,27 @@ package org.sopt.makers.crew.main.entity.comment; +import java.util.List; +import java.util.Optional; + import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.response.ErrorStatus; import org.springframework.data.jpa.repository.JpaRepository; -public interface CommentRepository extends JpaRepository { +public interface CommentRepository extends JpaRepository, CommentSearchRepository { + + default Comment findByIdOrThrow(Integer commentId) { + return findById(commentId) + .orElseThrow(() -> new BadRequestException(ErrorStatus.NOT_FOUND_COMMENT.getErrorCode())); + } + + Optional findFirstByParentIdOrderByOrderDesc(Integer parentId); + + Optional findByIdAndPostId(Integer id, Integer postId); + + default Comment findByIdAndPostIdOrThrow(Integer id, Integer postId){ + return findByIdAndPostId(id, postId) + .orElseThrow(() -> new BadRequestException(ErrorStatus.NOT_FOUND_COMMENT.getErrorCode())); + } - default Comment findByIdOrThrow(Integer commentId) { - return findById(commentId) - .orElseThrow(() -> new BadRequestException(ErrorStatus.NOT_FOUND_COMMENT.getErrorCode())); - } + List findAllByPostIdOrderByCreatedDate(Integer postId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepository.java new file mode 100644 index 00000000..0eae30f5 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepository.java @@ -0,0 +1,9 @@ +package org.sopt.makers.crew.main.entity.comment; + + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface CommentSearchRepository { + Page findAllByPostIdPagination(Integer postId, int depth, Pageable pageable); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepositoryImpl.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepositoryImpl.java new file mode 100644 index 00000000..1d19f944 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepositoryImpl.java @@ -0,0 +1,50 @@ +package org.sopt.makers.crew.main.entity.comment; + +import static org.sopt.makers.crew.main.entity.comment.QComment.*; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class CommentSearchRepositoryImpl implements CommentSearchRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Page findAllByPostIdPagination(Integer postId, int depth, Pageable pageable) { + QComment parentComment = new QComment("parentComment"); + QComment childComment = new QComment("childComment"); + + List content = queryFactory + .select(childComment) + .from(parentComment) + .join(childComment).on(childComment.parentId.eq(parentComment.id)) + .where(parentComment.postId.eq(postId)) + .orderBy(parentComment.createdDate.asc(), childComment.createdDate.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = getCount(postId); + + return PageableExecutionUtils.getPage(content, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()), countQuery::fetchFirst); + } + + private JPAQuery getCount(Integer postId) { + return queryFactory + .select(comment.count()) + .from(comment) + .where(comment.postId.eq(postId)); + + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java index 0870c8ba..142f20e8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java @@ -1,6 +1,10 @@ package org.sopt.makers.crew.main.entity.like; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; public interface LikeRepository extends JpaRepository { + + List findAllByUserIdAndPostIdNotNull(Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/like/MyLikes.java b/main/src/main/java/org/sopt/makers/crew/main/entity/like/MyLikes.java new file mode 100644 index 00000000..3f293373 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/like/MyLikes.java @@ -0,0 +1,17 @@ +package org.sopt.makers.crew.main.entity.like; + +import java.util.List; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class MyLikes { + private final List myLikes; + + public boolean isLikeComment(Integer commentId){ + return myLikes.stream() + .anyMatch(like -> like.getCommentId().equals(commentId)); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java index 47c6964c..b2968c1d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java @@ -14,15 +14,16 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; + import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; + import org.hibernate.annotations.Parameter; import org.hibernate.annotations.Type; import org.sopt.makers.crew.main.entity.apply.Apply; @@ -31,7 +32,6 @@ import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; -import org.sopt.makers.crew.main.entity.post.Post; import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -42,200 +42,179 @@ @Table(name = "meeting") public class Meeting { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - /** - * 개설한 유저 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 유저 id - */ - @Column(insertable = false, updatable = false) - private Integer userId; - - /** - * 지원 or 초대 정보 - */ - @OneToMany(mappedBy = "meeting", cascade = CascadeType.REMOVE) - private List appliedInfo; - - /** - * 모임 제목 - */ - @Column(nullable = false) - private String title; - - /** - * 모임 카테고리 - */ - @Column(name = "category", nullable = false) - @Convert(converter = MeetingCategoryConverter.class) - private MeetingCategory category; - - /** - * 이미지 - */ - @Column(name = "imageURL", columnDefinition = "jsonb") - @Type(JsonBinaryType.class) - //@JdbcTypeCode(SqlTypes.JSON) - private List imageURL; - - /** - * 모집 시작 기간 - */ - @Column(name = "startDate", nullable = false, columnDefinition = "TIMESTAMP") - private LocalDateTime startDate; - - /** - * 모집 마감 기간 - */ - @Column(name = "endDate", nullable = false, columnDefinition = "TIMESTAMP") - private LocalDateTime endDate; - - /** - * 모집 인원 - */ - @Column(name = "capacity", nullable = false) - private Integer capacity; - - /** - * 모임 소개 - */ - @Column(name = "desc", nullable = false) - private String desc; - - /** - * 진행방식 소개 - */ - @Column(name = "processDesc", nullable = false) - private String processDesc; - - /** - * 모임 시작 기간 - */ - @Column(name = "mStartDate", nullable = false, columnDefinition = "TIMESTAMP") - private LocalDateTime mStartDate; - - /** - * 모임 마감 기간 - */ - @Column(name = "mEndDate", nullable = false, columnDefinition = "TIMESTAMP") - private LocalDateTime mEndDate; - - /** - * 개설자 소개 - */ - @Column(name = "leaderDesc", nullable = false) - private String leaderDesc; - - /** - * 모집 대상 - */ - @Column(name = "targetDesc", nullable = false) - private String targetDesc; - - /** - * 유의 사항 - */ - @Column(name = "note") - private String note; - - /** - * 멘토 필요 여부 - */ - @Column(name = "isMentorNeeded", nullable = false) - private Boolean isMentorNeeded; - - /** - * 활동 기수만 참여 가능한지 여부 - */ - @Column(name = "canJoinOnlyActiveGeneration", nullable = false) - private Boolean canJoinOnlyActiveGeneration; - - /** - * 모임 기수 - */ - @Column(name = "createdGeneration", nullable = false) - private Integer createdGeneration; - - /** - * 대상 활동 기수 - */ - @Column(name = "targetActiveGeneration") - private Integer targetActiveGeneration; - - /** - * 모임 참여 가능한 파트 - */ - @Type(value = EnumArrayType.class, - parameters = @Parameter(name = AbstractArrayType.SQL_ARRAY_TYPE, - value = "meeting_joinableparts_enum")) - @Column(name = "joinableParts", columnDefinition = "meeting_joinableparts_enum[]") - private MeetingJoinablePart[] joinableParts; - - /** - * 게시글 리스트 - */ - @OneToMany(mappedBy = "meeting", cascade = CascadeType.REMOVE) - private List posts; - - @Builder - public Meeting(User user, Integer userId, List appliedInfo, String title, MeetingCategory category, - List imageURL, LocalDateTime startDate, LocalDateTime endDate, Integer capacity, - String desc, String processDesc, LocalDateTime mStartDate, LocalDateTime mEndDate, - String leaderDesc, String targetDesc, String note, Boolean isMentorNeeded, - Boolean canJoinOnlyActiveGeneration, Integer createdGeneration, - Integer targetActiveGeneration, MeetingJoinablePart[] joinableParts) { - this.user = user; - this.userId = userId; - this.appliedInfo = appliedInfo != null ? appliedInfo : new ArrayList<>(); - this.title = title; - this.category = category; - this.imageURL = imageURL; - this.startDate = startDate; - this.endDate = endDate; - this.capacity = capacity; - this.desc = desc; - this.processDesc = processDesc; - this.mStartDate = mStartDate; - this.mEndDate = mEndDate; - this.leaderDesc = leaderDesc; - this.targetDesc = targetDesc; - this.note = note; - this.isMentorNeeded = isMentorNeeded; - this.canJoinOnlyActiveGeneration = canJoinOnlyActiveGeneration; - this.createdGeneration = createdGeneration; - this.targetActiveGeneration = targetActiveGeneration; - this.joinableParts = joinableParts; - } - - public void addApply(Apply apply) { - appliedInfo.add(apply); - } - - public void addPost(Post post) { - posts.add(post); - } - - /** - * 모임 모집상태 확인 - * - * @return 모임 모집상태 - */ - public Integer getMeetingStatus() { - LocalDateTime now = LocalDateTime.now(); - if (now.isBefore(startDate)) { - return EnMeetingStatus.BEFORE_START.getValue(); - } else if (now.isBefore(endDate)) { - return EnMeetingStatus.APPLY_ABLE.getValue(); - } else { - return EnMeetingStatus.RECRUITMENT_COMPLETE.getValue(); - } - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 개설한 유저 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + /** + * 유저 id + */ + @Column(insertable = false, updatable = false) + private Integer userId; + + /** + * 모임 제목 + */ + @Column(nullable = false) + private String title; + + /** + * 모임 카테고리 + */ + @Column(name = "category", nullable = false) + @Convert(converter = MeetingCategoryConverter.class) + private MeetingCategory category; + + /** + * 이미지 + */ + @Column(name = "imageURL", columnDefinition = "jsonb") + @Type(JsonBinaryType.class) + //@JdbcTypeCode(SqlTypes.JSON) + private List imageURL; + + /** + * 모집 시작 기간 + */ + @Column(name = "startDate", nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime startDate; + + /** + * 모집 마감 기간 + */ + @Column(name = "endDate", nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime endDate; + + /** + * 모집 인원 + */ + @Column(name = "capacity", nullable = false) + private Integer capacity; + + /** + * 모임 소개 + */ + @Column(name = "desc", nullable = false) + private String desc; + + /** + * 진행방식 소개 + */ + @Column(name = "processDesc", nullable = false) + private String processDesc; + + /** + * 모임 시작 기간 + */ + @Column(name = "mStartDate", nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime mStartDate; + + /** + * 모임 마감 기간 + */ + @Column(name = "mEndDate", nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime mEndDate; + + /** + * 개설자 소개 + */ + @Column(name = "leaderDesc", nullable = false) + private String leaderDesc; + + /** + * 모집 대상 + */ + @Column(name = "targetDesc", nullable = false) + private String targetDesc; + + /** + * 유의 사항 + */ + @Column(name = "note") + private String note; + + /** + * 멘토 필요 여부 + */ + @Column(name = "isMentorNeeded", nullable = false) + private Boolean isMentorNeeded; + + /** + * 활동 기수만 참여 가능한지 여부 + */ + @Column(name = "canJoinOnlyActiveGeneration", nullable = false) + private Boolean canJoinOnlyActiveGeneration; + + /** + * 모임 기수 + */ + @Column(name = "createdGeneration", nullable = false) + private Integer createdGeneration; + + /** + * 대상 활동 기수 + */ + @Column(name = "targetActiveGeneration") + private Integer targetActiveGeneration; + + /** + * 모임 참여 가능한 파트 + */ + @Type(value = EnumArrayType.class, + parameters = @Parameter(name = AbstractArrayType.SQL_ARRAY_TYPE, + value = "meeting_joinableparts_enum")) + @Column(name = "joinableParts", columnDefinition = "meeting_joinableparts_enum[]") + private MeetingJoinablePart[] joinableParts; + + @Builder + public Meeting(User user, Integer userId, List appliedInfo, String title, MeetingCategory category, + List imageURL, LocalDateTime startDate, LocalDateTime endDate, Integer capacity, + String desc, String processDesc, LocalDateTime mStartDate, LocalDateTime mEndDate, + String leaderDesc, String targetDesc, String note, Boolean isMentorNeeded, + Boolean canJoinOnlyActiveGeneration, Integer createdGeneration, + Integer targetActiveGeneration, MeetingJoinablePart[] joinableParts) { + this.user = user; + this.userId = userId; + this.title = title; + this.category = category; + this.imageURL = imageURL; + this.startDate = startDate; + this.endDate = endDate; + this.capacity = capacity; + this.desc = desc; + this.processDesc = processDesc; + this.mStartDate = mStartDate; + this.mEndDate = mEndDate; + this.leaderDesc = leaderDesc; + this.targetDesc = targetDesc; + this.note = note; + this.isMentorNeeded = isMentorNeeded; + this.canJoinOnlyActiveGeneration = canJoinOnlyActiveGeneration; + this.createdGeneration = createdGeneration; + this.targetActiveGeneration = targetActiveGeneration; + this.joinableParts = joinableParts; + } + + /** + * 모임 모집상태 확인 + * + * @return 모임 모집상태 + */ + public Integer getMeetingStatus() { + LocalDateTime now = LocalDateTime.now(); + if (now.isBefore(startDate)) { + return EnMeetingStatus.BEFORE_START.getValue(); + } else if (now.isBefore(endDate)) { + return EnMeetingStatus.APPLY_ABLE.getValue(); + } else { + return EnMeetingStatus.RECRUITMENT_COMPLETE.getValue(); + } + } } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java index 0a8f3d5c..55fcb12c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java @@ -11,19 +11,18 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; + import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; + import org.hibernate.annotations.Type; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.meeting.Meeting; -import org.sopt.makers.crew.main.entity.report.Report; import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; @@ -36,124 +35,107 @@ @Table(name = "post") public class Post { - /** - * 게시글의 고유 식별자 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - /** - * 게시글 제목 - */ - @Column(nullable = false) - private String title; - - /** - * 게시글 내용 - */ - @Column(nullable = false, columnDefinition = "TEXT") - private String contents; - - /** - * 게시글 작성일 - */ - @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") - @CreatedDate - private LocalDateTime createdDate; - - /** - * 게시글 수정일 - */ - @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") - @LastModifiedDate - private LocalDateTime updatedDate; - - /** - * 조회수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int viewCount; - - /** - * 이미지 리스트 - */ - @Column(name = "images", columnDefinition = "text[]") - @Type(StringArrayType.class) - private String[] images; - - /** - * 작성자 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 작성자의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer userId; - - /** - * 게시글이 속한 미팅 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "meetingId", nullable = false) - private Meeting meeting; - - /** - * 게시글이 속한 미팅의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer meetingId; - - /** - * 게시글에 달린 댓글 목록 - */ - @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) - private List comments = new ArrayList<>(); - - /** - * 게시글에 달린 댓글 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int commentCount; - - /** - * 게시글에 대한 좋아요 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int likeCount; - - /** - * 게시글에 대한 신고 목록 - */ - @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) - private List reports; - - @Builder - public Post(String title, String contents, String[] images, User user, Meeting meeting) { - this.title = title; - this.contents = contents; - this.viewCount = 0; - this.images = images; - this.user = user; - this.meeting = meeting; - this.commentCount = 0; - this.likeCount = 0; - } - - public void addComment(Comment comment) { - this.comments.add(comment); - this.commentCount++; - } - - public void addReport(Report report) { - this.reports.add(report); - } - - public void decreaseCommentCount() { - this.commentCount--; - } + /** + * 게시글의 고유 식별자 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 게시글 제목 + */ + @Column(nullable = false) + private String title; + + /** + * 게시글 내용 + */ + @Column(nullable = false, columnDefinition = "TEXT") + private String contents; + + /** + * 게시글 작성일 + */ + @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime createdDate; + + /** + * 게시글 수정일 + */ + @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") + @LastModifiedDate + private LocalDateTime updatedDate; + + /** + * 조회수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int viewCount; + + /** + * 이미지 리스트 + */ + @Column(name = "images", columnDefinition = "text[]") + @Type(StringArrayType.class) + private String[] images; + + /** + * 작성자 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + /** + * 작성자의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer userId; + + /** + * 게시글이 속한 미팅 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "meetingId", nullable = false) + private Meeting meeting; + + /** + * 게시글이 속한 미팅의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer meetingId; + + /** + * 게시글에 달린 댓글 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int commentCount; + + /** + * 게시글에 대한 좋아요 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int likeCount; + + @Builder + public Post(String title, String contents, String[] images, User user, Meeting meeting) { + this.title = title; + this.contents = contents; + this.viewCount = 0; + this.images = images; + this.user = user; + this.meeting = meeting; + this.commentCount = 0; + this.likeCount = 0; + } + + public void increaseCommentCount() { + this.commentCount++; + } + + public void decreaseCommentCount() { + this.commentCount--; + } } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java index c6cab8ff..ef23d79b 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java @@ -14,4 +14,6 @@ default Post findByIdOrThrow(Integer postId) { return findById(postId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_POST.getErrorCode())); } + + Optional findFirstByMeetingIdOrderByIdDesc(Integer meetingId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java index 1ffd454e..e13ecae5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java @@ -3,30 +3,20 @@ import static org.sopt.makers.crew.main.common.response.ErrorStatus.*; import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; -import java.util.ArrayList; import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Set; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Type; import org.sopt.makers.crew.main.common.exception.ServerException; -import org.sopt.makers.crew.main.common.response.ErrorStatus; -import org.sopt.makers.crew.main.entity.apply.Apply; -import org.sopt.makers.crew.main.entity.meeting.Meeting; -import org.sopt.makers.crew.main.entity.post.Post; -import org.sopt.makers.crew.main.entity.report.Report; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; @Entity @@ -74,32 +64,8 @@ public class User { @Column(name = "phone") private String phone; - /** - * 내가 생성한 모임 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private Set meetings = new HashSet<>(); - - /** - * 내가 지원한 내역 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private List applies = new ArrayList<>(); - - /** - * 작성한 게시글 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private List posts = new ArrayList<>(); - - /** - * 신고 내역 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private List reports = new ArrayList<>(); - @Builder - public User(String name, int orgId, List activities, String profileImage, + public User(String name, Integer orgId, List activities, String profileImage, String phone) { this.name = name; this.orgId = orgId; @@ -108,18 +74,6 @@ public User(String name, int orgId, List activities, String prof this.phone = phone; } - public void addMeeting(Meeting meeting) { - this.meetings.add(meeting); - } - - public void addApply(Apply apply) { - this.applies.add(apply); - } - - public void addReport(Report report) { - this.reports.add(report); - } - public void setUserIdForTest(Integer userId) { this.id = userId; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationService.java b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationService.java index 00749056..947b9b06 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationService.java +++ b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationService.java @@ -1,29 +1,36 @@ package org.sopt.makers.crew.main.internal.notification; +import static org.sopt.makers.crew.main.common.response.ErrorStatus.*; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_ACTION; import java.util.UUID; + import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + import org.sopt.makers.crew.main.internal.notification.dto.PushNotificationRequestDto; -import org.sopt.makers.crew.main.internal.notification.dto.PushNotificationResponseDto; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor +@Slf4j public class PushNotificationService { - @Value("${push-notification.x-api-key}") - private String pushNotificationApiKey; + @Value("${push-notification.x-api-key}") + private String pushNotificationApiKey; - @Value("${push-notification.service}") - private String service; + @Value("${push-notification.service}") + private String service; - private final PushNotificationServerClient pushServerClient; + private final PushNotificationServerClient pushServerClient; - public void sendPushNotification(PushNotificationRequestDto request) { - PushNotificationResponseDto response = - pushServerClient.sendPushNotification(pushNotificationApiKey, - PUSH_NOTIFICATION_ACTION.getValue(), UUID.randomUUID().toString(), service, request); - } + public void sendPushNotification(PushNotificationRequestDto request) { + try { + pushServerClient.sendPushNotification(pushNotificationApiKey, + PUSH_NOTIFICATION_ACTION.getValue(), UUID.randomUUID().toString(), service, request); + }catch (Exception e){ + log.error(NOTIFICATION_SERVER_ERROR.getErrorCode(), e); + } + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java index b234363b..4331585e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java @@ -11,7 +11,7 @@ import jakarta.validation.Valid; import java.security.Principal; import java.util.List; -import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; @@ -69,6 +69,6 @@ ResponseEntity applyMeetingCancel(@PathVariable Integer meetingId, @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "모임 지원자/참여자 조회 성공"), @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content),}) ResponseEntity findApplyList(@PathVariable Integer meetingId, - @ModelAttribute MeetingGetApplyListCommand queryCommand, + @ModelAttribute MeetingGetAppliesQueryDto queryCommand, Principal principal); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java index 1b1ab29f..e05957c8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java @@ -6,7 +6,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.util.UserUtil; -import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; @@ -25,7 +25,6 @@ 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.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -84,7 +83,7 @@ public ResponseEntity applyMeetingCancel(@PathVariable Integer meetingId, @Override @GetMapping("/{meetingId}/list") public ResponseEntity findApplyList(@PathVariable Integer meetingId, - @ModelAttribute MeetingGetApplyListCommand queryCommand, + @ModelAttribute MeetingGetAppliesQueryDto queryCommand, Principal principal) { Integer userId = UserUtil.getUserId(principal); diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetApplyListCommand.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetAppliesQueryDto.java similarity index 79% rename from main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetApplyListCommand.java rename to main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetAppliesQueryDto.java index a6c08449..3edecb09 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetApplyListCommand.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetAppliesQueryDto.java @@ -10,13 +10,13 @@ @Getter @Setter -public class MeetingGetApplyListCommand extends PageOptionsDto { +public class MeetingGetAppliesQueryDto extends PageOptionsDto { private List status; private String date; @Builder - public MeetingGetApplyListCommand(int page, int take, List status, String date) { + public MeetingGetAppliesQueryDto(int page, int take, List status, String date) { super(page, take); this.status = (status == null || status.isEmpty()) ? Arrays.asList(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT) : status; diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java index c443ce87..3e8d647a 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java @@ -52,7 +52,7 @@ public class MeetingV2GetMeetingBannerResponseDto { /** 가입된 지원자 수 */ private Integer approvedUserCount; /** 개설자 정보 */ - private Optional user; + private MeetingV2GetMeetingBannerResponseUserDto user; /** 미팅 상태 */ private Integer status; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java index 69fe3e10..5a1362bf 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java @@ -1,7 +1,7 @@ package org.sopt.makers.crew.main.meeting.v2.service; import java.util.List; -import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; @@ -24,6 +24,6 @@ MeetingV2GetAllMeetingByOrgUserDto getAllMeetingByOrgUser( void applyMeetingCancel(Integer meetingId, Integer userId); - MeetingGetApplyListResponseDto findApplyList(MeetingGetApplyListCommand queryCommand, Integer meetingId, + MeetingGetApplyListResponseDto findApplyList(MeetingGetAppliesQueryDto queryCommand, Integer meetingId, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java index 71212608..bf613ef8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java @@ -18,7 +18,9 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; @@ -31,13 +33,14 @@ import org.sopt.makers.crew.main.entity.meeting.MeetingRepository; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; import org.sopt.makers.crew.main.entity.post.Post; +import org.sopt.makers.crew.main.entity.post.PostRepository; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.entity.user.enums.UserPart; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper; import org.sopt.makers.crew.main.meeting.v2.dto.MeetingMapper; -import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; @@ -59,234 +62,236 @@ @Transactional(readOnly = true) public class MeetingV2ServiceImpl implements MeetingV2Service { - private final static int ZERO = 0; - - private final UserRepository userRepository; - private final ApplyRepository applyRepository; - private final MeetingRepository meetingRepository; - - private final MeetingMapper meetingMapper; - private final ApplyMapper applyMapper; - - @Override - public MeetingV2GetAllMeetingByOrgUserDto getAllMeetingByOrgUser( - MeetingV2GetAllMeetingByOrgUserQueryDto queryDto) { - int page = queryDto.getPage(); - int take = queryDto.getTake(); - - Optional user = userRepository.findByOrgId(queryDto.getOrgUserId()); - List userJoinedList = new ArrayList<>(); - - if (!user.isEmpty()) { - User existUser = user.get(); - userJoinedList = Stream - .concat(existUser.getMeetings().stream(), - applyRepository.findAllByUserIdAndStatus(existUser.getId(), EnApplyStatus.APPROVE) - .stream().map(apply -> apply.getMeeting())) - .map(meeting -> MeetingV2GetAllMeetingByOrgUserMeetingDto.of(meeting.getId(), - checkMeetingLeader(meeting, existUser.getId()), meeting.getTitle(), - meeting.getImageURL().get(0).getUrl(), meeting.getCategory().getValue(), - meeting.getMStartDate(), meeting.getMEndDate(), checkActivityStatus(meeting))) - .sorted(Comparator.comparing(MeetingV2GetAllMeetingByOrgUserMeetingDto::getId).reversed()) - .collect(Collectors.toList()); - } - - List pagedUserJoinedList = - userJoinedList.stream().skip((long) (page - 1) * take) // 스킵할 아이템 수 계산 - .limit(take) // 페이지당 아이템 수 제한 - .collect(Collectors.toList()); - PageOptionsDto pageOptionsDto = new PageOptionsDto(page, take); - PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, userJoinedList.size()); - return MeetingV2GetAllMeetingByOrgUserDto.of(pagedUserJoinedList, pageMetaDto); - } - - @Override - public List getMeetingBanner() { - List meetingBanners = this.meetingRepository.findAll() - .stream().sorted(Comparator.comparing(Meeting::getId).reversed()).limit(20).map(meeting -> { - List post = meeting.getPosts().stream() - .sorted(Comparator.comparing(Post::getId).reversed()).limit(1).toList(); - List applies = meeting.getAppliedInfo(); - - Integer applicantCount = applies.size(); - Integer appliedUserCount = applies.stream() - .filter(apply -> apply.getStatus().equals(EnApplyStatus.APPROVE)).toList().size(); - - Optional recentActivityDate = - post.isEmpty() ? Optional.empty() : Optional.of(post.get(0).getCreatedDate()); - - Optional meetingLeader = userRepository - .findById(meeting.getUserId()).map(user -> MeetingV2GetMeetingBannerResponseUserDto - .of(user.getId(), user.getName(), user.getOrgId(), user.getProfileImage())); - - return MeetingV2GetMeetingBannerResponseDto.of(meeting.getId(), meeting.getUserId(), - meeting.getTitle(), meeting.getCategory(), meeting.getImageURL(), - meeting.getMStartDate(), meeting.getMEndDate(), meeting.getStartDate(), - meeting.getEndDate(), - meeting.getCapacity(), recentActivityDate, meeting.getTargetActiveGeneration(), - meeting.getJoinableParts(), applicantCount, appliedUserCount, meetingLeader, - meeting.getMeetingStatus()); - }).toList(); - - return meetingBanners; - } - - - @Override - @Transactional - public MeetingV2CreateMeetingResponseDto createMeeting(MeetingV2CreateMeetingBodyDto requestBody, Integer userId) { - User user = userRepository.findByIdOrThrow(userId); - - if (user.getActivities() == null) { - throw new BadRequestException(VALIDATION_EXCEPTION.getErrorCode()); - } - - if (requestBody.getFiles().size() == ZERO || requestBody.getJoinableParts().length == ZERO) { - throw new BadRequestException(VALIDATION_EXCEPTION.getErrorCode()); - } - - Meeting meeting = meetingMapper.toMeetingEntity(requestBody, - getTargetActiveGeneration(requestBody.getCanJoinOnlyActiveGeneration()), ACTIVE_GENERATION, user, - user.getId()); - - Meeting savedMeeting = meetingRepository.save(meeting); - return MeetingV2CreateMeetingResponseDto.of(savedMeeting.getId()); - } - - @Override - @Transactional - public MeetingV2ApplyMeetingResponseDto applyMeeting(MeetingV2ApplyMeetingDto requestBody, Integer userId) { - Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); - User user = userRepository.findByIdOrThrow(userId); - - validateMeetingCapacity(meeting); - validateUserAlreadyApplied(meeting, userId); - validateApplyPeriod(meeting); - validateUserActivities(user); - validateUserJoinableParts(user, meeting); - - Apply apply = applyMapper.toApplyEntity(requestBody, EnApplyType.APPLY, meeting, user, - userId); - Apply savedApply = applyRepository.save(apply); - return MeetingV2ApplyMeetingResponseDto.of(savedApply.getId()); - } - - @Override - @Transactional - public void applyMeetingCancel(Integer meetingId, Integer userId) { - boolean exists = applyRepository.existsByMeetingIdAndUserId(meetingId, userId); - - if (!exists) { - throw new BadRequestException(NOT_FOUND_APPLY.getErrorCode()); - } - - applyRepository.deleteByMeetingIdAndUserId(meetingId, userId); - } - - @Override - @Transactional(readOnly = true) - public MeetingGetApplyListResponseDto findApplyList(MeetingGetApplyListCommand queryCommand, - Integer meetingId, - Integer userId) { - Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); - Page applyInfoDtos = applyRepository.findApplyList(queryCommand, - PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), - meetingId, meeting.getUserId(), userId); - PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), queryCommand.getTake()); - PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, (int) applyInfoDtos.getTotalElements()); - - return MeetingGetApplyListResponseDto.of(applyInfoDtos.getContent(), pageMetaDto); - } - - - private Boolean checkMeetingLeader(Meeting meeting, Integer userId) { - return meeting.getUserId().equals(userId); - } - - private Boolean checkActivityStatus(Meeting meeting) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime mStartDate = meeting.getMStartDate(); - LocalDateTime mEndDate = meeting.getMEndDate(); - return now.isEqual(mStartDate) || (now.isAfter(mStartDate) && now.isBefore(mEndDate)); - } - - private Integer getTargetActiveGeneration(Boolean canJoinOnlyActiveGeneration) { - return canJoinOnlyActiveGeneration ? ACTIVE_GENERATION : null; - } - - private List filterUserActivities(User user, Meeting meeting) { - // 현재 활동기수만 지원 가능할 경우 -> 현재 활동 기수에 해당하는 파트만 필터링 - if (meeting.getTargetActiveGeneration() == ACTIVE_GENERATION && meeting.getCanJoinOnlyActiveGeneration()) { - List filteredActivities = user.getActivities().stream() - .filter(activity -> activity.getGeneration() == ACTIVE_GENERATION) - .collect(Collectors.toList()); - - // 활동 기수가 아닌 경우 예외 처리 - if (filteredActivities.isEmpty()) { - throw new BadRequestException(NOT_ACTIVE_GENERATION.getErrorCode()); - } - - return filteredActivities; - } - return user.getActivities(); - } - - private void validateMeetingCapacity(Meeting meeting) { - List approvedApplies = meeting.getAppliedInfo().stream() - .filter(apply -> EnApplyStatus.APPROVE.equals(apply.getStatus())) - .collect(Collectors.toList()); - - if (approvedApplies.size() >= meeting.getCapacity()) { - throw new BadRequestException(FULL_MEETING_CAPACITY.getErrorCode()); - } - } - - private void validateUserAlreadyApplied(Meeting meeting, Integer userId) { - boolean hasApplied = meeting.getAppliedInfo().stream() - .anyMatch(appliedInfo -> appliedInfo.getUser().getId().equals(userId)); - - if (hasApplied) { - throw new BadRequestException(ALREADY_APPLIED_MEETING.getErrorCode()); - } - } - - private void validateApplyPeriod(Meeting meeting) { - LocalDateTime now = LocalDateTime.now(); - if (now.isAfter(meeting.getEndDate()) || now.isBefore(meeting.getStartDate())) { - throw new BadRequestException(NOT_IN_APPLY_PERIOD.getErrorCode()); - } - } - - private void validateUserActivities(User user) { - if (user.getActivities().isEmpty()) { - throw new BadRequestException(MISSING_GENERATION_PART.getErrorCode()); - } - } - - private void validateUserJoinableParts(User user, Meeting meeting) { - if (meeting.getJoinableParts().length == 0) { - return; - } - - List userActivities = filterUserActivities(user, meeting); - List userJoinableParts = userActivities.stream() - .map(UserActivityVO::getPart) - .filter(part -> { - MeetingJoinablePart meetingJoinablePart = UserPartUtil.getMeetingJoinablePart( - UserPart.ofValue(part)); - - // 임원진이라면 패스 - if (meetingJoinablePart == null) { - return true; - } - - return Arrays.stream(meeting.getJoinableParts()) - .anyMatch(joinablePart -> joinablePart == meetingJoinablePart); - }) - .collect(Collectors.toList()); - - if (userJoinableParts.isEmpty()) { - throw new BadRequestException(NOT_TARGET_PART.getErrorCode()); - } - } + private final static int ZERO = 0; + + private final UserRepository userRepository; + private final ApplyRepository applyRepository; + private final MeetingRepository meetingRepository; + private final PostRepository postRepository; + + private final MeetingMapper meetingMapper; + private final ApplyMapper applyMapper; + + @Override + public MeetingV2GetAllMeetingByOrgUserDto getAllMeetingByOrgUser( + MeetingV2GetAllMeetingByOrgUserQueryDto queryDto) { + int page = queryDto.getPage(); + int take = queryDto.getTake(); + + Optional user = userRepository.findByOrgId(queryDto.getOrgUserId()); + List userJoinedList = new ArrayList<>(); + + if (!user.isEmpty()) { + User existUser = user.get(); + List myMeetings = meetingRepository.findAllByUserId(existUser.getId()); + + userJoinedList = Stream + .concat(myMeetings.stream(), + applyRepository.findAllByUserIdAndStatus(existUser.getId(), EnApplyStatus.APPROVE) + .stream().map(apply -> apply.getMeeting())) + .map(meeting -> MeetingV2GetAllMeetingByOrgUserMeetingDto.of(meeting.getId(), + checkMeetingLeader(meeting, existUser.getId()), meeting.getTitle(), + meeting.getImageURL().get(0).getUrl(), meeting.getCategory().getValue(), + meeting.getMStartDate(), meeting.getMEndDate(), checkActivityStatus(meeting))) + .sorted(Comparator.comparing(MeetingV2GetAllMeetingByOrgUserMeetingDto::getId).reversed()) + .collect(Collectors.toList()); + } + + List pagedUserJoinedList = + userJoinedList.stream().skip((long)(page - 1) * take) // 스킵할 아이템 수 계산 + .limit(take) // 페이지당 아이템 수 제한 + .collect(Collectors.toList()); + PageOptionsDto pageOptionsDto = new PageOptionsDto(page, take); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, userJoinedList.size()); + return MeetingV2GetAllMeetingByOrgUserDto.of(pagedUserJoinedList, pageMetaDto); + } + + @Override + public List getMeetingBanner() { + return meetingRepository.findAll() + .stream() + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .limit(20) + .map(meeting -> { + Optional recentPost = postRepository.findFirstByMeetingIdOrderByIdDesc(meeting.getId()); + Optional recentActivityDate = recentPost.map(Post::getCreatedDate); + + List applies = applyRepository.findAllByMeetingId(meeting.getId()); + Integer applicantCount = applies.size(); + Integer appliedUserCount = applies.stream() + .filter(apply -> apply.getStatus().equals(EnApplyStatus.APPROVE)).toList().size(); + + User meetingLeader = userRepository.findByIdOrThrow(meeting.getUserId()); + MeetingV2GetMeetingBannerResponseUserDto meetingLeaderDto = MeetingV2GetMeetingBannerResponseUserDto + .of(meetingLeader.getId(), meetingLeader.getName(), meetingLeader.getOrgId(), + meetingLeader.getProfileImage()); + + return MeetingV2GetMeetingBannerResponseDto.of(meeting.getId(), meeting.getUserId(), + meeting.getTitle(), meeting.getCategory(), meeting.getImageURL(), + meeting.getMStartDate(), meeting.getMEndDate(), meeting.getStartDate(), + meeting.getEndDate(), + meeting.getCapacity(), recentActivityDate, meeting.getTargetActiveGeneration(), + meeting.getJoinableParts(), applicantCount, appliedUserCount, meetingLeaderDto, + meeting.getMeetingStatus()); + }).toList(); + } + + @Override + @Transactional + public MeetingV2CreateMeetingResponseDto createMeeting(MeetingV2CreateMeetingBodyDto requestBody, Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + + if (user.getActivities() == null) { + throw new BadRequestException(VALIDATION_EXCEPTION.getErrorCode()); + } + + if (requestBody.getFiles().size() == ZERO || requestBody.getJoinableParts().length == ZERO) { + throw new BadRequestException(VALIDATION_EXCEPTION.getErrorCode()); + } + + Meeting meeting = meetingMapper.toMeetingEntity(requestBody, + getTargetActiveGeneration(requestBody.getCanJoinOnlyActiveGeneration()), ACTIVE_GENERATION, user, + user.getId()); + + Meeting savedMeeting = meetingRepository.save(meeting); + return MeetingV2CreateMeetingResponseDto.of(savedMeeting.getId()); + } + + @Override + @Transactional + public MeetingV2ApplyMeetingResponseDto applyMeeting(MeetingV2ApplyMeetingDto requestBody, Integer userId) { + Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); + User user = userRepository.findByIdOrThrow(userId); + + List applies = applyRepository.findAllByMeetingId(meeting.getId()); + + validateMeetingCapacity(meeting, applies); + validateUserAlreadyApplied(userId, applies); + validateApplyPeriod(meeting); + validateUserActivities(user); + validateUserJoinableParts(user, meeting); + + Apply apply = applyMapper.toApplyEntity(requestBody, EnApplyType.APPLY, meeting, user, + userId); + Apply savedApply = applyRepository.save(apply); + return MeetingV2ApplyMeetingResponseDto.of(savedApply.getId()); + } + + @Override + @Transactional + public void applyMeetingCancel(Integer meetingId, Integer userId) { + boolean exists = applyRepository.existsByMeetingIdAndUserId(meetingId, userId); + + if (!exists) { + throw new BadRequestException(NOT_FOUND_APPLY.getErrorCode()); + } + + applyRepository.deleteByMeetingIdAndUserId(meetingId, userId); + } + + @Override + @Transactional(readOnly = true) + public MeetingGetApplyListResponseDto findApplyList(MeetingGetAppliesQueryDto queryCommand, + Integer meetingId, + Integer userId) { + Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, + PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), + meetingId, meeting.getUserId(), userId); + PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), queryCommand.getTake()); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, (int)applyInfoDtos.getTotalElements()); + + return MeetingGetApplyListResponseDto.of(applyInfoDtos.getContent(), pageMetaDto); + } + + private Boolean checkMeetingLeader(Meeting meeting, Integer userId) { + return meeting.getUserId().equals(userId); + } + + private Boolean checkActivityStatus(Meeting meeting) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime mStartDate = meeting.getMStartDate(); + LocalDateTime mEndDate = meeting.getMEndDate(); + return now.isEqual(mStartDate) || (now.isAfter(mStartDate) && now.isBefore(mEndDate)); + } + + private Integer getTargetActiveGeneration(Boolean canJoinOnlyActiveGeneration) { + return canJoinOnlyActiveGeneration ? ACTIVE_GENERATION : null; + } + + private List filterUserActivities(User user, Meeting meeting) { + // 현재 활동기수만 지원 가능할 경우 -> 현재 활동 기수에 해당하는 파트만 필터링 + if (meeting.getTargetActiveGeneration() == ACTIVE_GENERATION && meeting.getCanJoinOnlyActiveGeneration()) { + List filteredActivities = user.getActivities().stream() + .filter(activity -> activity.getGeneration() == ACTIVE_GENERATION) + .collect(Collectors.toList()); + + // 활동 기수가 아닌 경우 예외 처리 + if (filteredActivities.isEmpty()) { + throw new BadRequestException(NOT_ACTIVE_GENERATION.getErrorCode()); + } + + return filteredActivities; + } + return user.getActivities(); + } + + private void validateMeetingCapacity(Meeting meeting, List applies) { + List approvedApplies = applies.stream() + .filter(apply -> EnApplyStatus.APPROVE.equals(apply.getStatus())) + .toList(); + + if (approvedApplies.size() >= meeting.getCapacity()) { + throw new BadRequestException(FULL_MEETING_CAPACITY.getErrorCode()); + } + } + + private void validateUserAlreadyApplied(Integer userId, List applies) { + boolean hasApplied = applies.stream() + .anyMatch(appliedInfo -> appliedInfo.getUser().getId().equals(userId)); + + if (hasApplied) { + throw new BadRequestException(ALREADY_APPLIED_MEETING.getErrorCode()); + } + } + + private void validateApplyPeriod(Meeting meeting) { + LocalDateTime now = LocalDateTime.now(); + if (now.isAfter(meeting.getEndDate()) || now.isBefore(meeting.getStartDate())) { + throw new BadRequestException(NOT_IN_APPLY_PERIOD.getErrorCode()); + } + } + + private void validateUserActivities(User user) { + if (user.getActivities().isEmpty()) { + throw new BadRequestException(MISSING_GENERATION_PART.getErrorCode()); + } + } + + private void validateUserJoinableParts(User user, Meeting meeting) { + if (meeting.getJoinableParts().length == 0) { + return; + } + + List userActivities = filterUserActivities(user, meeting); + List userJoinableParts = userActivities.stream() + .map(UserActivityVO::getPart) + .filter(part -> { + MeetingJoinablePart meetingJoinablePart = UserPartUtil.getMeetingJoinablePart( + UserPart.ofValue(part)); + + // 임원진이라면 패스 + if (meetingJoinablePart == null) { + return true; + } + + return Arrays.stream(meeting.getJoinableParts()) + .anyMatch(joinablePart -> joinablePart == meetingJoinablePart); + }) + .collect(Collectors.toList()); + + if (userJoinableParts.isEmpty()) { + throw new BadRequestException(NOT_TARGET_PART.getErrorCode()); + } + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java index ecea9dc7..f5ff5a42 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java @@ -13,6 +13,7 @@ import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; +import org.sopt.makers.crew.main.entity.apply.Apply; import org.sopt.makers.crew.main.entity.apply.ApplyRepository; import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; import org.sopt.makers.crew.main.entity.meeting.Meeting; @@ -62,7 +63,9 @@ public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBod Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); User user = userRepository.findByIdOrThrow(userId); - boolean isInMeeting = meeting.getAppliedInfo().stream() + List applies = applyRepository.findAllById(List.of(meeting.getId())); + + boolean isInMeeting = applies.stream() .anyMatch(apply -> apply.getUserId().equals(userId) && apply.getStatus() == EnApplyStatus.APPROVE); diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java index e5129374..d955c645 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java @@ -4,10 +4,14 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.common.exception.BaseException; import org.sopt.makers.crew.main.entity.apply.ApplyRepository; import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; +import org.sopt.makers.crew.main.entity.meeting.Meeting; +import org.sopt.makers.crew.main.entity.meeting.MeetingRepository; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; @@ -21,45 +25,48 @@ @Transactional(readOnly = true) public class UserV2ServiceImpl implements UserV2Service { - private final UserRepository userRepository; - private final ApplyRepository applyRepository; + private final UserRepository userRepository; + private final ApplyRepository applyRepository; + private final MeetingRepository meetingRepository; + + @Override + public List getAllMeetingByUser(Integer userId) { + User user = userRepository.findByIdOrThrow(userId); - @Override - public List getAllMeetingByUser(Integer userId) { - User user = userRepository.findByIdOrThrow(userId); + List myMeetings = meetingRepository.findAllByUserId(user.getId()); - List userJoinedList = Stream.concat( - user.getMeetings().stream(), - applyRepository.findAllByUserIdAndStatus(userId, EnApplyStatus.APPROVE) - .stream() - .map(apply -> apply.getMeeting()) - ) - .map(meeting -> UserV2GetAllMeetingByUserMeetingDto.of( - meeting.getId(), - meeting.getTitle(), - meeting.getDesc(), - meeting.getImageURL().get(0).getUrl(), - meeting.getCategory().getValue() - )) - .sorted(Comparator.comparing(UserV2GetAllMeetingByUserMeetingDto::getId).reversed()) - .collect(Collectors.toList()); + List userJoinedList = Stream.concat( + myMeetings.stream(), + applyRepository.findAllByUserIdAndStatus(userId, EnApplyStatus.APPROVE) + .stream() + .map(apply -> apply.getMeeting()) + ) + .map(meeting -> UserV2GetAllMeetingByUserMeetingDto.of( + meeting.getId(), + meeting.getTitle(), + meeting.getDesc(), + meeting.getImageURL().get(0).getUrl(), + meeting.getCategory().getValue() + )) + .sorted(Comparator.comparing(UserV2GetAllMeetingByUserMeetingDto::getId).reversed()) + .collect(Collectors.toList()); - if (userJoinedList.isEmpty()) { - throw new BaseException(HttpStatus.NO_CONTENT); - } - return userJoinedList; - } + if (userJoinedList.isEmpty()) { + throw new BaseException(HttpStatus.NO_CONTENT); + } + return userJoinedList; + } - @Override - public List getAllMentionUser() { + @Override + public List getAllMentionUser() { - List users = userRepository.findAll(); + List users = userRepository.findAll(); - return users.stream() - .filter(user -> user.getActivities() != null) - .map(user -> UserV2GetAllMentionUserDto.of(user.getId(), user.getName(), - user.getRecentActivityVO().getPart(), user.getRecentActivityVO().getGeneration(), - user.getProfileImage())) - .toList(); - } + return users.stream() + .filter(user -> user.getActivities() != null) + .map(user -> UserV2GetAllMentionUserDto.of(user.getId(), user.getName(), + user.getRecentActivityVO().getPart(), user.getRecentActivityVO().getGeneration(), + user.getProfileImage())) + .toList(); + } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java index f9b1cfac..c970c079 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java @@ -25,6 +25,7 @@ import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.post.Post; +import org.sopt.makers.crew.main.entity.post.PostRepository; import org.sopt.makers.crew.main.entity.report.Report; import org.sopt.makers.crew.main.entity.report.ReportRepository; import org.sopt.makers.crew.main.entity.user.User; @@ -39,6 +40,8 @@ public class CommentV2ServiceTest { @Mock private CommentRepository commentRepository; @Mock + private PostRepository postRepository; + @Mock private ReportRepository reportRepository; @Mock private UserRepository userRepository; @@ -61,8 +64,12 @@ void init() { String[] images = {"image1", "image2", "image3"}; this.post = Post.builder().user(user).title("title").contents("contents").images(images) .build(); - this.comment = Comment.builder().contents("contents").post(post).user(user).build(); - post.addComment(this.comment); + this.comment = Comment.builder() + .contents("contents") + .post(post) + .postId(1) + .user(user) + .userId(1).build(); this.report = Report.builder().comment(comment).user(user).build(); } @@ -111,6 +118,7 @@ class 댓글_삭제 { // given int initialCommentCount = post.getCommentCount(); doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(post).when(postRepository).findByIdOrThrow((any())); // when commentV2Service.deleteComment(comment.getId(), user.getId()); @@ -125,10 +133,10 @@ class 댓글_삭제 { void 실패_본인_작성_댓글_아님() { // given doReturn(comment).when(commentRepository).findByIdOrThrow(any()); - + Integer id = comment.getUser().getId(); // when & then assertThrows(ForbiddenException.class, () -> { - commentV2Service.deleteComment(comment.getId(), comment.getUser().getId() + 1); + commentV2Service.deleteComment(0, comment.getUser().getId() + 1); }); } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java index b7098eb3..3b961566 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java @@ -10,7 +10,7 @@ import org.sopt.makers.crew.main.common.config.TestConfig; import org.sopt.makers.crew.main.entity.apply.ApplyRepository; import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; -import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetApplyListCommand; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; @@ -42,7 +42,7 @@ public class ApplyRepositoryTest { // given int page = 1; int take = 12; - MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "desc"); + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "desc"); Integer meetingId = 1; Integer studyCreatorId = 1; Integer userId = 1; @@ -96,7 +96,7 @@ public class ApplyRepositoryTest { // given int page = 1; int take = 12; - MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "asc"); + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "asc"); Integer meetingId = 1; Integer studyCreatorId = 1; Integer userId = 1; @@ -148,7 +148,7 @@ public class ApplyRepositoryTest { // given int page = 1; int take = 12; - MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.WAITING), "asc"); + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.WAITING), "asc"); Integer meetingId = 1; Integer studyCreatorId = 1; Integer userId = 1; @@ -180,7 +180,7 @@ public class ApplyRepositoryTest { // given int page = 1; int take = 12; - MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.APPROVE), "asc"); + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.APPROVE), "asc"); Integer meetingId = 1; Integer studyCreatorId = 1; Integer userId = 1; @@ -223,7 +223,7 @@ public class ApplyRepositoryTest { // given int page = 1; int take = 12; - MeetingGetApplyListCommand queryCommand = new MeetingGetApplyListCommand(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "desc"); + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "desc"); Integer meetingId = 1; Integer studyCreatorId = 1; Integer userId = 2; diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java index 66da1f4f..95173295 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java @@ -115,7 +115,7 @@ void init() { .content("제 지원동기는요") .build(); - meeting.addApply(apply); + //meeting.addApply(apply); apply.updateApplyStatus(EnApplyStatus.APPROVE); applies = new ArrayList<>(); @@ -126,6 +126,7 @@ void init() { void 내모임조회_성공() { // given User user = UserFixture.createStaticUser(); + user.setUserIdForTest(3); doReturn(Optional.of(user)).when(userRepository).findByOrgId(any()); doReturn(applies).when(applyRepository).findAllByUserIdAndStatus(any(), any()); @@ -145,6 +146,7 @@ void init() { @Test public void 모임신청시_정원이찼을때_예외발생() { // given + applies = new ArrayList<>(); for (int i = 0; i < meeting.getCapacity(); i++) { User tempUser = User.builder() .name("user" + i) @@ -162,12 +164,14 @@ void init() { .build(); apply.updateApplyStatus(EnApplyStatus.APPROVE); // 승인 상태로 변경 + applies.add(apply); } - applies = new ArrayList<>(meeting.getAppliedInfo()); + MeetingV2ApplyMeetingDto requestBody = new MeetingV2ApplyMeetingDto(meeting.getId(), "열심히 하겠습니다."); doReturn(meeting).when(meetingRepository).findByIdOrThrow(requestBody.getMeetingId()); doReturn(applyUser).when(userRepository).findByIdOrThrow(applyUser.getId()); + doReturn(applies).when(applyRepository).findAllByMeetingId(any()); // when & then BadRequestException exception = assertThrows(BadRequestException.class, () -> { diff --git a/server/src/comment/v1/comment-v1.controller.ts b/server/src/comment/v1/comment-v1.controller.ts index 8b78afc9..bfb127a0 100644 --- a/server/src/comment/v1/comment-v1.controller.ts +++ b/server/src/comment/v1/comment-v1.controller.ts @@ -43,6 +43,7 @@ export class CommentV1Controller { @ApiOperation({ summary: '모임 게시글 댓글 리스트 조회', + deprecated: true, }) @ApiOkResponseCommon(CommentV1GetCommentsResponseDto) @ApiResponse({ From 9f402277902c928bc1d6f502c5b18bf4640845b5 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Fri, 19 Jul 2024 21:49:53 +0900 Subject: [PATCH 27/35] =?UTF-8?q?[FIX]=20userId=20->=20orgId=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20orgId=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentV2MentionUserInCommentRequestDto.java | 4 ++++ .../main/comment/v2/dto/response/CommentDto.java | 4 ++-- .../comment/v2/dto/response/CommentWriterDto.java | 4 +++- .../comment/v2/service/CommentV2ServiceImpl.java | 5 ++--- .../dto/response/UserV2GetAllMentionUserDto.java | 14 +++++++++----- .../main/user/v2/service/UserV2ServiceImpl.java | 2 +- 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java index 5d0c14e0..5d059ef6 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java @@ -14,6 +14,10 @@ @Schema(description = "댓글에서 유저 언급 request body dto") public class CommentV2MentionUserInCommentRequestDto { + /** + * 주의!! : 필드명은 userIds 이지만 실제 요청받는 값은 orgId 입니다. + */ + @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") @NotEmpty private List userIds; diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java index caa2c8bb..945b5a2c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java @@ -35,9 +35,9 @@ public CommentDto(Integer id, String contents, CommentWriterDto user, LocalDateT this.replies = replies; } - public static CommentDto of(Comment comment, boolean isLiked, boolean isWriter, List replies){ + public static CommentDto of(Comment comment, boolean isLiked, boolean isWriter, List replies) { return new CommentDto(comment.getId(), comment.getContents(), - new CommentWriterDto(comment.getUser().getId(), comment.getUser().getName(), + new CommentWriterDto(comment.getUser().getId(), comment.getUser().getOrgId(), comment.getUser().getName(), comment.getUser().getProfileImage()), comment.getUpdatedDate(), comment.getLikeCount(), isLiked, isWriter, comment.getOrder(), replies); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java index 875a7e32..af69fa40 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java @@ -7,12 +7,14 @@ @Getter public class CommentWriterDto { private final Integer id; + private final Integer orgId; private final String name; private final String profileImage; @QueryProjection - public CommentWriterDto(Integer id, String name, String profileImage) { + public CommentWriterDto(Integer id, Integer orgId, String name, String profileImage) { this.id = id; + this.orgId = orgId; this.name = name; this.profileImage = profileImage; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index 6121fe6c..baea35c4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -242,9 +242,8 @@ public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto request user.getName(), requestBody.getContent()); String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - String[] userOrgIds = userRepository.findByIdIn(requestBody.getUserIds()) - .stream() - .map(mentionedUser -> String.valueOf(mentionedUser.getOrgId())) + String[] userOrgIds = requestBody.getUserIds().stream() + .map(Object::toString) .toArray(String[]::new); String newCommentMentionPushNotificationTitle = String.format( diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java index 3b81677c..f25de8b2 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java @@ -6,10 +6,14 @@ @Getter @AllArgsConstructor(staticName = "of") public class UserV2GetAllMentionUserDto { - private Integer userId; - private String userName; - private String recentPart; - private int recentGeneration; - private String profileImageUrl; + /** + * 주의!! : 필드명은 userId 이지만 실제 응답 해야하는 데이터는 orgId 입니다. + */ + private final Integer userId; + + private final String userName; + private final String recentPart; + private final int recentGeneration; + private final String profileImageUrl; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java index d955c645..cba42120 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java @@ -64,7 +64,7 @@ public List getAllMentionUser() { return users.stream() .filter(user -> user.getActivities() != null) - .map(user -> UserV2GetAllMentionUserDto.of(user.getId(), user.getName(), + .map(user -> UserV2GetAllMentionUserDto.of(user.getOrgId(), user.getName(), user.getRecentActivityVO().getPart(), user.getRecentActivityVO().getGeneration(), user.getProfileImage())) .toList(); From f346d1471ac189028dd41f83906b814c4adbad06 Mon Sep 17 00:00:00 2001 From: JiHwan <62228195+sgh002400@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:06:04 +0900 Subject: [PATCH 28/35] =?UTF-8?q?[FIX]=20=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=95=8C=EB=A6=BC=20=EC=8B=9C=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=20=EC=A0=9C=EA=B1=B0=20(#252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 멘션 시 비밀 문자열 제거 Util 구현 * [FIX] 댓글 생성 알림 전송 시 비밀문자열 제거 --------- Co-authored-by: Ji hwan Shin --- .../v2/service/CommentV2ServiceImpl.java | 453 +++++++++--------- .../util/MentionSecretStringRemover.java | 25 + 2 files changed, 257 insertions(+), 221 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index baea35c4..d6a8e2af 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -1,26 +1,27 @@ package org.sopt.makers.crew.main.comment.v2.service; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.*; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_PUSH_NOTIFICATION_TITLE; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; - import lombok.RequiredArgsConstructor; - import org.sopt.makers.crew.main.comment.v2.dto.CommentMapper; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.common.response.ErrorStatus; -import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.common.util.MentionSecretStringRemover; import org.sopt.makers.crew.main.common.util.Time; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; @@ -42,221 +43,231 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class CommentV2ServiceImpl implements CommentV2Service { - private static final int IS_PARENT_COMMENT = 0; - private static final int IS_REPLY_COMMENT = 1; - private static final String DELETE_COMMENT_CONTENT = "삭제된 댓글입니다."; - - private final PostRepository postRepository; - private final UserRepository userRepository; - private final CommentRepository commentRepository; - private final ReportRepository reportRepository; - private final LikeRepository likeRepository; - - private final PushNotificationService pushNotificationService; - - private final CommentMapper commentMapper; - - @Value("${push-notification.web-url}") - private String pushWebUrl; - - private final Time time; - - /** - * 모임 게시글 댓글 작성 - * - * @throws 400 존재하지 않는 게시글일 떄 - * @apiNote 모임에 속한 유저만 작성 가능 - */ - @Override - @Transactional - public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, - Integer userId) { - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - User writer = userRepository.findByIdOrThrow(userId); - - int depth = 0; - int order = 0; - Integer parentId = 0; - - boolean isReplyComment = !requestBody.isParent(); - if (isReplyComment) { - validateParentCommentId(requestBody); - depth = 1; - parentId = requestBody.getParentCommentId(); - order = getOrder(parentId); - } - - Comment comment = commentMapper.toComment(requestBody, post, writer, depth, order, parentId); - - Comment savedComment = commentRepository.save(comment); - post.increaseCommentCount(); - - sendPushNotification(requestBody, post, writer); - - return CommentV2CreateCommentResponseDto.of(savedComment.getId()); - } - - private void sendPushNotification(CommentV2CreateCommentBodyDto requestBody, Post post, User user) { - User PostWriter = post.getUser(); - String[] userIds = {String.valueOf(PostWriter.getOrgId())}; - - String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", - user.getName(), requestBody.getContents()); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); - - pushNotificationService.sendPushNotification(pushRequestDto); - } - - private void validateParentCommentId(CommentV2CreateCommentBodyDto requestBody) { - commentRepository.findByIdAndPostIdOrThrow(requestBody.getParentCommentId(), requestBody.getPostId()); - } - - private int getOrder(Integer parentId) { - Optional recentComment = commentRepository.findFirstByParentIdOrderByOrderDesc( - parentId); - return recentComment.map(comment -> comment.getOrder() + 1).orElse(1); - } - - /** - * 모임 게시글 댓글 수정 - * - * @param commentId 수정할 댓글 ID - * @param contents 수정할 내용 - * @param userId 수정하는 유저 ID - * @return 수정된 댓글 정보 - */ - @Override - @Transactional - public CommentV2UpdateCommentResponseDto updateComment(Integer commentId, - String contents, Integer userId) { - // 1. id를 기반으로 comment를 찾는다. - Comment comment = commentRepository.findByIdOrThrow(commentId); - - // 2. comment의 user_id와 userId가 같은지 확인한다. - comment.validateWriter(userId); - - // 3. comment의 contents를 수정한다. - comment.updateContents(contents, time.now()); - - // 4. 수정된 comment의 id, contents, updatedDate를 반환한다. - return CommentV2UpdateCommentResponseDto.of(comment.getId(), comment.getContents(), - String.valueOf(comment.getUpdatedDate())); - } - - @Override - public CommentV2GetCommentsResponseDto getComments(Integer postId, Integer page, Integer take, Integer userId) { - // TODO : 페이지네이션 구현 - - List comments = commentRepository.findAllByPostIdOrderByCreatedDate(postId); - - MyLikes myLikes = new MyLikes(likeRepository.findAllByUserIdAndPostIdNotNull(userId)); - - Map> replyMap = new HashMap<>(); - comments.stream() - .filter(comment -> !comment.isParentComment()) - .forEach(comment -> replyMap.computeIfAbsent(comment.getParentId(), k -> new ArrayList<>()) - .add(CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), comment.isWriter(userId), null))); - - List commentDtos = comments.stream() - .filter(Comment::isParentComment) - .map(comment -> CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), comment.isWriter(userId), - replyMap.get(comment.getId()))) - .toList(); - - return CommentV2GetCommentsResponseDto.of(commentDtos); - } - - /** - * 댓글 신고하기 - * - * @param commentId 댓글 신고할 댓글 id - * @param userId 신고하는 유저 id - * @return 신고 ID - * @throws BadRequestException 이미 신고한 댓글일 때 - * @apiNote 댓글 신고는 한 댓글당 한번만 가능 - */ - @Override - @Transactional - public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) - throws BadRequestException { - Comment comment = commentRepository.findByIdOrThrow(commentId); - User user = userRepository.findByIdOrThrow(userId); - - Optional existingReport = reportRepository.findByCommentAndUser(comment, user); - - if (existingReport.isPresent()) { - throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); - } - - Report report = Report.builder() - .comment(comment) - .user(user) - .build(); - - Report savedReport = reportRepository.save(report); - - return CommentV2ReportCommentResponseDto.of(savedReport.getId()); - } - - /** - * 모임 게시글 댓글 삭제 - * - * @throws ForbiddenException 댓글 작성자가 아닐 때 - * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 - */ - @Override - @Transactional - public void deleteComment(Integer commentId, Integer userId) throws ForbiddenException { - Comment comment = commentRepository.findByIdOrThrow(commentId); - - if (!comment.getUserId().equals(userId)) { - throw new ForbiddenException(); - } - - Post post = postRepository.findByIdOrThrow(comment.getPostId()); - post.decreaseCommentCount(); - - Optional childComment = commentRepository.findFirstByParentIdOrderByOrderDesc( - comment.getId()); - - if (comment.getDepth() == IS_REPLY_COMMENT || childComment.isEmpty()) { - commentRepository.delete(comment); - return; - } - - comment.deleteParentComment(DELETE_COMMENT_CONTENT, null, null); - } - - @Override - public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, - Integer userId) { - User user = userRepository.findByIdOrThrow(userId); - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - - String pushNotificationContent = String.format("[%s님이 회원님을 언급했어요.] : \"%s\"", - user.getName(), requestBody.getContent()); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - - String[] userOrgIds = requestBody.getUserIds().stream() - .map(Object::toString) - .toArray(String[]::new); - - String newCommentMentionPushNotificationTitle = String.format( - NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), user.getName()); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( - userOrgIds, - newCommentMentionPushNotificationTitle, - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), - pushNotificationWeblink - ); - - pushNotificationService.sendPushNotification(pushRequestDto); - } + + private static final int IS_PARENT_COMMENT = 0; + private static final int IS_REPLY_COMMENT = 1; + private static final String DELETE_COMMENT_CONTENT = "삭제된 댓글입니다."; + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final CommentRepository commentRepository; + private final ReportRepository reportRepository; + private final LikeRepository likeRepository; + + private final PushNotificationService pushNotificationService; + + private final CommentMapper commentMapper; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + private final Time time; + + /** + * 모임 게시글 댓글 작성 + * + * @throws 400 존재하지 않는 게시글일 떄 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + public CommentV2CreateCommentResponseDto createComment( + CommentV2CreateCommentBodyDto requestBody, + Integer userId) { + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + User writer = userRepository.findByIdOrThrow(userId); + + int depth = 0; + int order = 0; + Integer parentId = 0; + + boolean isReplyComment = !requestBody.isParent(); + if (isReplyComment) { + validateParentCommentId(requestBody); + depth = 1; + parentId = requestBody.getParentCommentId(); + order = getOrder(parentId); + } + + Comment comment = commentMapper.toComment(requestBody, post, writer, depth, order, + parentId); + + Comment savedComment = commentRepository.save(comment); + post.increaseCommentCount(); + + sendPushNotification(requestBody, post, writer); + + return CommentV2CreateCommentResponseDto.of(savedComment.getId()); + } + + private void sendPushNotification(CommentV2CreateCommentBodyDto requestBody, Post post, + User user) { + User PostWriter = post.getUser(); + String[] userIds = {String.valueOf(PostWriter.getOrgId())}; + String secretStringRemovedContent = MentionSecretStringRemover.removeSecretString( + requestBody.getContents()); + String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", + user.getName(), secretStringRemovedContent); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + } + + private void validateParentCommentId(CommentV2CreateCommentBodyDto requestBody) { + commentRepository.findByIdAndPostIdOrThrow(requestBody.getParentCommentId(), + requestBody.getPostId()); + } + + private int getOrder(Integer parentId) { + Optional recentComment = commentRepository.findFirstByParentIdOrderByOrderDesc( + parentId); + return recentComment.map(comment -> comment.getOrder() + 1).orElse(1); + } + + /** + * 모임 게시글 댓글 수정 + * + * @param commentId 수정할 댓글 ID + * @param contents 수정할 내용 + * @param userId 수정하는 유저 ID + * @return 수정된 댓글 정보 + */ + @Override + @Transactional + public CommentV2UpdateCommentResponseDto updateComment(Integer commentId, + String contents, Integer userId) { + // 1. id를 기반으로 comment를 찾는다. + Comment comment = commentRepository.findByIdOrThrow(commentId); + + // 2. comment의 user_id와 userId가 같은지 확인한다. + comment.validateWriter(userId); + + // 3. comment의 contents를 수정한다. + comment.updateContents(contents, time.now()); + + // 4. 수정된 comment의 id, contents, updatedDate를 반환한다. + return CommentV2UpdateCommentResponseDto.of(comment.getId(), comment.getContents(), + String.valueOf(comment.getUpdatedDate())); + } + + @Override + public CommentV2GetCommentsResponseDto getComments(Integer postId, Integer page, Integer take, + Integer userId) { + // TODO : 페이지네이션 구현 + + List comments = commentRepository.findAllByPostIdOrderByCreatedDate(postId); + + MyLikes myLikes = new MyLikes(likeRepository.findAllByUserIdAndPostIdNotNull(userId)); + + Map> replyMap = new HashMap<>(); + comments.stream() + .filter(comment -> !comment.isParentComment()) + .forEach( + comment -> replyMap.computeIfAbsent(comment.getParentId(), k -> new ArrayList<>()) + .add(CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), + comment.isWriter(userId), null))); + + List commentDtos = comments.stream() + .filter(Comment::isParentComment) + .map(comment -> CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), + comment.isWriter(userId), + replyMap.get(comment.getId()))) + .toList(); + + return CommentV2GetCommentsResponseDto.of(commentDtos); + } + + /** + * 댓글 신고하기 + * + * @param commentId 댓글 신고할 댓글 id + * @param userId 신고하는 유저 id + * @return 신고 ID + * @throws BadRequestException 이미 신고한 댓글일 때 + * @apiNote 댓글 신고는 한 댓글당 한번만 가능 + */ + @Override + @Transactional + public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException { + Comment comment = commentRepository.findByIdOrThrow(commentId); + User user = userRepository.findByIdOrThrow(userId); + + Optional existingReport = reportRepository.findByCommentAndUser(comment, user); + + if (existingReport.isPresent()) { + throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); + } + + Report report = Report.builder() + .comment(comment) + .user(user) + .build(); + + Report savedReport = reportRepository.save(report); + + return CommentV2ReportCommentResponseDto.of(savedReport.getId()); + } + + /** + * 모임 게시글 댓글 삭제 + * + * @throws ForbiddenException 댓글 작성자가 아닐 때 + * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 + */ + @Override + @Transactional + public void deleteComment(Integer commentId, Integer userId) throws ForbiddenException { + Comment comment = commentRepository.findByIdOrThrow(commentId); + + if (!comment.getUserId().equals(userId)) { + throw new ForbiddenException(); + } + + Post post = postRepository.findByIdOrThrow(comment.getPostId()); + post.decreaseCommentCount(); + + Optional childComment = commentRepository.findFirstByParentIdOrderByOrderDesc( + comment.getId()); + + if (comment.getDepth() == IS_REPLY_COMMENT || childComment.isEmpty()) { + commentRepository.delete(comment); + return; + } + + comment.deleteParentComment(DELETE_COMMENT_CONTENT, null, null); + } + + @Override + public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, + Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + + String pushNotificationContent = String.format("[%s님이 회원님을 언급했어요.] : \"%s\"", + user.getName(), requestBody.getContent()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + String[] userOrgIds = requestBody.getUserIds().stream() + .map(Object::toString) + .toArray(String[]::new); + + String newCommentMentionPushNotificationTitle = String.format( + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), user.getName()); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( + userOrgIds, + newCommentMentionPushNotificationTitle, + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), + pushNotificationWeblink + ); + + pushNotificationService.sendPushNotification(pushRequestDto); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java new file mode 100644 index 00000000..be823596 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java @@ -0,0 +1,25 @@ +package org.sopt.makers.crew.main.common.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class MentionSecretStringRemover { + + private static final String PREFIX_PATTERN = "-!@#"; + private static final String SUFFIX_PATTERN = "\\[\\d+\\]%\\^&\\+"; + + public static String removeSecretString(String content) { + Pattern prefixPattern = Pattern.compile(PREFIX_PATTERN); + Pattern suffixPattern = Pattern.compile(SUFFIX_PATTERN); + + Matcher prefixMatcher = prefixPattern.matcher(content); + content = prefixMatcher.replaceAll(""); + + Matcher suffixMatcher = suffixPattern.matcher(content); + content = suffixMatcher.replaceAll(""); + + return content; + } +} From f7761a03f007e493f1efaf4b2d424adaf4f554ce Mon Sep 17 00:00:00 2001 From: JiHwan <62228195+sgh002400@users.noreply.github.com> Date: Sat, 20 Jul 2024 19:10:45 +0900 Subject: [PATCH 29/35] =?UTF-8?q?[FIX]=20=EB=B9=84=EB=B0=80=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=ED=8C=A8=ED=84=B4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ji hwan Shin --- .../crew/main/common/util/MentionSecretStringRemover.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java index be823596..53020d71 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java @@ -7,8 +7,8 @@ @UtilityClass public class MentionSecretStringRemover { - private static final String PREFIX_PATTERN = "-!@#"; - private static final String SUFFIX_PATTERN = "\\[\\d+\\]%\\^&\\+"; + private static final String PREFIX_PATTERN = "-~!@#"; + private static final String SUFFIX_PATTERN = "\\[\\d+\\]%\\^&\\*\\+"; public static String removeSecretString(String content) { Pattern prefixPattern = Pattern.compile(PREFIX_PATTERN); From 664d7a32397ea62271a975a8b694765107c3854a Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:59:43 +0900 Subject: [PATCH 30/35] =?UTF-8?q?[CHORE]=20=EC=95=8C=EB=A6=BC=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EB=B3=80=EA=B2=BD=20(#257)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [CHORE] 알림 메세지 변경 * [CHORE] 따옴표 추가 --- .../crew/main/comment/v2/service/CommentV2ServiceImpl.java | 7 ++----- .../main/internal/notification/PushNotificationEnums.java | 7 ++++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index d6a8e2af..cce514fd 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -1,8 +1,6 @@ package org.sopt.makers.crew.main.comment.v2.service; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_PUSH_NOTIFICATION_TITLE; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.*; import java.util.ArrayList; import java.util.HashMap; @@ -249,8 +247,7 @@ public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto request User user = userRepository.findByIdOrThrow(userId); Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - String pushNotificationContent = String.format("[%s님이 회원님을 언급했어요.] : \"%s\"", - user.getName(), requestBody.getContent()); + String pushNotificationContent = NEW_COMMENT_MENTION_PUSH_NOTIFICATION_CONTENT.getValue(); String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); String[] userOrgIds = requestBody.getUserIds().stream() diff --git a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java index 999ff836..68f22341 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java +++ b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java @@ -12,9 +12,14 @@ public enum PushNotificationEnums { PUSH_NOTIFICATION_CATEGORY("NEWS"), NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."), - NEW_COMMENT_PUSH_NOTIFICATION_TITLE("📢내가 작성한 모임 피드에 새로운 댓글이 달렸어요."), + NEW_COMMENT_PUSH_NOTIFICATION_TITLE("💬나의 모임 피드에 새로운 댓글이 달렸어요."), + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE("💬%s님이 회원님을 언급했어요."), + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_CONTENT("\"해당 댓글 미리보기\""), + NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE("✏️모임 피드에서 회원님이 언급됐어요."), + + ; private final String value; } From f91dbd734bfedd6eccacbe03e8ea0b8ff88259ce Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:47:31 +0900 Subject: [PATCH 31/35] =?UTF-8?q?[FIX]=20=ED=94=BC=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20API=20=EC=88=98=EC=A0=95=20(#262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../makers/crew/main/post/v2/service/PostV2ServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java index f5ff5a42..dee4a582 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java @@ -63,11 +63,11 @@ public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBod Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); User user = userRepository.findByIdOrThrow(userId); - List applies = applyRepository.findAllById(List.of(meeting.getId())); + List applies = applyRepository.findAllByMeetingId(meeting.getId()); boolean isInMeeting = applies.stream() .anyMatch(apply -> apply.getUserId().equals(userId) - && apply.getStatus() == EnApplyStatus.APPROVE); + && apply.getStatus().equals(EnApplyStatus.APPROVE)); boolean isMeetingCreator = meeting.getUserId().equals(userId); From c45afa0e43bad913069301386446affb0ecc68bc Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Wed, 24 Jul 2024 00:00:31 +0900 Subject: [PATCH 32/35] =?UTF-8?q?[CHORE]=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20=EC=88=98=EC=A0=95=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=9D=BC=EB=B6=80=20=EC=88=98=EC=A0=95=20(#260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/v2/service/CommentV2Service.java | 2 +- .../v2/service/CommentV2ServiceImpl.java | 450 +++++++++--------- .../util/MentionSecretStringRemover.java | 10 + .../crew/main/entity/comment/Comment.java | 13 +- .../entity/comment/CommentRepository.java | 2 + .../crew/main/entity/comment/Comments.java | 24 + 6 files changed, 268 insertions(+), 233 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comments.java diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index fe18ffb7..39a95535 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -17,7 +17,7 @@ CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto re CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) throws BadRequestException; - void deleteComment(Integer commentId, Integer userId) throws ForbiddenException; + void deleteComment(Integer commentId, Integer userId); void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, Integer userId); diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index cce514fd..bf025bc5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -7,7 +7,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.comment.v2.dto.CommentMapper; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; @@ -23,6 +25,7 @@ import org.sopt.makers.crew.main.common.util.Time; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; +import org.sopt.makers.crew.main.entity.comment.Comments; import org.sopt.makers.crew.main.entity.like.LikeRepository; import org.sopt.makers.crew.main.entity.like.MyLikes; import org.sopt.makers.crew.main.entity.post.Post; @@ -41,230 +44,225 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class CommentV2ServiceImpl implements CommentV2Service { - - private static final int IS_PARENT_COMMENT = 0; - private static final int IS_REPLY_COMMENT = 1; - private static final String DELETE_COMMENT_CONTENT = "삭제된 댓글입니다."; - - private final PostRepository postRepository; - private final UserRepository userRepository; - private final CommentRepository commentRepository; - private final ReportRepository reportRepository; - private final LikeRepository likeRepository; - - private final PushNotificationService pushNotificationService; - - private final CommentMapper commentMapper; - - @Value("${push-notification.web-url}") - private String pushWebUrl; - - private final Time time; - - /** - * 모임 게시글 댓글 작성 - * - * @throws 400 존재하지 않는 게시글일 떄 - * @apiNote 모임에 속한 유저만 작성 가능 - */ - @Override - @Transactional - public CommentV2CreateCommentResponseDto createComment( - CommentV2CreateCommentBodyDto requestBody, - Integer userId) { - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - User writer = userRepository.findByIdOrThrow(userId); - - int depth = 0; - int order = 0; - Integer parentId = 0; - - boolean isReplyComment = !requestBody.isParent(); - if (isReplyComment) { - validateParentCommentId(requestBody); - depth = 1; - parentId = requestBody.getParentCommentId(); - order = getOrder(parentId); - } - - Comment comment = commentMapper.toComment(requestBody, post, writer, depth, order, - parentId); - - Comment savedComment = commentRepository.save(comment); - post.increaseCommentCount(); - - sendPushNotification(requestBody, post, writer); - - return CommentV2CreateCommentResponseDto.of(savedComment.getId()); - } - - private void sendPushNotification(CommentV2CreateCommentBodyDto requestBody, Post post, - User user) { - User PostWriter = post.getUser(); - String[] userIds = {String.valueOf(PostWriter.getOrgId())}; - String secretStringRemovedContent = MentionSecretStringRemover.removeSecretString( - requestBody.getContents()); - String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", - user.getName(), secretStringRemovedContent); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); - - pushNotificationService.sendPushNotification(pushRequestDto); - } - - private void validateParentCommentId(CommentV2CreateCommentBodyDto requestBody) { - commentRepository.findByIdAndPostIdOrThrow(requestBody.getParentCommentId(), - requestBody.getPostId()); - } - - private int getOrder(Integer parentId) { - Optional recentComment = commentRepository.findFirstByParentIdOrderByOrderDesc( - parentId); - return recentComment.map(comment -> comment.getOrder() + 1).orElse(1); - } - - /** - * 모임 게시글 댓글 수정 - * - * @param commentId 수정할 댓글 ID - * @param contents 수정할 내용 - * @param userId 수정하는 유저 ID - * @return 수정된 댓글 정보 - */ - @Override - @Transactional - public CommentV2UpdateCommentResponseDto updateComment(Integer commentId, - String contents, Integer userId) { - // 1. id를 기반으로 comment를 찾는다. - Comment comment = commentRepository.findByIdOrThrow(commentId); - - // 2. comment의 user_id와 userId가 같은지 확인한다. - comment.validateWriter(userId); - - // 3. comment의 contents를 수정한다. - comment.updateContents(contents, time.now()); - - // 4. 수정된 comment의 id, contents, updatedDate를 반환한다. - return CommentV2UpdateCommentResponseDto.of(comment.getId(), comment.getContents(), - String.valueOf(comment.getUpdatedDate())); - } - - @Override - public CommentV2GetCommentsResponseDto getComments(Integer postId, Integer page, Integer take, - Integer userId) { - // TODO : 페이지네이션 구현 - - List comments = commentRepository.findAllByPostIdOrderByCreatedDate(postId); - - MyLikes myLikes = new MyLikes(likeRepository.findAllByUserIdAndPostIdNotNull(userId)); - - Map> replyMap = new HashMap<>(); - comments.stream() - .filter(comment -> !comment.isParentComment()) - .forEach( - comment -> replyMap.computeIfAbsent(comment.getParentId(), k -> new ArrayList<>()) - .add(CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), - comment.isWriter(userId), null))); - - List commentDtos = comments.stream() - .filter(Comment::isParentComment) - .map(comment -> CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), - comment.isWriter(userId), - replyMap.get(comment.getId()))) - .toList(); - - return CommentV2GetCommentsResponseDto.of(commentDtos); - } - - /** - * 댓글 신고하기 - * - * @param commentId 댓글 신고할 댓글 id - * @param userId 신고하는 유저 id - * @return 신고 ID - * @throws BadRequestException 이미 신고한 댓글일 때 - * @apiNote 댓글 신고는 한 댓글당 한번만 가능 - */ - @Override - @Transactional - public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) - throws BadRequestException { - Comment comment = commentRepository.findByIdOrThrow(commentId); - User user = userRepository.findByIdOrThrow(userId); - - Optional existingReport = reportRepository.findByCommentAndUser(comment, user); - - if (existingReport.isPresent()) { - throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); - } - - Report report = Report.builder() - .comment(comment) - .user(user) - .build(); - - Report savedReport = reportRepository.save(report); - - return CommentV2ReportCommentResponseDto.of(savedReport.getId()); - } - - /** - * 모임 게시글 댓글 삭제 - * - * @throws ForbiddenException 댓글 작성자가 아닐 때 - * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 - */ - @Override - @Transactional - public void deleteComment(Integer commentId, Integer userId) throws ForbiddenException { - Comment comment = commentRepository.findByIdOrThrow(commentId); - - if (!comment.getUserId().equals(userId)) { - throw new ForbiddenException(); - } - - Post post = postRepository.findByIdOrThrow(comment.getPostId()); - post.decreaseCommentCount(); - - Optional childComment = commentRepository.findFirstByParentIdOrderByOrderDesc( - comment.getId()); - - if (comment.getDepth() == IS_REPLY_COMMENT || childComment.isEmpty()) { - commentRepository.delete(comment); - return; - } - - comment.deleteParentComment(DELETE_COMMENT_CONTENT, null, null); - } - - @Override - public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, - Integer userId) { - User user = userRepository.findByIdOrThrow(userId); - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - - String pushNotificationContent = NEW_COMMENT_MENTION_PUSH_NOTIFICATION_CONTENT.getValue(); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - - String[] userOrgIds = requestBody.getUserIds().stream() - .map(Object::toString) - .toArray(String[]::new); - - String newCommentMentionPushNotificationTitle = String.format( - NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), user.getName()); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( - userOrgIds, - newCommentMentionPushNotificationTitle, - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), - pushNotificationWeblink - ); - - pushNotificationService.sendPushNotification(pushRequestDto); - } + private static final int IS_REPLY_COMMENT = 1; + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final CommentRepository commentRepository; + private final ReportRepository reportRepository; + private final LikeRepository likeRepository; + + private final PushNotificationService pushNotificationService; + + private final CommentMapper commentMapper; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + private final Time time; + + /** + * 모임 게시글 댓글 작성 + * + * @throws 400 존재하지 않는 게시글일 떄 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + public CommentV2CreateCommentResponseDto createComment( + CommentV2CreateCommentBodyDto requestBody, + Integer userId) { + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + User writer = userRepository.findByIdOrThrow(userId); + + int depth = 0; + int order = 0; + Integer parentId = 0; + + boolean isReplyComment = !requestBody.isParent(); + if (isReplyComment) { + validateParentCommentId(requestBody); + depth = 1; + parentId = requestBody.getParentCommentId(); + order = getOrder(parentId); + } + + Comment comment = commentMapper.toComment(requestBody, post, writer, depth, order, + parentId); + + Comment savedComment = commentRepository.save(comment); + post.increaseCommentCount(); + + sendPushNotification(requestBody, post, writer); + + return CommentV2CreateCommentResponseDto.of(savedComment.getId()); + } + + private void sendPushNotification(CommentV2CreateCommentBodyDto requestBody, Post post, + User user) { + User PostWriter = post.getUser(); + String[] userIds = {String.valueOf(PostWriter.getOrgId())}; + String secretStringRemovedContent = MentionSecretStringRemover.removeSecretString( + requestBody.getContents()); + String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", + user.getName(), secretStringRemovedContent); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + } + + private void validateParentCommentId(CommentV2CreateCommentBodyDto requestBody) { + commentRepository.findByIdAndPostIdOrThrow(requestBody.getParentCommentId(), + requestBody.getPostId()); + } + + private int getOrder(Integer parentId) { + Optional recentComment = commentRepository.findFirstByParentIdOrderByOrderDesc( + parentId); + return recentComment.map(comment -> comment.getOrder() + 1).orElse(1); + } + + /** + * 모임 게시글 댓글 수정 + * + * @param commentId 수정할 댓글 ID + * @param contents 수정할 내용 + * @param userId 수정하는 유저 ID + * @return 수정된 댓글 정보 + */ + @Override + @Transactional + public CommentV2UpdateCommentResponseDto updateComment(Integer commentId, + String contents, Integer userId) { + // 1. id를 기반으로 comment를 찾는다. + Comment comment = commentRepository.findByIdOrThrow(commentId); + + // 2. comment의 user_id와 userId가 같은지 확인한다. + comment.validateWriter(userId); + + // 3. comment의 contents를 수정한다. + comment.updateContents(contents); + + // 4. 수정된 comment의 id, contents, updatedDate를 반환한다. + return CommentV2UpdateCommentResponseDto.of(comment.getId(), comment.getContents(), + String.valueOf(time.now())); + } + + @Override + public CommentV2GetCommentsResponseDto getComments(Integer postId, Integer page, Integer take, + Integer userId) { + // TODO : 페이지네이션 구현 + + List comments = commentRepository.findAllByPostIdOrderByCreatedDate(postId); + + MyLikes myLikes = new MyLikes(likeRepository.findAllByUserIdAndPostIdNotNull(userId)); + + Map> replyMap = new HashMap<>(); + comments.stream() + .filter(comment -> !comment.isParentComment()) + .forEach( + comment -> replyMap.computeIfAbsent(comment.getParentId(), k -> new ArrayList<>()) + .add(CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), + comment.isWriter(userId), null))); + + List commentDtos = comments.stream() + .filter(Comment::isParentComment) + .map(comment -> CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), + comment.isWriter(userId), + replyMap.get(comment.getId()))) + .toList(); + + return CommentV2GetCommentsResponseDto.of(commentDtos); + } + + /** + * 댓글 신고하기 + * + * @param commentId 댓글 신고할 댓글 id + * @param userId 신고하는 유저 id + * @return 신고 ID + * @throws BadRequestException 이미 신고한 댓글일 때 + * @apiNote 댓글 신고는 한 댓글당 한번만 가능 + */ + @Override + @Transactional + public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException { + Comment comment = commentRepository.findByIdOrThrow(commentId); + User user = userRepository.findByIdOrThrow(userId); + + Optional existingReport = reportRepository.findByCommentAndUser(comment, user); + + if (existingReport.isPresent()) { + throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); + } + + Report report = Report.builder() + .comment(comment) + .user(user) + .build(); + + Report savedReport = reportRepository.save(report); + + return CommentV2ReportCommentResponseDto.of(savedReport.getId()); + } + + /** + * 모임 게시글 댓글 삭제 + * + * @throws ForbiddenException 댓글 작성자가 아닐 때 + * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 + */ + @Override + @Transactional + public void deleteComment(Integer commentId, Integer userId) { + Comment comment = commentRepository.findByIdOrThrow(commentId); + comment.validateWriter(userId); + User user = comment.getUser(); + + Post post = postRepository.findByIdOrThrow(comment.getPostId()); + post.decreaseCommentCount(); + + Comments childComments = new Comments(commentRepository.findAllByParentIdOrderByOrderDesc(comment.getId())); + + if (comment.getDepth() == IS_REPLY_COMMENT || !childComments.hasChild()) { + commentRepository.delete(comment); + return; + } + + comment.deleteParentComment(); + childComments.deleteMention(user.getName(), user.getOrgId().toString()); + } + + @Override + public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, + Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + + String pushNotificationContent = NEW_COMMENT_MENTION_PUSH_NOTIFICATION_CONTENT.getValue(); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + String[] userOrgIds = requestBody.getUserIds().stream() + .map(Object::toString) + .toArray(String[]::new); + + String newCommentMentionPushNotificationTitle = String.format( + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), user.getName()); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( + userOrgIds, + newCommentMentionPushNotificationTitle, + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), + pushNotificationWeblink + ); + + pushNotificationService.sendPushNotification(pushRequestDto); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java index 53020d71..55319527 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java @@ -10,6 +10,9 @@ public class MentionSecretStringRemover { private static final String PREFIX_PATTERN = "-~!@#"; private static final String SUFFIX_PATTERN = "\\[\\d+\\]%\\^&\\*\\+"; + private static final String DELETED_PREFIX_PATTERN = "-~!@#@"; + private static final String DELETED_SUFFIX_PATTERN = "%^&*+"; + public static String removeSecretString(String content) { Pattern prefixPattern = Pattern.compile(PREFIX_PATTERN); Pattern suffixPattern = Pattern.compile(SUFFIX_PATTERN); @@ -22,4 +25,11 @@ public static String removeSecretString(String content) { return content; } + + public static String deleteMentionContent(String content, String mentionName, String mentionOrgId) { + String deletedMentionContent = + DELETED_PREFIX_PATTERN + mentionName + "[" + mentionOrgId + "]" + DELETED_SUFFIX_PATTERN; + + return content.replace(deletedMentionContent, "@_"); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index daef6b01..76d44d4d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -36,6 +36,8 @@ public class Comment { private static final int PARENT_COMMENT = 0; + private static final String DELETE_COMMENT_CONTENT = "삭제된 댓글입니다."; + /** * 댓글의 고유 식별자 @@ -136,15 +138,14 @@ public Comment(String contents, int depth, int order, User user, Integer userId, this.parentId = parentId; } - public void updateContents(String contents, LocalDateTime updatedDate) { + public void updateContents(String contents) { this.contents = contents; - this.updatedDate = updatedDate; } - public void deleteParentComment(String contents, User user, Integer userId) { - this.contents = contents; - this.user = user; - this.userId = userId; + public void deleteParentComment() { + this.contents = DELETE_COMMENT_CONTENT; + this.user = null; + this.userId = null; } public void validateWriter(Integer userId) { diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java index e3faa2b0..e23cb8f5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java @@ -16,6 +16,8 @@ default Comment findByIdOrThrow(Integer commentId) { Optional findFirstByParentIdOrderByOrderDesc(Integer parentId); + List findAllByParentIdOrderByOrderDesc(Integer parentId); + Optional findByIdAndPostId(Integer id, Integer postId); default Comment findByIdAndPostIdOrThrow(Integer id, Integer postId){ diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comments.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comments.java new file mode 100644 index 00000000..bb5b91b0 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comments.java @@ -0,0 +1,24 @@ +package org.sopt.makers.crew.main.entity.comment; + +import java.util.List; + +import org.sopt.makers.crew.main.common.util.MentionSecretStringRemover; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class Comments { + private final List comments; + + public void deleteMention(String mentionName, String mentionOrgId){ + comments.forEach(comment -> { + String deletedMentionContent = MentionSecretStringRemover.deleteMentionContent(comment.getContents(), + mentionName, mentionOrgId); + comment.updateContents(deletedMentionContent); + }); + } + + public boolean hasChild(){ + return !comments.isEmpty(); + } +} From 5784dd4e496057a5886a852818f2383786ca40d8 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Wed, 24 Jul 2024 00:43:53 +0900 Subject: [PATCH 33/35] =?UTF-8?q?[FIX]=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=88=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20(#264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java index b8367b78..48bf7f21 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java @@ -13,6 +13,7 @@ public interface CommentMapper { @Mapping(source = "requestBody.contents", target = "contents") @Mapping(source = "user", target = "user") @Mapping(source = "user.id", target = "userId") + @Mapping(target = "likeCount", constant = "0") Comment toComment(CommentV2CreateCommentBodyDto requestBody, Post post, User user, int depth, int order, Integer parentId); } From 4a723c31e279eeb8975c7d6958b2f6da2ac4bfd7 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:27:12 +0900 Subject: [PATCH 34/35] =?UTF-8?q?[FIX]=20=EB=A9=98=EC=85=98=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EB=A9=98=ED=8A=B8=20=EB=B3=80=EA=B2=BD=20(#267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/main/comment/v2/service/CommentV2ServiceImpl.java | 2 +- .../crew/main/internal/notification/PushNotificationEnums.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index bf025bc5..a830c67e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -245,7 +245,7 @@ public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto request User user = userRepository.findByIdOrThrow(userId); Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - String pushNotificationContent = NEW_COMMENT_MENTION_PUSH_NOTIFICATION_CONTENT.getValue(); + String pushNotificationContent = requestBody.getContent(); String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); String[] userOrgIds = requestBody.getUserIds().stream() diff --git a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java index 68f22341..f8e92a3d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java +++ b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java @@ -15,7 +15,6 @@ public enum PushNotificationEnums { NEW_COMMENT_PUSH_NOTIFICATION_TITLE("💬나의 모임 피드에 새로운 댓글이 달렸어요."), NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE("💬%s님이 회원님을 언급했어요."), - NEW_COMMENT_MENTION_PUSH_NOTIFICATION_CONTENT("\"해당 댓글 미리보기\""), NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE("✏️모임 피드에서 회원님이 언급됐어요."), From 67dc9d11e0118f00998e4da241634ff9ecb1518b Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:38:22 +0900 Subject: [PATCH 35/35] =?UTF-8?q?[CHORE]=20=ED=81=B0=20=EB=94=B0=EC=98=B4?= =?UTF-8?q?=ED=91=9C=20=EC=B6=94=EA=B0=80=20(#269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/main/comment/v2/service/CommentV2ServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index a830c67e..6008f8b4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -245,7 +245,7 @@ public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto request User user = userRepository.findByIdOrThrow(userId); Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - String pushNotificationContent = requestBody.getContent(); + String pushNotificationContent = "\"" + requestBody.getContent() + "\""; String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); String[] userOrgIds = requestBody.getUserIds().stream()