From 84d07b222eefca7cbc3c3fd3e306fd034a62792a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC=20=28YeongMin=20Song=29?= Date: Fri, 12 Jan 2024 18:58:00 +0900 Subject: [PATCH 01/49] docs: add new properties to application local template --- gateway/src/main/resources/template-application-local.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gateway/src/main/resources/template-application-local.yaml b/gateway/src/main/resources/template-application-local.yaml index 0c3e1337..28e48c61 100644 --- a/gateway/src/main/resources/template-application-local.yaml +++ b/gateway/src/main/resources/template-application-local.yaml @@ -13,6 +13,10 @@ spring: show_sql: true format_sql: true app: + oauth: + google-client-id: ${GOOGLE_CLIENT_ID} + web: + versionFilterEnabled: false external-urls: slack-webhook: ${SLACK_WEBHOOK_URL} # Must Be Replaced token: @@ -24,5 +28,6 @@ cloud: end-point: ${OBJECT_STORAGE_END_POINT} access-key: ${OBJECT_STORAGE_ACCESS_KEY} secret-key: ${OBJECT_STORAGE_SECRET_KEY} + image-optimizer-cdn: ${IMAGE_OPTIMIZER_CDN_URL} storage: bucket: ${OBJECT_STORAGE_BUCKET_NAME} From 802b268e5845ec0890938244cd951156915b4d8c Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Sat, 13 Jan 2024 22:55:38 +0900 Subject: [PATCH 02/49] fix: Fix broken integration test of CalendarApiTest due to changed FamilyApi (#96) --- .../com/oing/restapi/CalendarApiTest.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java index 3c772a05..2b61d225 100644 --- a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java @@ -4,6 +4,7 @@ import com.oing.domain.*; import com.oing.dto.request.JoinFamilyRequest; import com.oing.dto.response.DeepLinkResponse; +import com.oing.dto.response.FamilyResponse; import com.oing.service.*; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; @@ -118,18 +119,20 @@ void setUp() { "values ('4', '" + TEST_USER1_ID + "', 'https://storage.com/images/4', 0, 0, '2023-11-02 14:00:00', '2023-11-02 14:00:00', 'post4444', '4');"); // family - String familyId = familyService.createFamily().getId(); - String inviteCode = objectMapper.readValue(mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_USER1_TOKEN)).andExpect(status().isOk()).andReturn().getResponse().getContentAsString(), DeepLinkResponse.class).getLinkId(); - JoinFamilyRequest joinFamilyRequest = new JoinFamilyRequest(inviteCode); - mockMvc.perform(post("/v1/me/join-family") - .header("X-AUTH-TOKEN", TEST_USER1_TOKEN) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(joinFamilyRequest)) - ).andExpect(status().isOk()); + String familyId = objectMapper.readValue( + mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_USER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), FamilyResponse.class + ).familyId(); + String inviteCode = objectMapper.readValue( + mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_USER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class + ).getLinkId(); mockMvc.perform(post("/v1/me/join-family") .header("X-AUTH-TOKEN", TEST_USER2_TOKEN) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(joinFamilyRequest)) + .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) ).andExpect(status().isOk()); From 78e30a2a4577d9a6f14ae623a33f525d0bc4ea54 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Sun, 14 Jan 2024 15:28:00 +0900 Subject: [PATCH 03/49] =?UTF-8?q?[OING-126]=20test:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=EC=88=98=EC=A0=95/=EC=A1=B0=ED=9A=8C/?= =?UTF-8?q?=ED=83=88=ED=87=B4=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [OING-18] feat: 공통 모듈 구축하기 (#5) * chore: add common module * chore: add common module to workflow * feat: add common exception classes * feat: alter DomainException to non abstract class * test: add DomainException unit test * test: add ErrorResponse unit test * chore: fix workflow annotations * [OINT-19] feature: 스프링 시큐리티 설정 (#6) * chore: change root project name * feat: add SpringSecurityConfig * feat: add auth handler filters * feat: add JwtAuthenticationHandler with properties * feat: add WebRequestInterceptor with filters * chore: change root project name * feat: add SpringSecurityConfig * feat: add auth handler filters * feat: add JwtAuthenticationHandler with properties * feat: add WebRequestInterceptor with filters * feat: change response to ErrorResponse * feat: add WebExceptionHandler * style: change line length under max line length * style: change line length under max line length * style: match line convention * [OING-10] chore: code coverage를 위한 Jacoco 설정 (#7) * chore: apply Jacoco for code coverage * chore: exclude specific patterns from Jacoco code coverage * chore: set required formats * fix: Jacoco configuration by moving jacoco afterEvaluate * [OING-23] chore: Jacoco 테스트 커버리지 범위에서 config 모듈 제외 (#8) * [OING-24] chore: Swagger 환경 세팅 (#10) * feat: add SwaggerConfig * feat: add SwaggerConfig environment variable * feat: exclude swagger-related requests from Interceptor path patterns * style: change swagger title * [OING-20] feat: JWT 토큰 관리 컴포넌트 생성 (#9) * feat: add token properties classes * feat: add token related domain classes * feat: add token generator and impl * feat: implement token generator with jwt * feat: add JWT Authenticator * test: add unit test for created classes * [OING-26] chore: CI/CD 파이프라인 워크플로우 개선 (#15) * [OING-27] chore: 배포 파이프라인 이후 슬랙 알림 보내기 * [OING-27] chore: 배포8] chore: PR시 테스트 커버리지 ì측정하기 * [OING-31] fix: SpringDocs (Swagger) 수리하기 (#18) * fix: config fix for swagger * docs: add default url path for swagger * [OING-25] chore: NCP ObjectStorage 세팅 (#16) * chore: add dependency for AWS S3 * chore: add NCP ObjectStorage Config * feat: add File uploader * test: add application.yaml for test * test: add test code for uploadImage * refactor: refact ObjectStorageConfig code * refactor: change FileUploader for presignedUrl * feat: add S3PreSignedUrlProvider * test: add test code for presigned-url * style: delete unused import statement * delete: delete unused util file for test * chore: exclude dto from jacoco test coverage * style: delete redundant code * refactor: refact ObjectStorageProperties file * refactor: refact S3PreSignedUrlProvider code * delete: delete unused file * [OING-21] feat: 기본 oAuth & 멤버 인증 관련 모듈 추가 (#17) * feat: create "POST /v1/auth/social" api * feat: create interface of apple login service * feat: create BaseAuditEntity * feat: add IdentityGenerator and impl * feat: add Member and SocialMember entity * feat: add Member Register logic * feat: add apple provider * feat: report slack on error * feat: only invoke slack on production * chore: add slack webhook conf with local props * feat: refactor yaml properties * docs: add environment example * feat: create "POST /v1/auth/social" api * feat: create interface of apple login service * feat: create BaseAuditEntity * feat: add IdentityGenerator and impl * feat: add Member and SocialMember entity * feat: add Member Register logic * feat: add apple provider * feat: report slack on error * feat: only invoke slack on production * chore: add slack webhook conf with local props * feat: refactor yaml properties * docs: add environment example * test: create test profile * test: add unit test on domain module * test: add object unit test codes * test: add component test codes * fix: override equals hashcode to check array * fix: override tostring * fix: remove setter for entity * fix: not-null on BaseAuditEntity * refactor: refactor codes * test: remove setter test for entity * [OING-46] feat: Member, Post 모듈 추가하기 (#20) * feat: add family, post module * chore: add workflow to check module changes * [OING-32] refactor: S3 PresignedUrl 로직 리팩터링 (#19) * refactor: refact PreSignedUrlResponse * feat: add PreSignedUrlGenerator Interface * fix: fix getPreSignedUrl Test * chore: exclude dto from jacoco test coverage * refactor: delete PreSignedUrlResponse DTO from config * chore: add NCP ev to template file * docs: update environment variables on README.md * chore: fix Qdomains in jacocoTestReport * feat: delete Qdomain * feat: document PreSignedUrlResponse * chore: add dependsOn test in jacocoTestReport * [OING-50] refactor: Config 모듈명 Gateway로 변경 (#21) * [OING-33] feat: DDL 작성 및 flyway 활성화 (#24) * feat: add sql DDL script * feat: enable flyway * fix: change url datatype to TEXT * feat: change emoji to ascii * test: fix s3 test * [OING-54] chore: mysql 환경변수 설정 및 프로덕션 환경설정 (#25) * chore: add mysql password to env * chore: add hikari configurations * chore: disable springdoc in production * chore: add application-dev profile * chore: change ddl-auto to validate * chore: test profile to apply h2 * [OING-57] feat: Member 도메인 entity & repository 구성 (#26) * feat: add entitites for member domain * feat: change BaseAuditEntity to extend BaseEntity * [OING-57] hotfix: Member 도메인 entity & repository 구성 오타 (#27) * [OING-47] feat: Auth 리프레시 API 추가및 경로 변경 (#22) * docs: add more description and example to api docs * feat: add refresh token logic * feat: add refresh token validation logic * style: fixed line style * test: add test code to new dto * [OING-32] refactor: S3 PresignedUrl 로직 리팩터링 (#19) * refactor: refact PreSignedUrlResponse * feat: add PreSignedUrlGenerator Interface * fix: fix getPreSignedUrl Test * chore: exclude dto from jacoco test coverage * refactor: delete PreSignedUrlResponse DTO from config * chore: add NCP ev to template file * docs: update environment variables on README.md * chore: fix Qdomains in jacocoTestReport * feat: delete Qdomain * feat: document PreSignedUrlResponse * chore: add dependsOn test in jacocoTestReport * [OING-50] refactor: Config 모듈명 Gateway로 변경 (#21) * docs: add more description and example to api docs * feat: add refresh token logic * feat: add refresh token validation logic * style: fixed line style * test: add test code to new dto * feat: add new temporary token * feat: add temporary token creation logic * feat: add register flow logic * feat: change api route to /register * refactor: add custom fromString on enum classes --------- Co-authored-by: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Co-authored-by: sckwon770 * [OING-40] feat: Feed부분 API 설계하기 (#23) * feat: add Pagination Response and DTO * test: add dto, response test code * feat: add PostFeedController * fix: change package issue * feat: implement mock logic * test: fix test * test: add test to PostFeedResponse * feat: add Member API * [OING-58] feat: MemberPost 도메인 entity & repository 구성 (#28) * feat: add MemberPost Entity * feat: add MemberPostReaction Entity * feat: add MemberPostComment Entity * feat: add Member module dependency * feat: add fetchType to Entity field * feat: add Repository in post module * chore: delete member dependency in post module * test: add domain test in MemberPost module * feat: add index in post table * fix: fix mappedBy in MemberPost * test: fix test error in MemberTest * fix: fix field in MemberPost domain * [OING-40] feature: API 경로 변경 및 Response DTO 변경 (#33) * feat: change api route and controller name * docs: update api docs * feat: add hasNext to PaginationResponse * [OING-62] feat: 가족 구성원 Profile 조회 API 설계 (#29) * feat: add FamilyMemberProfileResponse DTO * feat: add getFamilyMemberProfile API * test: add FamilyMemberProfileResponseTest * refactor: move getFamilyMemberProfile to member module * refactor: move FamilyMemberProfileResponse to member module * style: delete unused file * feat: add Pagination Response and DTO * refactor: cherry-pick PaginationDTO --------- Co-authored-by: ChuYong * [Suggestion] chore: PR 작성 자동화 (#32) * chore: Add the workflos to automate the pointless writing pr template works * chore: Add last empty line into workflow, pr.yaml * chore: Add base-branch-regex * fix: Fix base-branch-regex to constraint branch condition * [OING-56] feat: Family 모듈 구성 (#31) * [OING-41] feat: 금일 피드 업로드 여부 조회 API 추가 (#30) * feat: Add the mock api - getIsTodayFeedUploadedByUserId * fix: Fix wrong parameter annotation on getIsTodayFeedUploadedByUserId * refactor: Refactor fetchDailyFeeds api whose scope is ME according to code review * refactor: Refactor make mock api to response variously * feat: Add AuthenticationHolder * chore: Add the comment using TokenAuthenticationHolder * chore: Exclude QueryDSL Qdomains from git * chore: Organize module directory structure * fix: Fix compile error at MemberController * chore: Exclude .env file from git * [OING-66] feat: PresignedUrl 요청 API 구현 (#35) * feat: add S3presignedUrl API for image upload * refactor: add RequiredArgsConstructor in PostController * style: modify test code with error * refactor: refact PresignedUrl logic * fix: fix requestPresignedUrl http method * style: rename requestPresignedUrl uri * [OING-63] feat: 1차 와이어프레임 API 설계 (#34) * feat: add family api spec * feat: add member api sepc * feat: add post create logic * feat: add calendar query api * feat: add post query api * feat: add temporary token generate api * feat: change api url * feat: add reaction deletion api * hotfix: remove unused import * [OING-73] feat: family, post api mock 구현 (#37) * feat: mock family api * feat: mock post api * feat: add sort parameter * docs: add description * hotfix: add missing parameter * [OING-71] feat: 회원정보 수정 API 모킹 (#38) * feat: add updateMember mock api * feat: add updateMember mock api * [OING-67] feat: 피드 업로드 API 모킹 (#36) * feat: add V2__modify_Post DDL script * feat: modify column in Post domain * feat: add createPost mock API * test: fix failed test due to added column * refactor: refact S3 presignedUrl Api * refactor: delete unused memberId param in requestPresignedUrl * refactor: change ResponseEntity in requestPresignedUrl * refactor: refact Postmodule code * refactor: refact S3PreSignedUrlProvider * test: fix S3PreSignedUrlProviderTest code * style: change ResponseEntity to DTO * feat: correct merge conflicts * feat: add validation for CreatePostRequest * refact: add validateContent method --------- Co-authored-by: 송영민 (YeongMin Song) * [OING-68] feat: Post Reaction API 모킹 (#40) * feat: add PostReaction mock API * feat: change PostReaction DTO name * feat: change reactToPost API name * [OING-83] 회원 탈퇴 API 모킹 (#41) * feat: add deleteMember mock api * feat: delete DeleteMemberRequest dto * feat: change deleteMember mock api logic * style: delete /v1/members uri in url-whitelists * [OING-88] feat: Emoji Enum 타입으로 변경 (#43) * feat: add Emoji Enum Class * fix: fix emoji logic * test: fix test due to EnumType * [OING-84] feat: Widget API 구현 (#42) * feat: Add OptimizedImageUrlProvider component at gateway * feat: Add the code of service and repository for calendar controller * feat: Move Calendar controller to gateway modules and Impl getMonthlyCalendar request * refactor: Refactor MemberPostRepository to make method name simply rather than giving meaning in detail * feat: Impl getWeeklyCalendar requst and Substract the dupicated code lines from calendar requests * fix: Remove the code to substract 1 day from end date at calenar request, considering between operator * chore: Fix head branch regex for pr template automation workflow * feat: Add api spec for the single recent family widget * refactor: Exclude family concept from MemberPostService, MemberPostRepository * feat: Impl getSingleRecentFamilyPostWidget request * refactor: Rename memberId to myId * refactor: Change the type of week paramter of getWeeklyCalendar request to Long * refactor: Make the date parameters of CalendarCotroller required true * chore: Add test coverage excluding pattern **.DTO* * fix: Add cdn value at test application yml * refactor: Refactor for loop code to map post to calendar to functional code * refactor: Break the line over 120 characters * refactor: Refactor the complex JPQL code of MemberPostRepository to QueryDSL * refactor: Change the type of paramters of MemberPost code * refactor: Remove implementation class usage, not interface of OptimizedImageUrl component * [OING-70] feat: Calendar API 구현 (#39) * feat: Add OptimizedImageUrlProvider component at gateway * feat: Add the code of service and repository for calendar controller * feat: Move Calendar controller to gateway modules and Impl getMonthlyCalendar request * refactor: Refactor MemberPostRepository to make method name simply rather than giving meaning in detail * feat: Impl getWeeklyCalendar requst and Substract the dupicated code lines from calendar requests * fix: Remove the code to substract 1 day from end date at calenar request, considering between operator * chore: Fix head branch regex for pr template automation workflow * refactor: Exclude family concept from MemberPostService, MemberPostRepository * refactor: Rename memberId to myId * refactor: Change the type of week paramter of getWeeklyCalendar request to Long * refactor: Make the date parameters of CalendarCotroller required true * chore: Add test coverage excluding pattern **.DTO* * fix: Add cdn value at test application yml * refactor: Refactor for loop code to map post to calendar to functional code * refactor: Break the line over 120 characters * refactor: Refactor the complex JPQL code of MemberPostRepository to QueryDSL * refactor: Change the type of paramters of MemberPost code * refactor: Remove implementation class usage, not interface of OptimizedImageUrl component --------- Co-authored-by: 송영민 (YeongMin Song) * [OING-90] hotfix: MemberService 및 WidgetController에서의 컴파일 에러 (#44) * hotfix: update swagger header * hotfix: add token security * [OING-75] feat: Post 작성 API 구현 (#45) * [OING-79] feat: 멤버 닉네임/프로필 이미지 수정 API 구현 (#47) * feat: add updateMember API * feat: add requestPresignedUrl for profileImg API * feat: add transactional annotation * style: remove getMember method * refactor: split existing logic into two separate APIs * feat: add deleteMemberProfileImage method * style: update imageUrl RequestDTO example * style: change method name for delete ObjectStorage image * refactor: add validateName logic * feat: change queryString to requestBody * feat: move validateName method * feat: add Async to deleteImage logic * style: add newline * [OING-77] feat: Post 조회 관련 API 구현 (#50) * feat: implement GET v1/posts * feat: implement search posts * style: remove unused import * refactor: change to query-dsl style * [OING-81] feat: v1/families 관련 API 구현 (#49) * feat: implement POST /v1/families * feat: implement GET /v1/families/invitation-link * feat; change method name * feat: change link to constant * feat: change to new exception class * [OING-80] feat: 멤버 정보 조회 관련 및 가족 그룹 생성일 조회 API 구현 (#46) * feat: add getFamilyMemberProfile API * feat: add getMember API * feat: add familyCreatedAt in FamiyMemberProfilesResponse DTO * refactor: refact getFamilyMemberProfiles method * style: code cleanup * feat: delete family dependency in member * feat: seperate getFamilyCreatedAt API * style: change default value in getFamilyMemberProfiles * style: delete unused annotation * refactor: refact findFamilyCreatedAt method * style: rename method for getFamilyMembersProfiles * refactor: refact createFamilyMemberProfiles logic * fix: remove sorting for findFamilyMembersProfiles * style: change getFamilyCreatedAt ResponseDto * refact: createFamilyMemberProfiles method logic * [OING-97] CalendarApi와 관련된 에러와 잘못된 설정 해결 (#54) * fix: Add alias name at memberPost.count() to fix runtime QueryDSL query error * refactor: Make param require false at CalendarApi * chore: Add Swagger params description to explain type of CalendarApi * feat: commenting out deleteMemberProfileImage method (#55) * [OING-97] PR54로 구현한 티켓의 런타입 에러 해결을 위한 핫픽스... (#56) * fix: Add alias name at memberPost.count() to fix runtime QueryDSL query error * refactor: Make param require false at CalendarApi * chore: Add Swagger params description to explain type of CalendarApi * fix: Change MemberPostCountDTO type to Class to use the QueryDSL projections field instead of projections bean that occur runtime error * fix: Add NoArgsContructor at MemberPostCountDTO to fix QueryDSL projections QBean runtime error (#57) * [OING-76] feat: Post에 대한 반응 생성/삭제 구현 (#48) * feat: add validate logic for createPostReaction * feat: add createPostReaction API * feat: add deletePostReaction API * refactor: refact post reaction logic in controller * refactor: refact findReaction logic in service * fix: change FetchType Eager to Lazy * [OING-94] fix: createPost 코드 개선 (#51) * refactor: refact createPost code * refactor: delete unused log * feat: add DuplicatePostUploadException * style: add comments for upload time validation * refactor: convert time comparison logic to use LocalDateTime * fix: fix postDate for Asia/Seoul * refactor: refact PostResponse * style: detail InvalidUploadTime Exception * fix: remove extractLocalDate method * style: correct image request dto example * fix: fix validateUploadTime logic to ZonedDateTime * [OING-95] feat: 카카오 인증(로그인) 기능 구현 (#52) * feat: add kakao provider * feat: implement kakao * [OING-92] feat: 회원 탈퇴 API 구현 (#58) * feat: add BaseAuditEntityWithDelete * feat: add member delete basic logic * feat: add findAllSocialMemberByMember method * fix: fix updateDeletedAt() * refact: delete updateDeletedAt method argument * feat: add memberId PathVariable * refactor: refact deleteMember logic * feat: add memberId PathVariable in Put method * fix: fix social login provider test * [OING-100] feat: 내 정보 조회 API & MemberResponse 누락필드 추가 (#59) * feat: move member response to function * feat: add me api * hotfix: fix page count issue (#60) * [OING-102] hotfix: Family 초대 링크 기발급 경우 기존 링크 반환 (#61) * feat: change emoji type name * feat: implement post reaction api * [OING-107] 임시 토큰 통한 회원가입 불가 이슈 해결 (#62) * hotfix: add missing transactional * [OING-108] feature: 응답값 없는 Operation의 기본 응답값 추가 (#63) * feat: add memberdevice domain * feat: add device api * fix: change missing name * [OING-109] feat: Member, MemberPost 엔티티에 imageKey 필드 추가 (#64) * Revert "[OING-109] feat: Member, MemberPost 엔티티에 imageKey 필드 추가 (#64)" This reverts commit 926f3819cbf5a1166ac635b5a46a2ef4b19294aa. * Revert "Revert "[OING-109] feat: Member, MemberPost 엔티티에 imageKey 필드 추가 (#64)"" This reverts commit 0bb818d12edbaaee9cf54f6323c29922d305b74e. * [OING-111] 새로운 형태의 Object storage url으로 인해 썸네일 url이 작동하지 못한 오류 해결 #65 * feat: add not found handler * [OING-112] feat: 딥링크 API & 딥링크 가족 가입 API 구현 (#66) * feat: add family join api * feat: add deep link api * feat: add join family feature * Revert "feat: add not found handler" This reverts commit 6bcdaa14012020e8f14ad6a61a373d3248494d06. * feat: handle 404 * [OING-113] refactor: 게시물 반응 전체 조회 응답 형식 수정 (#67) * [OING-115] feat: 프로덕션용 설정 및 앱 키 기능 추가 (#68) * feat: fix validation * feat: config production-ready setups * feat: add additional logging * feat: add app version feature * feat: changed log targets * feat: change method not allowed exception * [OING-120] hotfix: 내 가족만 게시물 조회 가능하게 & 앱 키 스웨거 추가 (#69) * feat: add version filter toggle * feat: add app version key in swagger * feat: only query my family * [OING-118] hotfix: 기회원가입자 재회원가입 방지 (#70) * test: jwt 헤더 테스트 깨지는거 해결 * feat: prevent register if already member exists * [OING-119] feat: Post content 공백 존재 검증 로직 추가 (#71) * feat: add spacing validate logic in post * style: change throw InvalidParameterException * test: jwt 헤더 테스트 깨지는거 해결 --------- Co-authored-by: ChuYong * [OING-122] hotfix: post 등록 시, 내용 길이 검증 수정 (#73) * [OING-121] refactor: 1차 MVP 코드 전체 정리 (#72) * style: clean up code * refactor: common module package cleanup * refactor: family module package cleanup * refactor: member module package cleanup * refactor: post module package cleanup * style: clean up code finally * feat: remove asyncconfig * refactor: refact post module class name * [OING-122] hotfix: post 등록 시, 내용 길이 검증 수정 (#73) * style: clean up code * refactor: common module package cleanup * refactor: family module package cleanup * refactor: member module package cleanup * refactor: post module package cleanup * style: clean up code finally * feat: remove asyncconfig * refactor: refact post module class name --------- Co-authored-by: jisu Co-authored-by: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> * [OING-124] hotfix: 리프레시 api 작동 안함 해결 (#75) * feat: add links to whitelisted url * fix: change image key extraction logic (#74) * [OING-125] hotfix: 유효하지 않은 토큰에 대해 401 예외 반환하는 핸들러 추가 (#76) * hotfix: add handleAuthenticationFailedException * style: change log message * [OING-125] hotfix: PR76 핫픽스 (#77) * hotfix: add handleAuthenticationFailedException * style: change log message * fix: change 401 to HttpStatus UNAUTHORIZED * [OING-125] hotfix: PR76 핫픽스 2 (#78) * [OING-127] feat: 구글 oAuth 추가 (#79) * [OING-128] refactor: 가족 멤버 프로필 조회 시, 탈퇴한 회원은 응답에서 제외되도록 수정 (#80) * feat: add DeletedAtIsNull to findFamilyMembers * test: fix SocialLoginProviderTest * style: delete unused lines * [OING-129] Widget용 이미지 압축 기능 추가 (#82) * [OING-130] feat: postDate 컬럼 삭제 및 가족이 없는 멤버에 대해 가족 프로필 조회 시, 예외 반환 로직 추가 (#83) * feat: delete postDate column in post * style: change method name * feat: add delete postdate column sql * fix: fix existsByMemberIdAndCreatedAt method * test: fix memberPost test * feat: add FamilyNotFoundException * [OING-129] OING-129 의 컴파일 에러 핫픽스 (#84) * feat: Add getKBImageUrlGenerator at OptimizedImageUrlGenerator and Append image optimizing code at getSingleRecentFamilyPostWidget at WidgetController * fix: Hotfix compile error * [OING-129] hotfix: OING-129 PR의 핫픽스가 머지되는 중에 꼬인 에러 해결 (#85) * feat: Add getKBImageUrlGenerator at OptimizedImageUrlGenerator and Append image optimizing code at getSingleRecentFamilyPostWidget at WidgetController * fix: Hotfix compile error * fix: Fix wrongly twisted merged getKBImageUrlGenerator code * chore: update swarm service name * [OING-129] OING-129 PR에서 이미지 최적화 쿼리를 변경 (#86) * feat: Add getKBImageUrlGenerator at OptimizedImageUrlGenerator and Append image optimizing code at getSingleRecentFamilyPostWidget at WidgetController * fix: Hotfix compile error * fix: Fix wrongly twisted merged getKBImageUrlGenerator code * refactor: Change KB_IMAGE_OPTIMIZER_QUERY_STRING * chore: update prod swarm service name * [OING-131] 위젯 응답 객체에 게시자 이름 추가 (#87) * test: add MemberControllerTest for change profile info * test: add MemberControllerTest for get Member Profile info * test: add MemberControllerTest for memberDelete * test: add MemberControllerTest for exception and presignedUrl * [OING-129] 이미지 최적화 빈의 NullPointerException 방지 (#88) * [OING-104] test: 월간 캘린더 API 통합테스트 추가 (#81) * feat: Add test code for getMonthlyCalendar of CalendarController * fix: Fix calendar query that cant group the posts daily and Fix test code according to changed calendar query * refactor: Change the integration test, CalendarControllerTest to CalendarApiTest * fix: Add fake token expiration value in applicaiton-test.yaml * fix: Remove post_date column from meber_post insertion sql at CalenarApiTest * test: add additional test for nickname validate * test: add additional test for nickname validate * fix: fix getThumbnailUrlGenerator typo * hotfix: fix getThumbnailUrlGenerator typo (#90) * [OING-135] hotfix: 닉네임 길이 제한 조건 수정 (#92) * fix: fix validateMemberName condition * feat: add/fix @Valid for DTO * feat: add @NotBlank to CreateNewUserDto * test: fix Nickname validate test * test: add MemberControllerTest for change profile info * test: add MemberControllerTest for get Member Profile info * test: add MemberControllerTest for memberDelete * test: add additional test for nickname validate * test: add additional test for nickname validate * fix: fix getThumbnailUrlGenerator typo * hotfix: fix getThumbnailUrlGenerator typo (#90) * [OING-135] hotfix: 닉네임 길이 제한 조건 수정 (#92) * fix: fix validateMemberName condition * feat: add/fix @Valid for DTO * feat: add @NotBlank to CreateNewUserDto * [OING-135] hotfix: 닉네임 길이 제한 조건 수정 (#92) * fix: fix validateMemberName condition * feat: add/fix @Valid for DTO * feat: add @NotBlank to CreateNewUserDto * test: fix Nickname validate test * test: remove spy --------- Co-authored-by: 송영민 (YeongMin Song) Co-authored-by: sckwon770 --- .../filter/JwtAuthenticationHandler.java | 6 + .../oing/controller/DeepLinkController.java | 5 +- .../repository/MemberPostRepositoryImpl.java | 1 - gateway/src/main/resources/application.yaml | 2 +- .../resources/template-application-local.yaml | 2 + .../src/main/java/com/oing/domain/Member.java | 2 +- .../oing/controller/MemberControllerTest.java | 199 ++++++++++++++++++ 7 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 member/src/test/java/com/oing/controller/MemberControllerTest.java diff --git a/gateway/src/main/java/com/oing/config/filter/JwtAuthenticationHandler.java b/gateway/src/main/java/com/oing/config/filter/JwtAuthenticationHandler.java index 2177b0f9..577cad9c 100644 --- a/gateway/src/main/java/com/oing/config/filter/JwtAuthenticationHandler.java +++ b/gateway/src/main/java/com/oing/config/filter/JwtAuthenticationHandler.java @@ -50,4 +50,10 @@ protected void doFilterInternal( } filterChain.doFilter(request, response); } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getServletPath(); + return webProperties.isWhitelisted(path); + } } diff --git a/gateway/src/main/java/com/oing/controller/DeepLinkController.java b/gateway/src/main/java/com/oing/controller/DeepLinkController.java index 392a9cb8..d4e6d43d 100644 --- a/gateway/src/main/java/com/oing/controller/DeepLinkController.java +++ b/gateway/src/main/java/com/oing/controller/DeepLinkController.java @@ -10,11 +10,11 @@ import com.oing.restapi.DeepLinkApi; import com.oing.service.DeepLinkDetailService; import com.oing.service.DeepLinkService; -import com.oing.service.MemberBridge; -import com.oing.util.AuthenticationHolder; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; +import com.oing.service.MemberBridge; +import com.oing.util.AuthenticationHolder; import java.util.Objects; /** @@ -27,6 +27,7 @@ @Controller public class DeepLinkController implements DeepLinkApi { public static String FAMILY_LINK_PREFIX = "https://no5ing.kr/o/"; + private final DeepLinkService deepLinkService; private final AuthenticationHolder authenticationHolder; private final MemberBridge memberBridge; diff --git a/gateway/src/main/java/com/oing/repository/MemberPostRepositoryImpl.java b/gateway/src/main/java/com/oing/repository/MemberPostRepositoryImpl.java index 7e5a537c..00e2549a 100644 --- a/gateway/src/main/java/com/oing/repository/MemberPostRepositoryImpl.java +++ b/gateway/src/main/java/com/oing/repository/MemberPostRepositoryImpl.java @@ -55,7 +55,6 @@ public List findPostDailyCalendarDTOs(List m } - @Override public QueryResults searchPosts(int page, int size, LocalDate date, String memberId, String requesterMemberId, String familyId, boolean asc) { return queryFactory diff --git a/gateway/src/main/resources/application.yaml b/gateway/src/main/resources/application.yaml index ab2a3ce1..5b9d3353 100644 --- a/gateway/src/main/resources/application.yaml +++ b/gateway/src/main/resources/application.yaml @@ -42,7 +42,7 @@ app: - /v3/api-docs/** - /v3/api-docs - /error - - /v1/links/* + - /v1/links/** version-check-whitelists: - /actuator/** - /swagger-ui.html diff --git a/gateway/src/main/resources/template-application-local.yaml b/gateway/src/main/resources/template-application-local.yaml index 28e48c61..b2e98324 100644 --- a/gateway/src/main/resources/template-application-local.yaml +++ b/gateway/src/main/resources/template-application-local.yaml @@ -13,6 +13,8 @@ spring: show_sql: true format_sql: true app: +<<<<<<< HEAD +======= oauth: google-client-id: ${GOOGLE_CLIENT_ID} web: diff --git a/member/src/main/java/com/oing/domain/Member.java b/member/src/main/java/com/oing/domain/Member.java index 9d412e1f..5163404d 100644 --- a/member/src/main/java/com/oing/domain/Member.java +++ b/member/src/main/java/com/oing/domain/Member.java @@ -50,7 +50,7 @@ public void updateName(String name) { public void deleteMemberInfo() { super.updateDeletedAt(); - this.name = "DeletedUser"; + this.name = "DeletedMember"; this.profileImgUrl = null; } diff --git a/member/src/test/java/com/oing/controller/MemberControllerTest.java b/member/src/test/java/com/oing/controller/MemberControllerTest.java new file mode 100644 index 00000000..669d1f99 --- /dev/null +++ b/member/src/test/java/com/oing/controller/MemberControllerTest.java @@ -0,0 +1,199 @@ +package com.oing.controller; + +import com.oing.domain.Member; +import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.request.UpdateMemberNameRequest; +import com.oing.dto.request.UpdateMemberProfileImageUrlRequest; +import com.oing.dto.response.FamilyMemberProfileResponse; +import com.oing.dto.response.MemberResponse; +import com.oing.dto.response.PaginationResponse; +import com.oing.dto.response.PreSignedUrlResponse; +import com.oing.exception.AuthorizationFailedException; +import com.oing.service.MemberService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.PreSignedUrlGenerator; +import jakarta.transaction.Transactional; +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.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.test.context.ActiveProfiles; + +import java.security.InvalidParameterException; +import java.time.LocalDate; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@Transactional +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +public class MemberControllerTest { + + @InjectMocks + private MemberController memberController; + @Mock + private MemberService memberService; + @Mock + private AuthenticationHolder authenticationHolder; + @Mock + private PreSignedUrlGenerator preSignedUrlGenerator; + + @Test + void 멤버_프로필_조회_테스트() { + // given + Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), + "testMember1", "http://test.com/test-profile.jpg", null); + when(memberService.findMemberById(any())).thenReturn(member); + + // when + MemberResponse response = memberController.getMember(member.getId()); + + // then + assertEquals(member.getId(), response.memberId()); + assertEquals(member.getName(), response.name()); + assertEquals(member.getProfileImgUrl(), response.imageUrl()); + assertEquals(member.getFamilyId(), response.familyId()); + assertEquals(member.getDayOfBirth(), response.dayOfBirth()); + } + + @Test + void 가족_멤버_프로필_조회_테스트() { + // given + Member member1 = new Member("1", "1", LocalDate.of(2000, 7, 8), + "testMember1", "http://test.com/test-profile.jpg", null); + Member member2 = new Member("2", "1", LocalDate.of(2003, 7, 26), + "testMember2", null, null); + String familyId = "1"; + when(authenticationHolder.getUserId()).thenReturn("1"); + when(memberService.findFamilyIdByMemberId(anyString())).thenReturn(familyId); + Page profilePage = new PageImpl<>(Arrays.asList( + new FamilyMemberProfileResponse(member1.getId(), member1.getName(), member1.getProfileImgUrl()), + new FamilyMemberProfileResponse(member2.getId(), member2.getName(), member2.getProfileImgUrl()) + )); + when(memberService.findFamilyMembersProfilesByFamilyId(familyId, 1, 5)) + .thenReturn(profilePage); + + // when + PaginationResponse response = memberController. + getFamilyMembersProfiles(1, 5); + + // then + assertFalse(response.hasNext()); + assertEquals(2, response.results().size()); + } + + @Test + void 멤버_닉네임_수정_테스트() { + // given + String newName = "newName"; + Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), + "testMember1", "http://test.com/test-profile.jpg", null); + when(memberService.findMemberById(any())).thenReturn(member); + when(authenticationHolder.getUserId()).thenReturn("1"); + + // when + UpdateMemberNameRequest request = new UpdateMemberNameRequest(newName); + memberController.updateMemberName(member.getId(), request); + + // then + assertEquals(newName, member.getName()); + } + + @Test + void 아홉_자_초과_형식의_닉네임_수정_예외_테스트() { + // given + String newName = "wrong-length-nam"; + Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), + "testMember1", "http://test.com/test-profile.jpg", null); + when(memberService.findMemberById(any())).thenReturn(member); + when(authenticationHolder.getUserId()).thenReturn("1"); + + // when + UpdateMemberNameRequest request = new UpdateMemberNameRequest(newName); + + // then + assertThrows(InvalidParameterException.class, () -> memberController.updateMemberName(member.getId(), request)); + } + + @Test + void 한_자_미만_형식의_닉네임_수정_예외_테스트() { + // given + String newName = ""; + Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), + "testMember1", "http://test.com/test-profile.jpg", null); + when(memberService.findMemberById(any())).thenReturn(member); + when(authenticationHolder.getUserId()).thenReturn("1"); + + // when + UpdateMemberNameRequest request = new UpdateMemberNameRequest(newName); + + // then + assertThrows(InvalidParameterException.class, () -> memberController.updateMemberName(member.getId(), request)); + } + + @Test + void 멤버_프로필이미지_업로드_URL_요청_테스트() { + // given + String newProfileImage = "profile.jpg"; + + // when + PreSignedUrlRequest request = new PreSignedUrlRequest(newProfileImage); + PreSignedUrlResponse dummyResponse = new PreSignedUrlResponse("https://test.com/presigend-request-url"); + when(preSignedUrlGenerator.getProfileImagePreSignedUrl(any())).thenReturn(dummyResponse); + PreSignedUrlResponse response = memberController.requestPresignedUrl(request); + + // then + assertNotNull(response.url()); + } + + @Test + void 멤버_프로필이미지_수정_테스트() { + // given + String newProfileImageUrl = "http://test.com/profile.jpg"; + Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), + "testMember1", "http://test.com/test-profile.jpg", null); + when(memberService.findMemberById(any())).thenReturn(member); + when(authenticationHolder.getUserId()).thenReturn("1"); + when(preSignedUrlGenerator.extractImageKey(any())).thenReturn("/profile.jpg"); + + // when + UpdateMemberProfileImageUrlRequest request = new UpdateMemberProfileImageUrlRequest(newProfileImageUrl); + memberController.updateMemberProfileImageUrl(member.getId(), request); + + // then + assertEquals(newProfileImageUrl, member.getProfileImgUrl()); + } + + @Test + void 멤버_탈퇴_테스트() { + // given + Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), + "testMember1", "http://test.com/test-profile.jpg", null); + when(memberService.findMemberById(any())).thenReturn(member); + when(authenticationHolder.getUserId()).thenReturn("1"); + + // when + memberController.deleteMember(member.getId()); + + // then + assertEquals("DeletedMember", member.getName()); + assertNull(member.getProfileImgUrl()); + } + + @Test + void 잘못된_요청의_멤버_탈퇴_예외_테스트() { + // given + Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), + "testMember1", "http://test.com/test-profile.jpg", null); + when(authenticationHolder.getUserId()).thenReturn("2"); + + // then + assertThrows(AuthorizationFailedException.class, () -> memberController.deleteMember(member.getId())); + } +} From 8eb2b666cc5c8f53e22b158530be6b418e096292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Sun, 14 Jan 2024 18:33:16 +0900 Subject: [PATCH 04/49] fix: update filter condition --- .../com/oing/config/filter/JwtAuthenticationHandler.java | 6 ------ gateway/src/main/resources/application.yaml | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/gateway/src/main/java/com/oing/config/filter/JwtAuthenticationHandler.java b/gateway/src/main/java/com/oing/config/filter/JwtAuthenticationHandler.java index 577cad9c..2177b0f9 100644 --- a/gateway/src/main/java/com/oing/config/filter/JwtAuthenticationHandler.java +++ b/gateway/src/main/java/com/oing/config/filter/JwtAuthenticationHandler.java @@ -50,10 +50,4 @@ protected void doFilterInternal( } filterChain.doFilter(request, response); } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { - String path = request.getServletPath(); - return webProperties.isWhitelisted(path); - } } diff --git a/gateway/src/main/resources/application.yaml b/gateway/src/main/resources/application.yaml index 5b9d3353..ab2a3ce1 100644 --- a/gateway/src/main/resources/application.yaml +++ b/gateway/src/main/resources/application.yaml @@ -42,7 +42,7 @@ app: - /v3/api-docs/** - /v3/api-docs - /error - - /v1/links/** + - /v1/links/* version-check-whitelists: - /actuator/** - /swagger-ui.html From b9f526fa98eee375ab0c5471cae1c0411ae06d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC=20=28YeongMin=20Song=29?= Date: Mon, 15 Jan 2024 10:03:41 +0900 Subject: [PATCH 05/49] =?UTF-8?q?[OING-142]=20feat:=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=84=A4=EA=B3=84=20&=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20&=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: fix generic response return issue * feat: structure api document * feat: update dto * feat: implement crud * feat: add some validations * feat: add family validation * test: add MemberPostCommentControllerTest * test: add MemberPostCommentControllerTest * test: add MemberPostCommentApiTest * feat: add count to member post * fix: update exception type --- .../com/oing/dto/response/ArrayResponse.java | 2 - .../oing/dto/response/PaginationResponse.java | 10 - .../java/com/oing/exception/ErrorCode.java | 4 + .../java/com/oing/service/MemberBridge.java | 9 + .../com/oing/service/MemberBridgeImpl.java | 16 + .../src/main/resources/application-test.yaml | 4 + .../restapi/MemberPostCommentApiTest.java | 196 +++++++++++++ .../MemberPostCommentController.java | 127 ++++++++ .../main/java/com/oing/domain/MemberPost.java | 11 + .../com/oing/domain/MemberPostComment.java | 4 + .../dto/request/CreatePostCommentRequest.java | 21 ++ .../dto/request/UpdatePostCommentRequest.java | 20 ++ .../dto/response/PostCommentResponse.java | 35 +++ .../MemberPostCommentNotFoundException.java | 7 + .../MemberPostCommentRepository.java | 2 +- .../MemberPostCommentRepositoryCustom.java | 8 + ...MemberPostCommentRepositoryCustomImpl.java | 27 ++ .../oing/restapi/MemberPostCommentApi.java | 83 ++++++ .../service/MemberPostCommentService.java | 71 +++++ .../com/oing/service/MemberPostService.java | 2 +- .../MemberPostCommentControllerTest.java | 276 ++++++++++++++++++ .../service/MemberPostCommentServiceTest.java | 161 ++++++++++ 22 files changed, 1082 insertions(+), 14 deletions(-) create mode 100644 gateway/src/test/java/com/oing/restapi/MemberPostCommentApiTest.java create mode 100644 post/src/main/java/com/oing/controller/MemberPostCommentController.java create mode 100644 post/src/main/java/com/oing/dto/request/CreatePostCommentRequest.java create mode 100644 post/src/main/java/com/oing/dto/request/UpdatePostCommentRequest.java create mode 100644 post/src/main/java/com/oing/dto/response/PostCommentResponse.java create mode 100644 post/src/main/java/com/oing/exception/MemberPostCommentNotFoundException.java create mode 100644 post/src/main/java/com/oing/repository/MemberPostCommentRepositoryCustom.java create mode 100644 post/src/main/java/com/oing/repository/MemberPostCommentRepositoryCustomImpl.java create mode 100644 post/src/main/java/com/oing/restapi/MemberPostCommentApi.java create mode 100644 post/src/main/java/com/oing/service/MemberPostCommentService.java create mode 100644 post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java create mode 100644 post/src/test/java/com/oing/service/MemberPostCommentServiceTest.java diff --git a/common/src/main/java/com/oing/dto/response/ArrayResponse.java b/common/src/main/java/com/oing/dto/response/ArrayResponse.java index 442d00d4..18e4fc40 100644 --- a/common/src/main/java/com/oing/dto/response/ArrayResponse.java +++ b/common/src/main/java/com/oing/dto/response/ArrayResponse.java @@ -10,9 +10,7 @@ * Date: 2023/12/05 * Time: 12:30 PM */ -@Schema(description = "배열(복수) 응답") public record ArrayResponse( - @Schema(description = "실제 데이터 컬렉션", example = "[\"data\"]") Collection results ) { public static ArrayResponse of(Collection results) { diff --git a/common/src/main/java/com/oing/dto/response/PaginationResponse.java b/common/src/main/java/com/oing/dto/response/PaginationResponse.java index 4d7ed9fe..9755983e 100644 --- a/common/src/main/java/com/oing/dto/response/PaginationResponse.java +++ b/common/src/main/java/com/oing/dto/response/PaginationResponse.java @@ -12,21 +12,11 @@ * Date: 2023/12/05 * Time: 12:30 PM */ -@Schema(description = "페이지네이션 응답") public record PaginationResponse( - @Schema(description = "현재 페이지", example = "1") int currentPage, - - @Schema(description = "전체 페이지 수", example = "30") int totalPage, - - @Schema(description = "페이지당 데이터 수", example = "10") int itemPerPage, - - @Schema(description = "더 데이터가 있는지", example = "true") boolean hasNext, - - @Schema(description = "실제 데이터 컬렉션", example = "[\"data\"]") Collection results ) { public static PaginationResponse of(PaginationDTO dto, int currentPage, int itemPerPage) { diff --git a/common/src/main/java/com/oing/exception/ErrorCode.java b/common/src/main/java/com/oing/exception/ErrorCode.java index 13ce5618..22914b7e 100644 --- a/common/src/main/java/com/oing/exception/ErrorCode.java +++ b/common/src/main/java/com/oing/exception/ErrorCode.java @@ -39,6 +39,10 @@ public enum ErrorCode { */ EMOJI_ALREADY_EXISTS("EM0001", "Emoji already exists"), EMOJI_NOT_FOUND("EM0002", "Emoji not found"), + /** + * MemberComment Related Errors + */ + POST_COMMENT_NOT_FOUND("CM0001", "Comment not found"), /** * Family Related Errors */ diff --git a/common/src/main/java/com/oing/service/MemberBridge.java b/common/src/main/java/com/oing/service/MemberBridge.java index bdbaf1cf..3d38f9dc 100644 --- a/common/src/main/java/com/oing/service/MemberBridge.java +++ b/common/src/main/java/com/oing/service/MemberBridge.java @@ -15,4 +15,13 @@ public interface MemberBridge { * @return family id */ String getFamilyIdByMemberId(String memberId); + + /** + * 같은 가족에 속해있는지 확인합니다 + * @param memberIdFirst 첫 번쨰 사용자 아이디 + * @param memberIdSecond 두 번째 사용자 아이디 + * @return 가족 같은지 여부 (한쪽이라도 null이면 false) + * @throws com.oing.exception.MemberNotFoundException 사용자가 존재하지 않을 경우 + */ + boolean isInSameFamily(String memberIdFirst, String memberIdSecond); } diff --git a/gateway/src/main/java/com/oing/service/MemberBridgeImpl.java b/gateway/src/main/java/com/oing/service/MemberBridgeImpl.java index 14564685..3140c968 100644 --- a/gateway/src/main/java/com/oing/service/MemberBridgeImpl.java +++ b/gateway/src/main/java/com/oing/service/MemberBridgeImpl.java @@ -4,6 +4,7 @@ import com.oing.exception.FamilyNotFoundException; import com.oing.exception.MemberNotFoundException; import com.oing.repository.MemberRepository; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -27,4 +28,19 @@ public String getFamilyIdByMemberId(String memberId) { if (familyId == null) throw new FamilyNotFoundException(); return familyId; } + + @Transactional + @Override + public boolean isInSameFamily(String memberIdFirst, String memberIdSecond) { + Member firstMember = memberRepository + .findById(memberIdFirst) + .orElseThrow(MemberNotFoundException::new); + + Member secondMember = memberRepository + .findById(memberIdSecond) + .orElseThrow(MemberNotFoundException::new); + + return firstMember.hasFamily() && secondMember.hasFamily() && + firstMember.getFamilyId().equals(secondMember.getFamilyId()); + } } diff --git a/gateway/src/main/resources/application-test.yaml b/gateway/src/main/resources/application-test.yaml index 92a7e294..6318ef8a 100644 --- a/gateway/src/main/resources/application-test.yaml +++ b/gateway/src/main/resources/application-test.yaml @@ -38,6 +38,10 @@ app: userid-header: X-USER-ID appkey-header: X-APP-KEY +logging: + level: + com.oing: DEBUG + cloud: ncp: region: test diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostCommentApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostCommentApiTest.java new file mode 100644 index 00000000..f690db0c --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MemberPostCommentApiTest.java @@ -0,0 +1,196 @@ +package com.oing.restapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.domain.*; +import com.oing.dto.request.CreatePostCommentRequest; +import com.oing.dto.request.UpdatePostCommentRequest; +import com.oing.repository.MemberPostCommentRepository; +import com.oing.repository.MemberPostRepository; +import com.oing.repository.MemberRepository; +import com.oing.service.MemberService; +import com.oing.service.TokenGenerator; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class MemberPostCommentApiTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private TokenGenerator tokenGenerator; + @Autowired + private ObjectMapper objectMapper; + + + private String TEST_MEMBER_ID = "01HGW2N7EHJVJ4CJ999RRS2E97"; + private String TEST_POST_ID = "01HGW2N7EHJVJ4CJ999RRS2A97"; + private String TEST_MEMBER_TOKEN; + + @Autowired + private MemberRepository memberRepository; + @Autowired + private MemberPostRepository memberPostRepository; + @Autowired + private MemberPostCommentRepository memberPostCommentRepository; + + @BeforeEach + void setUp() { + memberRepository.save( + new Member( + TEST_MEMBER_ID, + "testUser1", + LocalDate.now(), + "", "", "" + ) + ); + TEST_MEMBER_TOKEN = tokenGenerator + .generateTokenPair(TEST_MEMBER_ID) + .accessToken(); + + memberPostRepository.save( + new MemberPost( + TEST_POST_ID, + TEST_MEMBER_ID, + "img", + "img", + "content" + ) + ); + } + + @Test + void 게시물_댓글_추가_테스트() throws Exception { + //given + String comment = "testComment"; + CreatePostCommentRequest createPostCommentRequest = new CreatePostCommentRequest( + comment + ); + + + //when + ResultActions resultActions = mockMvc.perform( + post("/v1/posts/{postId}/comments", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createPostCommentRequest)) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.comment").value(comment)) + .andExpect(jsonPath("$.memberId").value(TEST_MEMBER_ID)) + .andExpect(jsonPath("$.postId").value(TEST_POST_ID)); + + } + + @Test + void 게시물_댓글_삭제_테스트() throws Exception { + //given + String commentId = "01HGW2N7EHJVJ4CJ999RRS2A97"; + memberPostCommentRepository.save( + new MemberPostComment( + commentId, + memberPostRepository.getReferenceById(TEST_POST_ID), + TEST_MEMBER_ID, + "comment" + ) + ); + + //when + ResultActions resultActions = mockMvc.perform( + delete("/v1/posts/{postId}/comments/{commentId}", TEST_POST_ID, commentId) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + void 게시물_댓글_수정_테스트() throws Exception { + //given + String commentId = "01HGW2N7EHJVJ4CJ999RRS2A97"; + String newContent = "hello world"; + UpdatePostCommentRequest updatePostCommentRequest = new UpdatePostCommentRequest( + newContent + ); + memberPostCommentRepository.save( + new MemberPostComment( + commentId, + memberPostRepository.getReferenceById(TEST_POST_ID), + TEST_MEMBER_ID, + "comment" + ) + ); + + //when + ResultActions resultActions = mockMvc.perform( + put("/v1/posts/{postId}/comments/{commentId}", TEST_POST_ID, commentId) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatePostCommentRequest)) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.commentId").value(commentId)) + .andExpect(jsonPath("$.comment").value(newContent)) + .andExpect(jsonPath("$.memberId").value(TEST_MEMBER_ID)) + .andExpect(jsonPath("$.postId").value(TEST_POST_ID)) + ; + } + + @Test + void 게시물_댓글_조회_테스트() throws Exception { + //given + String commentId = "01HGW2N7EHJVJ4CJ999RRS2A97"; + String content = "hello world"; + memberPostCommentRepository.save( + new MemberPostComment( + commentId, + memberPostRepository.getReferenceById(TEST_POST_ID), + TEST_MEMBER_ID, + content + ) + ); + + //when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/{postId}/comments", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].commentId").value(commentId)) + .andExpect(jsonPath("$.results[0].comment").value(content)) + .andExpect(jsonPath("$.results[0].memberId").value(TEST_MEMBER_ID)) + .andExpect(jsonPath("$.results[0].postId").value(TEST_POST_ID)) + ; + } +} diff --git a/post/src/main/java/com/oing/controller/MemberPostCommentController.java b/post/src/main/java/com/oing/controller/MemberPostCommentController.java new file mode 100644 index 00000000..1244cebc --- /dev/null +++ b/post/src/main/java/com/oing/controller/MemberPostCommentController.java @@ -0,0 +1,127 @@ +package com.oing.controller; + +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostComment; +import com.oing.domain.PaginationDTO; +import com.oing.dto.request.CreatePostCommentRequest; +import com.oing.dto.request.UpdatePostCommentRequest; +import com.oing.dto.response.DefaultResponse; +import com.oing.dto.response.PaginationResponse; +import com.oing.dto.response.PostCommentResponse; +import com.oing.exception.AuthorizationFailedException; +import com.oing.restapi.MemberPostCommentApi; +import com.oing.service.MemberBridge; +import com.oing.service.MemberPostCommentService; +import com.oing.service.MemberPostService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; + +@RequiredArgsConstructor +@Controller +public class MemberPostCommentController implements MemberPostCommentApi { + private final AuthenticationHolder authenticationHolder; + private final IdentityGenerator identityGenerator; + private final MemberPostService memberPostService; + private final MemberPostCommentService memberPostCommentService; + private final MemberBridge memberBridge; + + /** + * 게시물의 댓글을 생성합니다 + * @param postId 게시물 ID + * @param request 댓글 생성 요청 + * @return 생성된 댓글 + * @throws AuthorizationFailedException 내 가족이 올린 게시물이 아닌 경우 + */ + @Transactional + @Override + public PostCommentResponse createPostComment(String postId, CreatePostCommentRequest request) { + String memberId = authenticationHolder.getUserId(); + MemberPost memberPost = memberPostService.getMemberPostById(postId); + + // 내 가족의 게시물인지 검증 + if (!memberBridge.isInSameFamily(memberId, memberPost.getMemberId())) + throw new AuthorizationFailedException(); + + MemberPostComment memberPostComment = new MemberPostComment( + identityGenerator.generateIdentity(), + memberPost, + memberId, + request.content() + ); + MemberPostComment addedComment = memberPost.addComment(memberPostComment); + return PostCommentResponse.from(addedComment); + } + + /** + * 게시물의 댓글을 삭제합니다 + * @param postId 게시물 ID + * @param commentId 댓글 ID + * @return 삭제 결과 + * @throws AuthorizationFailedException 내가 작성한 댓글이 아닌 경우 + * @throws com.oing.exception.MemberPostCommentNotFoundException 댓글이 존재하지 않거나 게시물ID와 댓글ID가 일치하지 않는 경우 + */ + @Transactional + @Override + public DefaultResponse deletePostComment(String postId, String commentId) { + String memberId = authenticationHolder.getUserId(); + MemberPost memberPost = memberPostService.getMemberPostById(postId); + MemberPostComment memberPostComment = memberPostCommentService.getMemberPostComment(postId, commentId); + + //내가 작성한 댓글인지 권한 검증 + if (!memberPostComment.getMemberId().equals(memberId)) { + throw new AuthorizationFailedException(); + } + + memberPost.removeComment(memberPostComment); + return DefaultResponse.ok(); + } + + /** + * 게시물의 댓글을 수정합니다 + * @param postId 게시물 ID + * @param commentId 댓글 ID + * @param request 댓글 수정 요청 + * @return 수정된 댓글 + * @throws AuthorizationFailedException 내가 작성한 댓글이 아닌 경우 + * @throws com.oing.exception.MemberPostCommentNotFoundException 댓글이 존재하지 않거나 게시물ID와 댓글ID가 일치하지 않는 경우 + */ + @Transactional + @Override + public PostCommentResponse updatePostComment(String postId, String commentId, UpdatePostCommentRequest request) { + String memberId = authenticationHolder.getUserId(); + MemberPostComment memberPostComment = memberPostCommentService.getMemberPostComment(postId, commentId); + + //내가 작성한 댓글인지 권한 검증 + if (!memberPostComment.getMemberId().equals(memberId)) { + throw new AuthorizationFailedException(); + } + + memberPostComment.setContent(request.content()); + MemberPostComment savedMemberPostComment = memberPostCommentService + .savePostComment(memberPostComment); + return PostCommentResponse.from(savedMemberPostComment); + } + + /** + * 게시물의 댓글 목록을 조회합니다 + * @param postId 게시물 ID + * @param page 페이지 번호 + * @param size 페이지 크기 + * @param sort 정렬 방식 (오름차순/내림차순) + * @return 댓글 목록 + */ + @Transactional + @Override + public PaginationResponse getPostComments(String postId, Integer page, Integer size, String sort) { + PaginationDTO fetchResult = memberPostCommentService.searchPostComments( + page, size, postId, sort == null || sort.equalsIgnoreCase("ASC") + ); + + return PaginationResponse + .of(fetchResult, page, size) + .map(PostCommentResponse::from); + } +} diff --git a/post/src/main/java/com/oing/domain/MemberPost.java b/post/src/main/java/com/oing/domain/MemberPost.java index 73b8cada..170632e9 100644 --- a/post/src/main/java/com/oing/domain/MemberPost.java +++ b/post/src/main/java/com/oing/domain/MemberPost.java @@ -71,4 +71,15 @@ public void removeReaction(MemberPostReaction reaction) { this.reactions.remove(reaction); this.reactionCnt -= 1; } + + public MemberPostComment addComment(MemberPostComment comment) { + this.comments.add(comment); + this.commentCnt = this.comments.size(); + return comment; + } + + public void removeComment(MemberPostComment comment) { + this.comments.remove(comment); + this.commentCnt = this.comments.size(); + } } diff --git a/post/src/main/java/com/oing/domain/MemberPostComment.java b/post/src/main/java/com/oing/domain/MemberPostComment.java index 1da2dd79..a5d998e7 100644 --- a/post/src/main/java/com/oing/domain/MemberPostComment.java +++ b/post/src/main/java/com/oing/domain/MemberPostComment.java @@ -27,4 +27,8 @@ public class MemberPostComment extends BaseAuditEntity { @Column(name = "comment", nullable = false) private String comment; + + public void setContent(String comment) { + this.comment = comment; + } } diff --git a/post/src/main/java/com/oing/dto/request/CreatePostCommentRequest.java b/post/src/main/java/com/oing/dto/request/CreatePostCommentRequest.java new file mode 100644 index 00000000..fb5e7dd1 --- /dev/null +++ b/post/src/main/java/com/oing/dto/request/CreatePostCommentRequest.java @@ -0,0 +1,21 @@ +package com.oing.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * no5ing-server + * User: CChuYong + * Date: 2024/01/13 + * Time: 11:30 PM + */ +@Schema(description = "피드 게시물 댓글 생성 요청") +public record CreatePostCommentRequest( + @NotBlank + @Size(max = 255) + @Schema(description = "content", example = "댓글 내용", maxLength = 255) + String content +) { +} diff --git a/post/src/main/java/com/oing/dto/request/UpdatePostCommentRequest.java b/post/src/main/java/com/oing/dto/request/UpdatePostCommentRequest.java new file mode 100644 index 00000000..2dcea758 --- /dev/null +++ b/post/src/main/java/com/oing/dto/request/UpdatePostCommentRequest.java @@ -0,0 +1,20 @@ +package com.oing.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * no5ing-server + * User: CChuYong + * Date: 2024/01/13 + * Time: 11:30 PM + */ +@Schema(description = "피드 게시물 댓글 수정 요청") +public record UpdatePostCommentRequest( + @NotBlank + @Size(max = 255) + @Schema(description = "content", example = "댓글 내용", maxLength = 255) + String content +) { +} diff --git a/post/src/main/java/com/oing/dto/response/PostCommentResponse.java b/post/src/main/java/com/oing/dto/response/PostCommentResponse.java new file mode 100644 index 00000000..575884c1 --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/PostCommentResponse.java @@ -0,0 +1,35 @@ +package com.oing.dto.response; + +import com.oing.domain.MemberPostComment; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +@Schema(description = "피드 게시물 댓글 응답") +public record PostCommentResponse( + @Schema(description = "피드 게시물 댓글 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String commentId, + + @Schema(description = "피드 게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String postId, + + @Schema(description = "댓글 작성 사용자 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String memberId, + + @Schema(description = "피드 게시물 내용", example = "정말 환상적인 하루였네요!") + String comment, + + @Schema(description = "댓글 작성 시간", example = "2023-12-23T01:53:21.577347+09:00") + ZonedDateTime createdAt +) { + public static PostCommentResponse from(MemberPostComment postComment) { + return new PostCommentResponse( + postComment.getId(), + postComment.getPost().getId(), + postComment.getMemberId(), + postComment.getComment(), + postComment.getCreatedAt() != null ? postComment.getCreatedAt().atZone(ZoneId.systemDefault()) : null + ); + } +} diff --git a/post/src/main/java/com/oing/exception/MemberPostCommentNotFoundException.java b/post/src/main/java/com/oing/exception/MemberPostCommentNotFoundException.java new file mode 100644 index 00000000..85854dd0 --- /dev/null +++ b/post/src/main/java/com/oing/exception/MemberPostCommentNotFoundException.java @@ -0,0 +1,7 @@ +package com.oing.exception; + +public class MemberPostCommentNotFoundException extends DomainException { + public MemberPostCommentNotFoundException() { + super(ErrorCode.POST_COMMENT_NOT_FOUND); + } +} diff --git a/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java b/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java index e99c0dbf..afe745f2 100644 --- a/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java @@ -3,5 +3,5 @@ import com.oing.domain.MemberPostComment; import org.springframework.data.jpa.repository.JpaRepository; -public interface MemberPostCommentRepository extends JpaRepository { +public interface MemberPostCommentRepository extends JpaRepository, MemberPostCommentRepositoryCustom { } diff --git a/post/src/main/java/com/oing/repository/MemberPostCommentRepositoryCustom.java b/post/src/main/java/com/oing/repository/MemberPostCommentRepositoryCustom.java new file mode 100644 index 00000000..bd18c317 --- /dev/null +++ b/post/src/main/java/com/oing/repository/MemberPostCommentRepositoryCustom.java @@ -0,0 +1,8 @@ +package com.oing.repository; + +import com.oing.domain.MemberPostComment; +import com.querydsl.core.QueryResults; + +public interface MemberPostCommentRepositoryCustom { + QueryResults searchPostComments(int page, int size, String postId, boolean asc); +} diff --git a/post/src/main/java/com/oing/repository/MemberPostCommentRepositoryCustomImpl.java b/post/src/main/java/com/oing/repository/MemberPostCommentRepositoryCustomImpl.java new file mode 100644 index 00000000..6b807da2 --- /dev/null +++ b/post/src/main/java/com/oing/repository/MemberPostCommentRepositoryCustomImpl.java @@ -0,0 +1,27 @@ +package com.oing.repository; + +import com.oing.domain.MemberPostComment; +import com.querydsl.core.QueryResults; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import static com.oing.domain.QMemberPostComment.memberPostComment; + +@RequiredArgsConstructor +@Repository +public class MemberPostCommentRepositoryCustomImpl implements MemberPostCommentRepositoryCustom { + private final JPAQueryFactory queryFactory; + + @Override + public QueryResults searchPostComments(int page, int size, String postId, boolean asc) { + return queryFactory + .select(memberPostComment) + .from(memberPostComment) + .where(memberPostComment.post.id.eq(postId)) + .orderBy(asc ? memberPostComment.id.asc() : memberPostComment.id.desc()) + .offset((long) (page - 1) * size) + .limit(size) + .fetchResults(); + } +} diff --git a/post/src/main/java/com/oing/restapi/MemberPostCommentApi.java b/post/src/main/java/com/oing/restapi/MemberPostCommentApi.java new file mode 100644 index 00000000..1ab5a30c --- /dev/null +++ b/post/src/main/java/com/oing/restapi/MemberPostCommentApi.java @@ -0,0 +1,83 @@ +package com.oing.restapi; + +import com.oing.dto.request.CreatePostCommentRequest; +import com.oing.dto.request.UpdatePostCommentRequest; +import com.oing.dto.response.DefaultResponse; +import com.oing.dto.response.PaginationResponse; +import com.oing.dto.response.PostCommentResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "게시물 댓글 API", description = "게시물 댓글 관련 API") +@RestController +@Valid +@RequestMapping("/v1/posts/{postId}/comments") +public interface MemberPostCommentApi { + @Operation(summary = "게시물 댓글 추가", description = "게시물에 댓글을 추가합니다.") + @PostMapping + PostCommentResponse createPostComment( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId, + + @Valid + @RequestBody + CreatePostCommentRequest request + ); + + @Operation(summary = "게시물 댓글 삭제", description = "게시물에 댓글을 삭제합니다.") + @DeleteMapping("/{commentId}") + DefaultResponse deletePostComment( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId, + + @Parameter(description = "댓글 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String commentId + ); + + @Operation(summary = "게시물 댓글 수정", description = "게시물에 댓글을 수정합니다.") + @PutMapping("/{commentId}") + PostCommentResponse updatePostComment( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId, + + @Parameter(description = "댓글 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String commentId, + + @Valid + @RequestBody + UpdatePostCommentRequest request + ); + + + @Operation(summary = "게시물 댓글 조회", description = "게시물에 달린 댓글을 조회합니다.") + @GetMapping + PaginationResponse getPostComments( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId, + + @RequestParam(required = false, defaultValue = "1") + @Parameter(description = "가져올 현재 페이지", example = "1") + @Min(value = 1) + Integer page, + + @RequestParam(required = false, defaultValue = "10") + @Parameter(description = "가져올 페이지당 크기", example = "10") + @Min(value = 1) + Integer size, + + @RequestParam(required = false) + @Parameter(description = "정렬 방식", example = "DESC | ASC") + String sort + ); + +} diff --git a/post/src/main/java/com/oing/service/MemberPostCommentService.java b/post/src/main/java/com/oing/service/MemberPostCommentService.java new file mode 100644 index 00000000..88e87574 --- /dev/null +++ b/post/src/main/java/com/oing/service/MemberPostCommentService.java @@ -0,0 +1,71 @@ +package com.oing.service; + +import com.oing.domain.MemberPostComment; +import com.oing.domain.PaginationDTO; +import com.oing.exception.MemberPostCommentNotFoundException; +import com.oing.repository.MemberPostCommentRepository; +import com.querydsl.core.QueryResults; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class MemberPostCommentService { + private final MemberPostCommentRepository memberPostCommentRepository; + + /** + * 게시물의 댓글을 저장합니다 + * @param memberPostComment 댓글 + * @return 저장된 댓글 + */ + @Transactional + public MemberPostComment savePostComment(MemberPostComment memberPostComment) { + return memberPostCommentRepository.save(memberPostComment); + } + + /** + * 게시물의 댓글을 조회합니다 + * @param postId 게시글 ID + * @param commentId 댓글 ID + * @return 댓글 + * @throws MemberPostCommentNotFoundException 댓글이 존재하지 않거나 게시글 ID가 댓글의 ID와 일치하지 않을 경우 + */ + @Transactional + public MemberPostComment getMemberPostComment(String postId, String commentId) { + MemberPostComment memberPostComment = memberPostCommentRepository + .findById(commentId) + .orElseThrow(MemberPostCommentNotFoundException::new); + + if (!memberPostComment.getPost().getId().equals(postId)) throw new MemberPostCommentNotFoundException(); + return memberPostComment; + } + + /** + * 게시물의 댓글을 삭제합니다 + * @param memberPostComment 댓글 + */ + @Transactional + public void deletePostComment(MemberPostComment memberPostComment) { + memberPostCommentRepository.delete(memberPostComment); + } + + /** + * 게시글의 댓글들을 조회합니다. + * @param page 페이지 + * @param size 페이지당 댓글 수 + * @param postId 게시글 ID + * @param asc 오름차순 여부 + * @return 댓글들 조회 결과 + */ + @Transactional + public PaginationDTO searchPostComments(int page, int size, String postId, boolean asc) { + QueryResults results = memberPostCommentRepository + .searchPostComments(page, size, postId, asc); + int totalPage = (int) Math.ceil((double) results.getTotal() / size); + return new PaginationDTO<>( + totalPage, + results.getResults() + ); + } +} diff --git a/post/src/main/java/com/oing/service/MemberPostService.java b/post/src/main/java/com/oing/service/MemberPostService.java index ee8dce5e..56f79372 100644 --- a/post/src/main/java/com/oing/service/MemberPostService.java +++ b/post/src/main/java/com/oing/service/MemberPostService.java @@ -78,7 +78,7 @@ public MemberPost save(MemberPost post) { @Transactional public MemberPost getMemberPostById(String postId) { - return memberPostRepository.findById(postId).orElseThrow(() -> new DomainException(ErrorCode.MEMBER_NOT_FOUND)); + return memberPostRepository.findById(postId).orElseThrow(PostNotFoundException::new); } @Transactional diff --git a/post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java b/post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java new file mode 100644 index 00000000..d7f59829 --- /dev/null +++ b/post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java @@ -0,0 +1,276 @@ +package com.oing.controller; + +import com.google.common.collect.Lists; +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostComment; +import com.oing.domain.PaginationDTO; +import com.oing.dto.request.CreatePostCommentRequest; +import com.oing.dto.request.UpdatePostCommentRequest; +import com.oing.dto.response.PaginationResponse; +import com.oing.dto.response.PostCommentResponse; +import com.oing.exception.AuthorizationFailedException; +import com.oing.service.MemberBridge; +import com.oing.service.MemberPostCommentService; +import com.oing.service.MemberPostService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +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 java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MemberPostCommentControllerTest { + @InjectMocks + private MemberPostCommentController memberPostCommentController; + + @Mock + private AuthenticationHolder authenticationHolder; + @Mock + private IdentityGenerator identityGenerator; + @Mock + private MemberPostService memberPostService; + @Mock + private MemberPostCommentService memberPostCommentService; + @Mock + private MemberBridge memberBridge; + + @Test + void 게시물_댓글_생성_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment memberPostComment = spy(new MemberPostComment( + "1", + memberPost, + "1", + "1" + )); + CreatePostCommentRequest request = new CreatePostCommentRequest(memberPostComment.getComment()); + when(memberPostService.getMemberPostById("1")).thenReturn(memberPost); + when(authenticationHolder.getUserId()).thenReturn("1"); + when(memberBridge.isInSameFamily("1", "1")).thenReturn(true); + when(identityGenerator.generateIdentity()).thenReturn(memberPost.getId()); + + //when + PostCommentResponse response = memberPostCommentController.createPostComment( + memberPost.getId(), + request + ); + + //then + assertEquals(response.comment(), request.content()); + } + + @Test + void 게시물_댓글_생성_내_가족이_아닌_경우_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment memberPostComment = spy(new MemberPostComment( + "1", + memberPost, + "1", + "1" + )); + CreatePostCommentRequest request = new CreatePostCommentRequest(memberPostComment.getComment()); + when(memberPostService.getMemberPostById("1")).thenReturn(memberPost); + when(authenticationHolder.getUserId()).thenReturn("1"); + when(memberBridge.isInSameFamily("1", "1")).thenReturn(false); + + //when + //then + assertThrows(AuthorizationFailedException.class, () -> { + memberPostCommentController.createPostComment( + memberPost.getId(), + request + ); + }); + } + + @Test + void 게시물_댓글_삭제_테스트() { + //given + MemberPost memberPost = spy(new MemberPost( + "1", + "1", + "1", + "1", + "1" + )); + MemberPostComment memberPostComment = spy(new MemberPostComment( + "1", + memberPost, + "1", + "1" + )); + when(memberPostService.getMemberPostById(memberPost.getId())).thenReturn(memberPost); + when(memberPostCommentService.getMemberPostComment(memberPost.getId(), memberPostComment.getId())) + .thenReturn(memberPostComment); + when(authenticationHolder.getUserId()).thenReturn("1"); + + //when + memberPostCommentController.deletePostComment( + memberPost.getId(), + memberPostComment.getId() + ); + + //then + //nothing. just check no exception + } + + @Test + void 게시물_댓글_삭제_내가_작성하지_않은경우_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment othersMemberPostComment = new MemberPostComment( + "1", + memberPost, + "2", + "1" + ); + when(memberPostCommentService.getMemberPostComment(memberPost.getId(), othersMemberPostComment.getId())) + .thenReturn(othersMemberPostComment); + when(authenticationHolder.getUserId()).thenReturn("1"); + + //when + //then + assertThrows(AuthorizationFailedException.class, () -> { + memberPostCommentController.deletePostComment( + memberPost.getId(), + othersMemberPostComment.getId() + ); + }); + } + + @Test + void 게시물_댓글_수정_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment memberPostComment = spy(new MemberPostComment( + "1", + memberPost, + "1", + "1" + )); + UpdatePostCommentRequest request = new UpdatePostCommentRequest(memberPostComment.getComment()); + when(memberPostCommentService.getMemberPostComment(memberPost.getId(), memberPostComment.getId())) + .thenReturn(memberPostComment); + when(authenticationHolder.getUserId()).thenReturn("1"); + when(memberPostCommentService.savePostComment(any())).thenReturn(memberPostComment); + when(memberPostComment.getCreatedAt()).thenReturn(LocalDateTime.now()); + + //when + PostCommentResponse response = memberPostCommentController.updatePostComment( + memberPost.getId(), + memberPostComment.getId(), + request + ); + + //then + assertEquals(response.comment(), request.content()); + } + + @Test + void 게시물_댓글_수정_내가_작성하지_않은경우_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment othersMemberPostComment = new MemberPostComment( + "1", + memberPost, + "2", + "1" + ); + UpdatePostCommentRequest request = new UpdatePostCommentRequest(othersMemberPostComment.getComment()); + when(memberPostCommentService.getMemberPostComment(memberPost.getId(), othersMemberPostComment.getId())) + .thenReturn(othersMemberPostComment); + when(authenticationHolder.getUserId()).thenReturn("1"); + + //when + //then + assertThrows(AuthorizationFailedException.class, () -> { + memberPostCommentController.updatePostComment( + memberPost.getId(), + othersMemberPostComment.getId(), + request + ); + }); + } + + @Test + void 게시물_댓글_목록_조회_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment memberPostComment = spy(new MemberPostComment( + "1", + memberPost, + "1", + "1" + )); + int page = 1; + int size = 10; + String postId = "1"; + boolean asc = true; + List memberPostComments = Lists.newArrayList(memberPostComment); + + when(memberPostComment.getCreatedAt()).thenReturn(LocalDateTime.now()); + when(memberPostCommentService.searchPostComments(page, size, postId, asc)) + .thenReturn(new PaginationDTO( + 5, + memberPostComments + )); + + //when + PaginationResponse responses = memberPostCommentController + .getPostComments(postId, page, size, "ASC"); + + //then + assertEquals(responses.results().size(), memberPostComments.size()); + assertEquals(responses.currentPage(), page); + assertEquals(responses.itemPerPage(), size); + } +} diff --git a/post/src/test/java/com/oing/service/MemberPostCommentServiceTest.java b/post/src/test/java/com/oing/service/MemberPostCommentServiceTest.java new file mode 100644 index 00000000..708b3078 --- /dev/null +++ b/post/src/test/java/com/oing/service/MemberPostCommentServiceTest.java @@ -0,0 +1,161 @@ +package com.oing.service; + +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostComment; +import com.oing.domain.PaginationDTO; +import com.oing.exception.MemberPostCommentNotFoundException; +import com.oing.repository.MemberPostCommentRepository; +import com.querydsl.core.QueryResults; +import org.assertj.core.util.Lists; +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MemberPostCommentServiceTest { + @InjectMocks + private MemberPostCommentService memberPostCommentService; + + @Mock + private MemberPostCommentRepository memberPostCommentRepository; + + @Test + void 게시물_저장_테스트() { + //given + MemberPostComment memberPostComment = new MemberPostComment( + "1", + null, + "1", + "1" + ); + when(memberPostCommentRepository.save(any())).thenReturn(memberPostComment); + + //when + MemberPostComment memberPostComment1 = memberPostCommentService.savePostComment(memberPostComment); + + //then + assertEquals(memberPostComment, memberPostComment1); + } + + @Test + void 게시물_댓글_조회_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment memberPostComment = new MemberPostComment( + "1", + memberPost, + "1", + "1" + ); + when(memberPostCommentRepository.findById("1")).thenReturn(java.util.Optional.of(memberPostComment)); + + //when + MemberPostComment memberPostComment1 = memberPostCommentService + .getMemberPostComment("1", "1"); + + //then + assertEquals(memberPostComment, memberPostComment1); + } + + @Test + void 게시물_댓글_조회_게시물ID_댓글ID_불일치_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment memberPostComment = new MemberPostComment( + "1", + memberPost, + "1", + "1" + ); + when(memberPostCommentRepository.findById("2")).thenReturn(java.util.Optional.of(memberPostComment)); + + //when + //then + assertThrows(MemberPostCommentNotFoundException.class, () -> { + memberPostCommentService.getMemberPostComment("2", "2"); + }); + } + + @Test + void 게시물_댓글_조회_댓글_못찾음_테스트() { + //given + when(memberPostCommentRepository.findById("1")).thenReturn(java.util.Optional.empty()); + + //when + //then + assertThrows(MemberPostCommentNotFoundException.class, () -> { + memberPostCommentService.getMemberPostComment("1", "1"); + }); + } + + @Test + void 게시물_삭제_테스트() { + //given + MemberPostComment memberPostComment = new MemberPostComment( + "1", + null, + "1", + "1" + ); + doNothing().when(memberPostCommentRepository).delete(any()); + + //when + memberPostCommentService.deletePostComment(memberPostComment); + + //then + //ignore if no exception + } + + @Test + void 게시물_댓글_검색_테스트() { + //given + MemberPost memberPost = new MemberPost( + "1", + "1", + "1", + "1", + "1" + ); + MemberPostComment memberPostComment = new MemberPostComment( + "1", + memberPost, + "1", + "1" + ); + int page = 1; + int size = 5; + when(memberPostCommentRepository.searchPostComments( + page, + size, + memberPost.getId(), + true + )).thenReturn(new QueryResults<>(Lists.newArrayList(memberPostComment), (long)size, 1L, 1L)); + + //when + PaginationDTO memberPostComment1 = memberPostCommentService + .searchPostComments(page, size, memberPost.getId(), true); + + //then + //nothing + } +} From 52129516d1a7a50bb53894bc0cb55d20b7312582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC=20=28YeongMin=20Song=29?= Date: Mon, 15 Jan 2024 22:20:28 +0900 Subject: [PATCH 06/49] =?UTF-8?q?[OING-143]=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=ED=83=88=ED=87=B4=20=EC=9D=B4=EC=9C=A0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add quit reason feature * test: add MemberQuitReasonServiceTest * test: add MemberApiTest with quit * feat: change type to enum * feat: allow multi cause * test: fix test --- ...V202401132338__create_MemberQuitReason.sql | 8 ++ .../java/com/oing/restapi/MemberApiTest.java | 117 ++++++++++++++++++ .../com/oing/controller/MemberController.java | 9 +- .../com/oing/domain/MemberQuitReason.java | 28 +++++ .../com/oing/domain/MemberQuitReasonType.java | 26 ++++ .../oing/domain/key/MemberQuitReasonKey.java | 22 ++++ .../oing/dto/request/QuitMemberRequest.java | 22 ++++ .../MemberQuitReasonRepository.java | 8 ++ .../main/java/com/oing/restapi/MemberApi.java | 6 +- .../oing/service/MemberQuitReasonService.java | 26 ++++ .../service/MemberQuitReasonServiceTest.java | 43 +++++++ 11 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 gateway/src/main/resources/db/migration/V202401132338__create_MemberQuitReason.sql create mode 100644 gateway/src/test/java/com/oing/restapi/MemberApiTest.java create mode 100644 member/src/main/java/com/oing/domain/MemberQuitReason.java create mode 100644 member/src/main/java/com/oing/domain/MemberQuitReasonType.java create mode 100644 member/src/main/java/com/oing/domain/key/MemberQuitReasonKey.java create mode 100644 member/src/main/java/com/oing/dto/request/QuitMemberRequest.java create mode 100644 member/src/main/java/com/oing/repository/MemberQuitReasonRepository.java create mode 100644 member/src/main/java/com/oing/service/MemberQuitReasonService.java create mode 100644 member/src/test/java/com/oing/service/MemberQuitReasonServiceTest.java diff --git a/gateway/src/main/resources/db/migration/V202401132338__create_MemberQuitReason.sql b/gateway/src/main/resources/db/migration/V202401132338__create_MemberQuitReason.sql new file mode 100644 index 00000000..cf01f1db --- /dev/null +++ b/gateway/src/main/resources/db/migration/V202401132338__create_MemberQuitReason.sql @@ -0,0 +1,8 @@ +CREATE TABLE `member_quit_reason` +( + `member_id` CHAR(26) NOT NULL COMMENT '사용자아이디', + `reason_id` VARCHAR(255) NOT NULL COMMENT '탈퇴사유', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`member_id`, `reason_id`) +) DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci comment '탈퇴이유관리테이블'; diff --git a/gateway/src/test/java/com/oing/restapi/MemberApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberApiTest.java new file mode 100644 index 00000000..ab8d5bcb --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MemberApiTest.java @@ -0,0 +1,117 @@ +package com.oing.restapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.domain.Member; +import com.oing.domain.MemberQuitReasonType; +import com.oing.dto.request.QuitMemberRequest; +import com.oing.repository.MemberRepository; +import com.oing.service.TokenGenerator; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class MemberApiTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private TokenGenerator tokenGenerator; + @Autowired + private ObjectMapper objectMapper; + + + private String TEST_MEMBER_ID = "01HGW2N7EHJVJ4CJ999RRS2E97"; + private String TEST_MEMBER_TOKEN; + + @Autowired + private MemberRepository memberRepository; + + @BeforeEach + void setUp() { + memberRepository.save( + new Member( + TEST_MEMBER_ID, + "testUser1", + LocalDate.now(), + "", "", "" + ) + ); + TEST_MEMBER_TOKEN = tokenGenerator + .generateTokenPair(TEST_MEMBER_ID) + .accessToken(); + } + + @Test + void 회원탈퇴_이유없이_테스트() throws Exception { + // given + + // when + ResultActions resultActions = mockMvc.perform( + delete("/v1/members/{memberId}", TEST_MEMBER_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + void 회원탈퇴_이유있게_테스트() throws Exception { + // given + QuitMemberRequest quitMemberRequest = new QuitMemberRequest(List.of(MemberQuitReasonType.NO_FREQUENTLY_USE)); + + // when + ResultActions resultActions = mockMvc.perform( + delete("/v1/members/{memberId}", TEST_MEMBER_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(quitMemberRequest)) + ); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + void 회원탈퇴_이유여러개_테스트() throws Exception { + // given + QuitMemberRequest quitMemberRequest = new QuitMemberRequest(List.of( + MemberQuitReasonType.NO_FREQUENTLY_USE, MemberQuitReasonType.SERVICE_UX_IS_BAD)); + + // when + ResultActions resultActions = mockMvc.perform( + delete("/v1/members/{memberId}", TEST_MEMBER_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(quitMemberRequest)) + ); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } +} diff --git a/member/src/main/java/com/oing/controller/MemberController.java b/member/src/main/java/com/oing/controller/MemberController.java index d508205d..5e63277e 100644 --- a/member/src/main/java/com/oing/controller/MemberController.java +++ b/member/src/main/java/com/oing/controller/MemberController.java @@ -3,11 +3,13 @@ import com.oing.domain.Member; import com.oing.domain.PaginationDTO; import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.request.QuitMemberRequest; import com.oing.dto.request.UpdateMemberNameRequest; import com.oing.dto.request.UpdateMemberProfileImageUrlRequest; import com.oing.dto.response.*; import com.oing.exception.AuthorizationFailedException; import com.oing.restapi.MemberApi; +import com.oing.service.MemberQuitReasonService; import com.oing.service.MemberService; import com.oing.util.AuthenticationHolder; import com.oing.util.PreSignedUrlGenerator; @@ -25,6 +27,7 @@ public class MemberController implements MemberApi { private final AuthenticationHolder authenticationHolder; private final PreSignedUrlGenerator preSignedUrlGenerator; private final MemberService memberService; + private final MemberQuitReasonService memberQuitReasonService; @Override public PaginationResponse getFamilyMembersProfiles(Integer page, Integer size) { @@ -83,12 +86,16 @@ private void validateName(String name) { @Override @Transactional - public DefaultResponse deleteMember(String memberId) { + public DefaultResponse deleteMember(String memberId, QuitMemberRequest request) { validateMemberId(memberId); Member member = memberService.findMemberById(memberId); memberService.deleteAllSocialMembersByMember(memberId); member.deleteMemberInfo(); + if (request != null) { //For Api Version Compatibility + memberQuitReasonService.recordMemberQuitReason(memberId, request.reasonIds()); + } + return DefaultResponse.ok(); } diff --git a/member/src/main/java/com/oing/domain/MemberQuitReason.java b/member/src/main/java/com/oing/domain/MemberQuitReason.java new file mode 100644 index 00000000..9aa7c326 --- /dev/null +++ b/member/src/main/java/com/oing/domain/MemberQuitReason.java @@ -0,0 +1,28 @@ +package com.oing.domain; + +import com.oing.domain.key.MemberQuitReasonKey; +import jakarta.persistence.*; +import lombok.*; + +/** + * no5ing-server + * User: CChuYong + * Date: 2024/01/13 + * Time: 11:31 PM + */ +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(callSuper = false) +@AllArgsConstructor +@Getter +@IdClass(MemberQuitReasonKey.class) +@Entity(name = "member_quit_reason") +public class MemberQuitReason extends BaseEntity { + @Id + @Column(name = "member_id", length = 26, columnDefinition = "CHAR(26)") + private String memberId; + + @Id + @Enumerated(EnumType.STRING) + @Column(name = "reason_id", length = 255, columnDefinition = "VARCHAR(255)") + private MemberQuitReasonType reasonId; +} diff --git a/member/src/main/java/com/oing/domain/MemberQuitReasonType.java b/member/src/main/java/com/oing/domain/MemberQuitReasonType.java new file mode 100644 index 00000000..8cacc6f6 --- /dev/null +++ b/member/src/main/java/com/oing/domain/MemberQuitReasonType.java @@ -0,0 +1,26 @@ +package com.oing.domain; + +import lombok.RequiredArgsConstructor; + +import java.security.InvalidParameterException; + +@RequiredArgsConstructor +public enum MemberQuitReasonType { + NO_NEED_TO_SHARE_DAILY("가족과 일상을 공유하고 싶지 않아서"), + FAMILY_MEMBER_NOT_USING("가족 구성원이 참여하지 않아서"), + NO_PREFER_WIDGET_OR_NOTIFICATION("위젯이나 알림 기능을 선호하지 않아서"), + SERVICE_UX_IS_BAD("서비스 이용이 어렵거나 불편해서"), + NO_FREQUENTLY_USE("자주 사용하지 않아서"); + private final String description; + + public static MemberQuitReasonType fromString(String typeKey) { + return switch (typeKey.toUpperCase()) { + case "NO_NEED_TO_SHARE_DAILY" -> NO_NEED_TO_SHARE_DAILY; + case "FAMILY_MEMBER_NOT_USING" -> FAMILY_MEMBER_NOT_USING; + case "NO_PREFER_WIDGET_OR_NOTIFICATION" -> NO_PREFER_WIDGET_OR_NOTIFICATION; + case "SERVICE_UX_IS_BAD" -> SERVICE_UX_IS_BAD; + case "NO_FREQUENTLY_USE" -> NO_FREQUENTLY_USE; + default -> throw new InvalidParameterException(); + }; + } +} diff --git a/member/src/main/java/com/oing/domain/key/MemberQuitReasonKey.java b/member/src/main/java/com/oing/domain/key/MemberQuitReasonKey.java new file mode 100644 index 00000000..8667259d --- /dev/null +++ b/member/src/main/java/com/oing/domain/key/MemberQuitReasonKey.java @@ -0,0 +1,22 @@ +package com.oing.domain.key; + +import com.oing.domain.MemberQuitReasonType; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * no5ing-server + * User: CChuYong + * Date: 2024/01/02 + * Time: 11:43 AM + */ +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode +public class MemberQuitReasonKey implements Serializable { + private String memberId; + private MemberQuitReasonType reasonId; +} diff --git a/member/src/main/java/com/oing/dto/request/QuitMemberRequest.java b/member/src/main/java/com/oing/dto/request/QuitMemberRequest.java new file mode 100644 index 00000000..74cf5f98 --- /dev/null +++ b/member/src/main/java/com/oing/dto/request/QuitMemberRequest.java @@ -0,0 +1,22 @@ +package com.oing.dto.request; + +import com.oing.domain.MemberQuitReasonType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.time.LocalDate; +import java.util.List; + +/** + * no5ing-server + * User: CChuYong + * Date: 2024/01/13 + * Time: 11:13 PM + */ +@Schema(description = "사용자 회원탈퇴 요청") +public record QuitMemberRequest( + @Schema(description = "탈퇴 사유 목록", example = "NO_FREQUENTLY_USE") + List reasonIds +) { +} diff --git a/member/src/main/java/com/oing/repository/MemberQuitReasonRepository.java b/member/src/main/java/com/oing/repository/MemberQuitReasonRepository.java new file mode 100644 index 00000000..3055c172 --- /dev/null +++ b/member/src/main/java/com/oing/repository/MemberQuitReasonRepository.java @@ -0,0 +1,8 @@ +package com.oing.repository; + +import com.oing.domain.MemberQuitReason; +import com.oing.domain.key.MemberQuitReasonKey; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberQuitReasonRepository extends JpaRepository { +} diff --git a/member/src/main/java/com/oing/restapi/MemberApi.java b/member/src/main/java/com/oing/restapi/MemberApi.java index 3db20f4c..209826e5 100644 --- a/member/src/main/java/com/oing/restapi/MemberApi.java +++ b/member/src/main/java/com/oing/restapi/MemberApi.java @@ -1,6 +1,7 @@ package com.oing.restapi; import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.request.QuitMemberRequest; import com.oing.dto.request.UpdateMemberNameRequest; import com.oing.dto.request.UpdateMemberProfileImageUrlRequest; import com.oing.dto.response.*; @@ -77,6 +78,9 @@ MemberResponse updateMemberName( DefaultResponse deleteMember( @Parameter(description = "탈퇴할 회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable - String memberId + String memberId, + + @RequestBody(required = false) //for api version compatibility + QuitMemberRequest request ); } diff --git a/member/src/main/java/com/oing/service/MemberQuitReasonService.java b/member/src/main/java/com/oing/service/MemberQuitReasonService.java new file mode 100644 index 00000000..6793a2f9 --- /dev/null +++ b/member/src/main/java/com/oing/service/MemberQuitReasonService.java @@ -0,0 +1,26 @@ +package com.oing.service; + +import com.oing.domain.MemberQuitReason; +import com.oing.domain.MemberQuitReasonType; +import com.oing.repository.MemberQuitReasonRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class MemberQuitReasonService { + private final MemberQuitReasonRepository memberQuitReasonRepository; + + @Transactional + public void recordMemberQuitReason(String memberId, List reasonIds) { + List records = reasonIds + .stream() + .map(reasonId -> new MemberQuitReason(memberId, reasonId)) + .collect(Collectors.toList()); + memberQuitReasonRepository.saveAll(records); + } +} diff --git a/member/src/test/java/com/oing/service/MemberQuitReasonServiceTest.java b/member/src/test/java/com/oing/service/MemberQuitReasonServiceTest.java new file mode 100644 index 00000000..fdfe2e28 --- /dev/null +++ b/member/src/test/java/com/oing/service/MemberQuitReasonServiceTest.java @@ -0,0 +1,43 @@ +package com.oing.service; + +import com.oing.domain.MemberQuitReason; +import com.oing.domain.MemberQuitReasonType; +import com.oing.repository.MemberQuitReasonRepository; +import org.assertj.core.util.Lists; +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MemberQuitReasonServiceTest { + @InjectMocks + private MemberQuitReasonService memberQuitReasonService; + + @Mock + private MemberQuitReasonRepository memberQuitReasonRepository; + + @Test + void 탈퇴_사유_저장_테스트() { + // given + String memberId = "memberId"; + MemberQuitReasonType reasonId = MemberQuitReasonType.FAMILY_MEMBER_NOT_USING; + when(memberQuitReasonRepository.saveAll(any())).thenReturn( + Lists.list(new MemberQuitReason( + memberId, + reasonId + )) + ); + + // when + memberQuitReasonService.recordMemberQuitReason(memberId, Lists.list(reasonId)); + // then + //nothing. just check no exception + } + +} From 10d7eb7915a31a5ac7a15ac0f303594512e535e6 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:42:23 +0900 Subject: [PATCH 07/49] feat: add dayOfBirth in FamilyMemberProfileResponse (#103) --- .../dto/response/FamilyMemberProfileResponse.java | 13 +++++++++---- .../com/oing/controller/MemberControllerTest.java | 4 ++-- .../response/FamilyMemberProfileResponseTest.java | 7 +++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/member/src/main/java/com/oing/dto/response/FamilyMemberProfileResponse.java b/member/src/main/java/com/oing/dto/response/FamilyMemberProfileResponse.java index 09c245f7..5373d469 100644 --- a/member/src/main/java/com/oing/dto/response/FamilyMemberProfileResponse.java +++ b/member/src/main/java/com/oing/dto/response/FamilyMemberProfileResponse.java @@ -3,6 +3,8 @@ import com.oing.domain.Member; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; + @Schema(description = "가족 구성원 프로필 응답") public record FamilyMemberProfileResponse( @Schema(description = "구성원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E") @@ -12,13 +14,16 @@ public record FamilyMemberProfileResponse( String name, @Schema(description = "구성원 프로필 이미지 주소", example = "https://asset.no5ing.kr/post/01HGW2N7EHJVJ4CJ999RRS2E97") - String imageUrl + String imageUrl, + + @Schema(description = "구성원의 생일", example = "2021-12-05") + LocalDate dayOfBirth ) { - public static FamilyMemberProfileResponse of(String memberId, String name, String imageUrl) { - return new FamilyMemberProfileResponse(memberId, name, imageUrl); + public static FamilyMemberProfileResponse of(String memberId, String name, String imageUrl, LocalDate dayOfBirth) { + return new FamilyMemberProfileResponse(memberId, name, imageUrl, dayOfBirth); } public static FamilyMemberProfileResponse of(Member member) { - return of(member.getId(), member.getName(), member.getProfileImgUrl()); + return of(member.getId(), member.getName(), member.getProfileImgUrl(), member.getDayOfBirth()); } } diff --git a/member/src/test/java/com/oing/controller/MemberControllerTest.java b/member/src/test/java/com/oing/controller/MemberControllerTest.java index 669d1f99..ebc47af1 100644 --- a/member/src/test/java/com/oing/controller/MemberControllerTest.java +++ b/member/src/test/java/com/oing/controller/MemberControllerTest.java @@ -73,8 +73,8 @@ public class MemberControllerTest { when(authenticationHolder.getUserId()).thenReturn("1"); when(memberService.findFamilyIdByMemberId(anyString())).thenReturn(familyId); Page profilePage = new PageImpl<>(Arrays.asList( - new FamilyMemberProfileResponse(member1.getId(), member1.getName(), member1.getProfileImgUrl()), - new FamilyMemberProfileResponse(member2.getId(), member2.getName(), member2.getProfileImgUrl()) + new FamilyMemberProfileResponse(member1.getId(), member1.getName(), member1.getProfileImgUrl(), member1.getDayOfBirth()), + new FamilyMemberProfileResponse(member2.getId(), member2.getName(), member2.getProfileImgUrl(), member2.getDayOfBirth()) )); when(memberService.findFamilyMembersProfilesByFamilyId(familyId, 1, 5)) .thenReturn(profilePage); diff --git a/member/src/test/java/com/oing/dto/response/FamilyMemberProfileResponseTest.java b/member/src/test/java/com/oing/dto/response/FamilyMemberProfileResponseTest.java index 3002c122..cbec8bbf 100644 --- a/member/src/test/java/com/oing/dto/response/FamilyMemberProfileResponseTest.java +++ b/member/src/test/java/com/oing/dto/response/FamilyMemberProfileResponseTest.java @@ -3,6 +3,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.time.LocalDate; + import static org.junit.jupiter.api.Assertions.assertEquals; public class FamilyMemberProfileResponseTest { @@ -13,14 +15,15 @@ void testFamilyMemberProfileResponse() { String memberId = "1"; String name = "디프만"; String imageUrl = "https://asset.no5ing.kr/post/01HGW2N7EHJVJ4CJ999RRS2E97"; - + LocalDate dayOfBirth = LocalDate.of(2000, 7, 8); // when - FamilyMemberProfileResponse response = new FamilyMemberProfileResponse(memberId, name, imageUrl); + FamilyMemberProfileResponse response = new FamilyMemberProfileResponse(memberId, name, imageUrl, dayOfBirth); // then assertEquals(response.memberId(), memberId); assertEquals(response.name(), name); assertEquals(response.imageUrl(), imageUrl); + assertEquals(response.dayOfBirth(), dayOfBirth); } } From 5565237606784dc36e0c562eb8d4a5e665deecb8 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:42:53 +0900 Subject: [PATCH 08/49] =?UTF-8?q?[OING-148]=20feat:=20Post=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add DeleteMemberPost Api * refactor: add EventListener for DeleteMemberPost --- .../com/oing/controller/MemberPostController.java | 7 +++++++ .../repository/MemberPostCommentRepository.java | 1 + .../repository/MemberPostReactionRepository.java | 2 ++ .../main/java/com/oing/restapi/MemberPostApi.java | 10 ++++++++++ .../com/oing/service/MemberPostCommentService.java | 9 +++++++++ .../com/oing/service/MemberPostReactionService.java | 8 ++++++++ .../java/com/oing/service/MemberPostService.java | 13 ++++++++++--- .../oing/service/event/DeleteMemberPostEvent.java | 6 ++++++ 8 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 post/src/main/java/com/oing/service/event/DeleteMemberPostEvent.java diff --git a/post/src/main/java/com/oing/controller/MemberPostController.java b/post/src/main/java/com/oing/controller/MemberPostController.java index cf1735ea..92369b8a 100644 --- a/post/src/main/java/com/oing/controller/MemberPostController.java +++ b/post/src/main/java/com/oing/controller/MemberPostController.java @@ -5,6 +5,7 @@ import com.oing.domain.PaginationDTO; import com.oing.dto.request.CreatePostRequest; import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.response.DefaultResponse; import com.oing.dto.response.PaginationResponse; import com.oing.dto.response.PostResponse; import com.oing.dto.response.PreSignedUrlResponse; @@ -107,4 +108,10 @@ public PostResponse getPost(String postId) { MemberPost memberPostProjection = memberPostService.getMemberPostById(postId); return PostResponse.from(memberPostProjection); } + + @Override + public DefaultResponse deletePost(String postId) { + memberPostService.deleteMemberPostById(postId); + return DefaultResponse.ok(); + } } diff --git a/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java b/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java index afe745f2..dd40360e 100644 --- a/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java @@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface MemberPostCommentRepository extends JpaRepository, MemberPostCommentRepositoryCustom { + void deleteAllByPostId(String memberPostId); } diff --git a/post/src/main/java/com/oing/repository/MemberPostReactionRepository.java b/post/src/main/java/com/oing/repository/MemberPostReactionRepository.java index d3dbb5e6..8cab8833 100644 --- a/post/src/main/java/com/oing/repository/MemberPostReactionRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostReactionRepository.java @@ -14,4 +14,6 @@ public interface MemberPostReactionRepository extends JpaRepository findReactionByPostAndMemberIdAndEmoji(MemberPost post, String memberId, Emoji emoji); List findAllByPostId(String postId); + + void deleteAllByPostId(String memberPostId); } diff --git a/post/src/main/java/com/oing/restapi/MemberPostApi.java b/post/src/main/java/com/oing/restapi/MemberPostApi.java index bd3e3d08..844c67ad 100644 --- a/post/src/main/java/com/oing/restapi/MemberPostApi.java +++ b/post/src/main/java/com/oing/restapi/MemberPostApi.java @@ -2,6 +2,7 @@ import com.oing.dto.request.CreatePostRequest; import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.response.DefaultResponse; import com.oing.dto.response.PaginationResponse; import com.oing.dto.response.PostResponse; import com.oing.dto.response.PreSignedUrlResponse; @@ -76,4 +77,13 @@ PostResponse getPost( @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") String postId ); + + //* 테스트용 API + @Operation(summary = "게시물 삭제", description = "ID를 통해 게시물을 삭제합니다.") + @DeleteMapping("/{postId}") + DefaultResponse deletePost( + @PathVariable + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String postId + ); } diff --git a/post/src/main/java/com/oing/service/MemberPostCommentService.java b/post/src/main/java/com/oing/service/MemberPostCommentService.java index 88e87574..054713de 100644 --- a/post/src/main/java/com/oing/service/MemberPostCommentService.java +++ b/post/src/main/java/com/oing/service/MemberPostCommentService.java @@ -1,12 +1,15 @@ package com.oing.service; +import com.oing.domain.MemberPost; import com.oing.domain.MemberPostComment; import com.oing.domain.PaginationDTO; import com.oing.exception.MemberPostCommentNotFoundException; import com.oing.repository.MemberPostCommentRepository; +import com.oing.service.event.DeleteMemberPostEvent; import com.querydsl.core.QueryResults; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -68,4 +71,10 @@ public PaginationDTO searchPostComments(int page, int size, S results.getResults() ); } + + @EventListener + public void deleteAllWhenPostDelete(DeleteMemberPostEvent event) { + MemberPost post = event.memberPost(); + memberPostCommentRepository.deleteAllByPostId(post.getId()); + } } diff --git a/post/src/main/java/com/oing/service/MemberPostReactionService.java b/post/src/main/java/com/oing/service/MemberPostReactionService.java index 30105e7f..268f30b3 100644 --- a/post/src/main/java/com/oing/service/MemberPostReactionService.java +++ b/post/src/main/java/com/oing/service/MemberPostReactionService.java @@ -5,7 +5,9 @@ import com.oing.domain.MemberPostReaction; import com.oing.exception.EmojiNotFoundException; import com.oing.repository.MemberPostReactionRepository; +import com.oing.service.event.DeleteMemberPostEvent; import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import java.util.List; @@ -39,4 +41,10 @@ public void deletePostReaction(MemberPostReaction reaction) { public List getMemberPostReactionsByPostId(String postId) { return memberPostReactionRepository.findAllByPostId(postId); } + + @EventListener + public void deleteAllWhenPostDelete(DeleteMemberPostEvent event) { + MemberPost post = event.memberPost(); + memberPostReactionRepository.deleteAllByPostId(post.getId()); + } } diff --git a/post/src/main/java/com/oing/service/MemberPostService.java b/post/src/main/java/com/oing/service/MemberPostService.java index 56f79372..3d46af10 100644 --- a/post/src/main/java/com/oing/service/MemberPostService.java +++ b/post/src/main/java/com/oing/service/MemberPostService.java @@ -3,13 +3,13 @@ import com.oing.domain.MemberPost; import com.oing.domain.MemberPostDailyCalendarDTO; import com.oing.domain.PaginationDTO; -import com.oing.exception.DomainException; -import com.oing.exception.ErrorCode; import com.oing.exception.PostNotFoundException; import com.oing.repository.MemberPostRepository; +import com.oing.service.event.DeleteMemberPostEvent; import com.querydsl.core.QueryResults; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import java.time.LocalDate; @@ -20,7 +20,7 @@ public class MemberPostService { private final MemberPostRepository memberPostRepository; - + private final ApplicationEventPublisher applicationEventPublisher; /** * 멤버들이 범위 날짜 안에 올린 대표 게시물들을 가져온다. @@ -90,4 +90,11 @@ public PaginationDTO searchMemberPost(int page, int size, LocalDate results.getResults() ); } + + @Transactional + public void deleteMemberPostById(String postId) { + MemberPost memberPost = memberPostRepository.findById(postId).orElseThrow(PostNotFoundException::new); + applicationEventPublisher.publishEvent(new DeleteMemberPostEvent(memberPost)); + memberPostRepository.delete(memberPost); + } } diff --git a/post/src/main/java/com/oing/service/event/DeleteMemberPostEvent.java b/post/src/main/java/com/oing/service/event/DeleteMemberPostEvent.java new file mode 100644 index 00000000..2403c229 --- /dev/null +++ b/post/src/main/java/com/oing/service/event/DeleteMemberPostEvent.java @@ -0,0 +1,6 @@ +package com.oing.service.event; + +import com.oing.domain.MemberPost; + +public record DeleteMemberPostEvent(MemberPost memberPost) { +} From 8bfa7135b870129de8119c13dc859660fa6890c4 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:57:41 +0900 Subject: [PATCH 09/49] =?UTF-8?q?[OING-147]=20feat:=20=EB=A6=AC=EC=96=BC?= =?UTF-8?q?=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=B0=8F=20=EB=A6=AC=EC=96=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20API=20=ED=8B=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add real emoji tbl migration file * feat: add RealEmoji Entity class * feat: add RealEmojiApi class * test: add additional field * feat: add PostRealEmojiResponse * feat: add RealEmoji Controller class * fix: change MemberPostRealEmoji's realEmoji Type * fix: change MemberPostRealEmoji's realEmoji relationship * style: change realEmoji dto * fix: delete BodyRequest in deleteRealEmoji API * refactor: refact RealEmojiApi uri --- .../V202401141930__add_real_emoji_tbl.sql | 30 +++++++++ .../oing/controller/MemberControllerTest.java | 4 +- .../MemberPostRealEmojiController.java | 46 +++++++++++++ .../controller/MemberRealEmojiController.java | 36 ++++++++++ .../main/java/com/oing/domain/MemberPost.java | 17 +++++ .../com/oing/domain/MemberPostRealEmoji.java | 31 +++++++++ .../java/com/oing/domain/MemberRealEmoji.java | 32 +++++++++ .../dto/request/CreateMyRealEmojiRequest.java | 16 +++++ .../dto/request/PostRealEmojiRequest.java | 12 ++++ .../dto/response/PostRealEmojiResponse.java | 20 ++++++ .../oing/dto/response/RealEmojiResponse.java | 16 +++++ .../oing/dto/response/RealEmojisResponse.java | 12 ++++ .../MemberPostRealEmojiRepository.java | 7 ++ .../repository/MemberRealEmojiRepository.java | 7 ++ .../oing/restapi/MemberPostRealEmojiApi.java | 50 ++++++++++++++ .../com/oing/restapi/MemberRealEmojiApi.java | 67 +++++++++++++++++++ .../domain/model/MemberPostCommentTest.java | 2 +- .../domain/model/MemberPostReactionTest.java | 2 +- .../com/oing/domain/model/MemberPostTest.java | 2 +- 19 files changed, 404 insertions(+), 5 deletions(-) create mode 100644 gateway/src/main/resources/db/migration/V202401141930__add_real_emoji_tbl.sql create mode 100644 post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java create mode 100644 post/src/main/java/com/oing/controller/MemberRealEmojiController.java create mode 100644 post/src/main/java/com/oing/domain/MemberPostRealEmoji.java create mode 100644 post/src/main/java/com/oing/domain/MemberRealEmoji.java create mode 100644 post/src/main/java/com/oing/dto/request/CreateMyRealEmojiRequest.java create mode 100644 post/src/main/java/com/oing/dto/request/PostRealEmojiRequest.java create mode 100644 post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java create mode 100644 post/src/main/java/com/oing/dto/response/RealEmojiResponse.java create mode 100644 post/src/main/java/com/oing/dto/response/RealEmojisResponse.java create mode 100644 post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java create mode 100644 post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java create mode 100644 post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java create mode 100644 post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java diff --git a/gateway/src/main/resources/db/migration/V202401141930__add_real_emoji_tbl.sql b/gateway/src/main/resources/db/migration/V202401141930__add_real_emoji_tbl.sql new file mode 100644 index 00000000..4b8f7b55 --- /dev/null +++ b/gateway/src/main/resources/db/migration/V202401141930__add_real_emoji_tbl.sql @@ -0,0 +1,30 @@ +CREATE TABLE `member_real_emoji` +( + `real_emoji_id` CHAR(26) NOT NULL COMMENT 'ULID', + `member_id` CHAR(26) NOT NULL COMMENT 'ULID', + `type` VARCHAR(16) NOT NULL, + `real_emoji_image_url` TEXT NOT NULL, + `real_emoji_image_key` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`real_emoji_id`), + INDEX `member_real_emoji_idx1` (`member_id`) +) DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci COMMENT '리얼이모지'; + +CREATE TABLE `member_post_real_emoji` +( + `post_real_emoji_id` CHAR(26) NOT NULL COMMENT 'ULID', + `real_emoji_id` CHAR(26) NOT NULL COMMENT 'ULID', + `post_id` CHAR(26) NOT NULL COMMENT 'ULID', + `member_id` CHAR(26) NOT NULL COMMENT 'ULID', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`post_real_emoji_id`), + FOREIGN KEY `member_post_real_emoji_fk1` (`post_id`) REFERENCES `member_post` (`post_id`), + FOREIGN KEY `member_post_real_emoji_fk2` (`real_emoji_id`) REFERENCES `member_real_emoji` (`real_emoji_id`), + INDEX `member_post_real_emoji_idx1` (`post_id`), + INDEX `member_post_real_emoji_idx2` (`member_id`) +) DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci COMMENT '게시물리얼이모지'; + +ALTER TABLE `member_post` ADD COLUMN `real_emoji_cnt` INTEGER NOT NULL DEFAULT 0; diff --git a/member/src/test/java/com/oing/controller/MemberControllerTest.java b/member/src/test/java/com/oing/controller/MemberControllerTest.java index ebc47af1..d8b01ce8 100644 --- a/member/src/test/java/com/oing/controller/MemberControllerTest.java +++ b/member/src/test/java/com/oing/controller/MemberControllerTest.java @@ -179,7 +179,7 @@ public class MemberControllerTest { when(authenticationHolder.getUserId()).thenReturn("1"); // when - memberController.deleteMember(member.getId()); + memberController.deleteMember(member.getId(), null); // then assertEquals("DeletedMember", member.getName()); @@ -194,6 +194,6 @@ public class MemberControllerTest { when(authenticationHolder.getUserId()).thenReturn("2"); // then - assertThrows(AuthorizationFailedException.class, () -> memberController.deleteMember(member.getId())); + assertThrows(AuthorizationFailedException.class, () -> memberController.deleteMember(member.getId(), null)); } } diff --git a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java new file mode 100644 index 00000000..b6bc44af --- /dev/null +++ b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java @@ -0,0 +1,46 @@ +package com.oing.controller; + + +import com.oing.domain.MemberPost; +import com.oing.domain.PaginationDTO; +import com.oing.dto.request.CreatePostRequest; +import com.oing.dto.request.PostRealEmojiRequest; +import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.response.*; +import com.oing.exception.DuplicatePostUploadException; +import com.oing.exception.InvalidUploadTimeException; +import com.oing.restapi.MemberPostApi; +import com.oing.restapi.MemberPostRealEmojiApi; +import com.oing.service.MemberBridge; +import com.oing.service.MemberPostService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +import com.oing.util.PreSignedUrlGenerator; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.Collections; + +@RequiredArgsConstructor +@Controller +public class MemberPostRealEmojiController implements MemberPostRealEmojiApi { + + @Override + public DefaultResponse createRealEmoji(String postId, PostRealEmojiRequest request) { + return new DefaultResponse(true); + } + + @Override + public DefaultResponse deleteRealEmoji(String postId, String realEmojiId) { + return new DefaultResponse(true); + } + + @Override + public ArrayResponse getPostRealEmojis(String postId) { + return ArrayResponse.of(Collections.emptyList()); + } +} diff --git a/post/src/main/java/com/oing/controller/MemberRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java new file mode 100644 index 00000000..de94bb27 --- /dev/null +++ b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java @@ -0,0 +1,36 @@ +package com.oing.controller; + + +import com.oing.dto.request.CreateMyRealEmojiRequest; +import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.response.DefaultResponse; +import com.oing.dto.response.PreSignedUrlResponse; +import com.oing.dto.response.RealEmojisResponse; +import com.oing.restapi.MemberRealEmojiApi; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; + +@RequiredArgsConstructor +@Controller +public class MemberRealEmojiController implements MemberRealEmojiApi { + + @Override + public PreSignedUrlResponse requestPresignedUrl(String memberId, PreSignedUrlRequest request) { + return new PreSignedUrlResponse("https://test/2021-08-22/real-emoji-1.jpg"); + } + + @Override + public DefaultResponse createMyRealEmoji(String memberId, CreateMyRealEmojiRequest request) { + return new DefaultResponse(true); + } + + @Override + public DefaultResponse changeMyRealEmoji(String memberId, String realEmojiId, CreateMyRealEmojiRequest request) { + return new DefaultResponse(true); + } + + @Override + public RealEmojisResponse getMyRealEmojis(String memberId) { + return new RealEmojisResponse(null); + } +} diff --git a/post/src/main/java/com/oing/domain/MemberPost.java b/post/src/main/java/com/oing/domain/MemberPost.java index 170632e9..009e3ae4 100644 --- a/post/src/main/java/com/oing/domain/MemberPost.java +++ b/post/src/main/java/com/oing/domain/MemberPost.java @@ -39,12 +39,18 @@ public class MemberPost extends BaseAuditEntity { @Column(name = "reaction_cnt", nullable = false, columnDefinition = "INTEGER DEFAULT 0") private int reactionCnt; + @Column(name = "real_emoji_cnt", nullable = false, columnDefinition = "INTEGER DEFAULT 0") + private int realEmojiCnt; + @OneToMany(mappedBy = "post") private List comments = new ArrayList<>(); @OneToMany(mappedBy = "post") private List reactions = new ArrayList<>(); + @OneToMany(mappedBy = "post") + private List realEmojis = new ArrayList<>(); + public MemberPost(String id, String memberId, String postImgUrl, String postImgKey, String content) { validateContent(content); this.id = id; @@ -54,6 +60,7 @@ public MemberPost(String id, String memberId, String postImgUrl, String postImgK this.content = content; this.commentCnt = 0; this.reactionCnt = 0; + this.realEmojiCnt = 0; } private void validateContent(String content) { @@ -72,6 +79,16 @@ public void removeReaction(MemberPostReaction reaction) { this.reactionCnt -= 1; } + public void addRealEmoji(MemberPostRealEmoji realEmoji) { + this.realEmojis.add(realEmoji); + this.realEmojiCnt += 1; + } + + public void removeRealEmoji(MemberPostRealEmoji realEmoji) { + this.realEmojis.remove(realEmoji); + this.realEmojiCnt -= 1; + } + public MemberPostComment addComment(MemberPostComment comment) { this.comments.add(comment); this.commentCnt = this.comments.size(); diff --git a/post/src/main/java/com/oing/domain/MemberPostRealEmoji.java b/post/src/main/java/com/oing/domain/MemberPostRealEmoji.java new file mode 100644 index 00000000..1ac0068d --- /dev/null +++ b/post/src/main/java/com/oing/domain/MemberPostRealEmoji.java @@ -0,0 +1,31 @@ +package com.oing.domain; + +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(callSuper = false) +@AllArgsConstructor +@Getter +@Table(indexes = { + @Index(name = "member_post_real_emoji_idx1", columnList = "post_id"), + @Index(name = "member_post_real_emoji_idx2", columnList = "member_id") +}) +@Entity(name = "member_post_real_emoji") +public class MemberPostRealEmoji extends BaseEntity { + + @Id + @Column(name = "post_real_emoji_id", columnDefinition = "CHAR(26)", nullable = false) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "real_emoji_id", nullable = false) + private MemberRealEmoji realEmoji; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private MemberPost post; + + @Column(name = "member_id", columnDefinition = "CHAR(26)", nullable = false) + private String memberId; +} diff --git a/post/src/main/java/com/oing/domain/MemberRealEmoji.java b/post/src/main/java/com/oing/domain/MemberRealEmoji.java new file mode 100644 index 00000000..aa44ef06 --- /dev/null +++ b/post/src/main/java/com/oing/domain/MemberRealEmoji.java @@ -0,0 +1,32 @@ +package com.oing.domain; + +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(callSuper = false) +@AllArgsConstructor +@Getter +@Table(indexes = { + @Index(name = "member_real_emoji_idx1", columnList = "member_id") +}) +@Entity(name = "member_real_emoji") +public class MemberRealEmoji extends BaseAuditEntity { + + @Id + @Column(name = "real_emoji_id", columnDefinition = "CHAR(26)", nullable = false) + private String id; + + @Column(name = "member_id", columnDefinition = "CHAR(26)", nullable = false) + private String memberId; + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + private Emoji type; + + @Column(name = "real_emoji_image_url", nullable = false) + private String realEmojiImageUrl; + + @Column(name = "real_emoji_image_key", nullable = false) + private String realEmojiImageKey; +} diff --git a/post/src/main/java/com/oing/dto/request/CreateMyRealEmojiRequest.java b/post/src/main/java/com/oing/dto/request/CreateMyRealEmojiRequest.java new file mode 100644 index 00000000..1a5aeb67 --- /dev/null +++ b/post/src/main/java/com/oing/dto/request/CreateMyRealEmojiRequest.java @@ -0,0 +1,16 @@ +package com.oing.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "자신의 리얼 이모지 생성 요청") +public record CreateMyRealEmojiRequest( + + @Schema(description = "리얼 이모지 타입", example = "EMOJI_1") + String type, + + @NotNull + @Schema(description = "리얼 이모지 사진 주소", example = "https://no5ing.com/feed/1.jpg") + String imageUrl +) { +} diff --git a/post/src/main/java/com/oing/dto/request/PostRealEmojiRequest.java b/post/src/main/java/com/oing/dto/request/PostRealEmojiRequest.java new file mode 100644 index 00000000..6480825e --- /dev/null +++ b/post/src/main/java/com/oing/dto/request/PostRealEmojiRequest.java @@ -0,0 +1,12 @@ +package com.oing.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "피드 게시물 리얼 이모지 생성 요청") +public record PostRealEmojiRequest( + @NotBlank + @Schema(description = "이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String realEmojiId +) { +} diff --git a/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java new file mode 100644 index 00000000..61a25db6 --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java @@ -0,0 +1,20 @@ +package com.oing.dto.response; + +import com.oing.domain.MemberPostReaction; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "피드 게시물 응답") +public record PostRealEmojiResponse( + @Schema(description = "피드 게시물 리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String realEmojiId, + + @Schema(description = "피드 게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String postId, + + @Schema(description = "반응 작성 사용자 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String memberId, + + @Schema(description = "피드 게시물 리얼 이모지 이미지 주소", example = "http://test.com/test-profile.jpg") + String emojiImageUrl +) { +} diff --git a/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java new file mode 100644 index 00000000..d1c52db7 --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java @@ -0,0 +1,16 @@ +package com.oing.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "자신이 생성한 리얼 이모지 응답") +public record RealEmojiResponse ( + @Schema(description = "리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String realEmojiId, + + @Schema(description = "리얼 이모지 타입", example = "EMOJI_1") + String type, + + @Schema(description = "리얼 이모지 이미지 주소", example = "https://no5ing.com/profile/1.jpg") + String imageUrl +){ +} diff --git a/post/src/main/java/com/oing/dto/response/RealEmojisResponse.java b/post/src/main/java/com/oing/dto/response/RealEmojisResponse.java new file mode 100644 index 00000000..85f84230 --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/RealEmojisResponse.java @@ -0,0 +1,12 @@ +package com.oing.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "자신이 생성한 리얼 이모지 리스트 응답") +public record RealEmojisResponse( + @Schema(description = "자신이 생성한 리얼 이모지 정보") + List myRealEmojiList +) { +} diff --git a/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java new file mode 100644 index 00000000..87a815f9 --- /dev/null +++ b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java @@ -0,0 +1,7 @@ +package com.oing.repository; + +import com.oing.domain.MemberPostRealEmoji; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberPostRealEmojiRepository extends JpaRepository { +} diff --git a/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java new file mode 100644 index 00000000..c926bd8b --- /dev/null +++ b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java @@ -0,0 +1,7 @@ +package com.oing.repository; + +import com.oing.domain.MemberRealEmoji; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRealEmojiRepository extends JpaRepository { +} diff --git a/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java b/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java new file mode 100644 index 00000000..3a2754f7 --- /dev/null +++ b/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java @@ -0,0 +1,50 @@ +package com.oing.restapi; + +import com.oing.dto.request.PostRealEmojiRequest; +import com.oing.dto.response.ArrayResponse; +import com.oing.dto.response.DefaultResponse; +import com.oing.dto.response.PostRealEmojiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "게시물 리얼 이모지 API", description = "게시물 리얼 이모지 관련 API") +@RestController +@Valid +@RequestMapping("/v1/posts/{postId}/real-emoji") +public interface MemberPostRealEmojiApi { + + @Operation(summary = "게시물에 리얼 이모지 등록", description = "게시물에 리얼 이모지를 추가합니다.") + @PostMapping + DefaultResponse createRealEmoji( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId, + + @Valid + @RequestBody + PostRealEmojiRequest request + ); + + @Operation(summary = "게시물에서 리얼 이모지 삭제", description = "게시물에서 리얼 이모지를 삭제합니다.") + @DeleteMapping("/{realEmojiId}") + DefaultResponse deleteRealEmoji( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId, + + @Parameter(description = "리얼 이모지 ID", example = "01HEFDFADFDFDAFDFDARS2E97") + @PathVariable + String realEmojiId + ); + + @Operation(summary = "게시물의 리얼 이모지 전체 조회", description = "게시물에 달린 모든 리얼 이모지 목록을 조회합니다.") + @GetMapping + ArrayResponse getPostRealEmojis( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId + ); +} diff --git a/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java b/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java new file mode 100644 index 00000000..3941a697 --- /dev/null +++ b/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java @@ -0,0 +1,67 @@ +package com.oing.restapi; + +import com.oing.dto.request.CreateMyRealEmojiRequest; +import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.response.DefaultResponse; +import com.oing.dto.response.RealEmojisResponse; +import com.oing.dto.response.PreSignedUrlResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "회원 리얼 이모지 API", description = "회원 리얼 이모지 관련 API") +@RestController +@Valid +@RequestMapping("/v1/members/{memberId}/real-emoji") +public interface MemberRealEmojiApi { + + @Operation(summary = "리얼 이모지 사진 Presigned Url 요청", description = "S3 Presigned Url을 요청합니다.") + @PostMapping("/image-upload-request") + PreSignedUrlResponse requestPresignedUrl( + @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String memberId, + + @Valid + @RequestBody + PreSignedUrlRequest request + ); + + @Operation(summary = "자신의 리얼 이모지 추가", description = "자신의 리얼 이모지를 추가합니다.") + @PostMapping + DefaultResponse createMyRealEmoji( + @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String memberId, + + @Valid + @RequestBody + CreateMyRealEmojiRequest request + ); + + @Operation(summary = "자신의 리얼 이모지 변경", description = "자신의 리얼 이모지 사진을 변경합니다.") + @PutMapping("/{realEmojiId}") + DefaultResponse changeMyRealEmoji( + @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String memberId, + + @Parameter(description = "리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String realEmojiId, + + @Valid + @RequestBody + CreateMyRealEmojiRequest request + ); + + @Operation(summary = "회원의 리얼 이모지 조회", description = "자신의 리얼 이모지를 조회합니다.") + @GetMapping + RealEmojisResponse getMyRealEmojis( + @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String memberId + ); +} diff --git a/post/src/test/java/com/oing/domain/model/MemberPostCommentTest.java b/post/src/test/java/com/oing/domain/model/MemberPostCommentTest.java index 76ecd91b..849b54b5 100644 --- a/post/src/test/java/com/oing/domain/model/MemberPostCommentTest.java +++ b/post/src/test/java/com/oing/domain/model/MemberPostCommentTest.java @@ -22,7 +22,7 @@ void testMemberPostCommentConstructorAndGetters() { String commentId = "sampleCommentId"; String commentContents = "sampleCommentContents"; MemberPost post = new MemberPost(postId, memberId, imageUrl, imageKey, content, 0, - 0, null, null); + 0, 0, null, null, null); // When MemberPostComment comment = new MemberPostComment(commentId, post, memberId, commentContents); diff --git a/post/src/test/java/com/oing/domain/model/MemberPostReactionTest.java b/post/src/test/java/com/oing/domain/model/MemberPostReactionTest.java index 7d6b7e69..e0384e4d 100644 --- a/post/src/test/java/com/oing/domain/model/MemberPostReactionTest.java +++ b/post/src/test/java/com/oing/domain/model/MemberPostReactionTest.java @@ -23,7 +23,7 @@ void testMemberPostReactionConstructorAndGetters() { String reactionId = "sampleCommentId"; Emoji emoji = Emoji.EMOJI_1; MemberPost post = new MemberPost(postId, memberId, imageUrl, imageKey, content, 0, - 0, null, null); + 0, 0, null, null, null); // When MemberPostReaction reaction = new MemberPostReaction(reactionId, post, memberId, emoji); diff --git a/post/src/test/java/com/oing/domain/model/MemberPostTest.java b/post/src/test/java/com/oing/domain/model/MemberPostTest.java index e08e6aae..f8a8dd91 100644 --- a/post/src/test/java/com/oing/domain/model/MemberPostTest.java +++ b/post/src/test/java/com/oing/domain/model/MemberPostTest.java @@ -23,7 +23,7 @@ void testMemberPostConstructorAndGetters() { // When MemberPost post = new MemberPost(postId, memberId, imageUrl, imageKey, content, 0, - 0, null, null); + 0, 0, null, null, null); // Then assertNotNull(post); From b27ab5e58c6ea301d3868403997e568416f2d4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Tue, 16 Jan 2024 20:39:47 +0900 Subject: [PATCH 10/49] fix: fix comment saving issue --- .../java/com/oing/controller/MemberPostCommentController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/post/src/main/java/com/oing/controller/MemberPostCommentController.java b/post/src/main/java/com/oing/controller/MemberPostCommentController.java index 1244cebc..e3c08c37 100644 --- a/post/src/main/java/com/oing/controller/MemberPostCommentController.java +++ b/post/src/main/java/com/oing/controller/MemberPostCommentController.java @@ -51,6 +51,7 @@ public PostCommentResponse createPostComment(String postId, CreatePostCommentReq memberId, request.content() ); + memberPostCommentService.savePostComment(memberPostComment); MemberPostComment addedComment = memberPost.addComment(memberPostComment); return PostCommentResponse.from(addedComment); } @@ -75,6 +76,7 @@ public DefaultResponse deletePostComment(String postId, String commentId) { throw new AuthorizationFailedException(); } + memberPostCommentService.deletePostComment(memberPostComment); memberPost.removeComment(memberPostComment); return DefaultResponse.ok(); } From 321b5788b7ffe1e8323f5bbee8f1dca72fa9e24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC=20=28YeongMin=20Song=29?= Date: Tue, 16 Jan 2024 21:08:13 +0900 Subject: [PATCH 11/49] =?UTF-8?q?[OING-144]=20feat:=20=EA=B0=80=EC=A1=B1?= =?UTF-8?q?=20=ED=83=88=ED=87=B4=20API=20=EC=B6=94=EA=B0=80=20(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/oing/controller/MeController.java | 12 ++++++++++++ gateway/src/main/java/com/oing/restapi/MeApi.java | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/gateway/src/main/java/com/oing/controller/MeController.java b/gateway/src/main/java/com/oing/controller/MeController.java index 59775c88..cb021be0 100644 --- a/gateway/src/main/java/com/oing/controller/MeController.java +++ b/gateway/src/main/java/com/oing/controller/MeController.java @@ -11,6 +11,7 @@ import com.oing.exception.AlreadyInFamilyException; import com.oing.exception.DomainException; import com.oing.exception.ErrorCode; +import com.oing.exception.FamilyNotFoundException; import com.oing.restapi.MeApi; import com.oing.service.FamilyInviteLinkService; import com.oing.service.FamilyService; @@ -83,4 +84,15 @@ public FamilyResponse createFamilyAndJoin() { member.setFamilyId(family.getId()); return FamilyResponse.of(family); } + + @Transactional + @Override + public DefaultResponse quitFamily() { + String memberId = authenticationHolder.getUserId(); + Member member = memberService.findMemberById(memberId); + if (!member.hasFamily()) throw new FamilyNotFoundException(); + member.setFamilyId(null); + + return DefaultResponse.ok(); + } } diff --git a/gateway/src/main/java/com/oing/restapi/MeApi.java b/gateway/src/main/java/com/oing/restapi/MeApi.java index 1d8e0652..65201126 100644 --- a/gateway/src/main/java/com/oing/restapi/MeApi.java +++ b/gateway/src/main/java/com/oing/restapi/MeApi.java @@ -54,4 +54,8 @@ FamilyResponse joinFamily( @Operation(summary = "가족 생성 및 가입", description = "가족을 생성하고 가입합니다.") @PostMapping("/create-family") FamilyResponse createFamilyAndJoin(); + + @Operation(summary = "가족 탈퇴", description = "가족을 탈퇴합니다.") + @PostMapping("/quit-family") + DefaultResponse quitFamily(); } From 39ce1a8a820700b60c305090ebfa262af32a4aa7 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Wed, 17 Jan 2024 11:09:41 +0900 Subject: [PATCH 12/49] =?UTF-8?q?[OING-105]=20feat:=20=EC=BA=98=EB=A6=B0?= =?UTF-8?q?=EB=8D=94=20API=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add the integration test for weekly calendarApiTest * feat: Add edge case of the integration test at CalendarApiTest * feat: Add unit test for CalendarController * feat: Add DataJpaTest for MemberPostRepositoryCustom methods for calendar * refactor: Change variable name from User to Member --- .../java/com/oing/QueryDslTestConfig.java | 19 ++ .../oing/controller/CalendarController.java | 21 +- ...va => MemberPostRepositoryCustomImpl.java} | 3 +- .../java/com/oing/restapi/CalendarApi.java | 2 +- .../src/main/resources/application-test.yaml | 3 +- .../controller/CalendarControllerTest.java | 243 ++++++++++++++++++ .../MemberPostRepositoryCustomTest.java | 113 ++++++++ .../com/oing/restapi/CalendarApiTest.java | 192 ++++++++++++-- 8 files changed, 565 insertions(+), 31 deletions(-) create mode 100644 common/src/test/java/com/oing/QueryDslTestConfig.java rename gateway/src/main/java/com/oing/repository/{MemberPostRepositoryImpl.java => MemberPostRepositoryCustomImpl.java} (96%) create mode 100644 gateway/src/test/java/com/oing/controller/CalendarControllerTest.java create mode 100644 gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java diff --git a/common/src/test/java/com/oing/QueryDslTestConfig.java b/common/src/test/java/com/oing/QueryDslTestConfig.java new file mode 100644 index 00000000..0d2af9a9 --- /dev/null +++ b/common/src/test/java/com/oing/QueryDslTestConfig.java @@ -0,0 +1,19 @@ +package com.oing; + +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 QueryDslTestConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/gateway/src/main/java/com/oing/controller/CalendarController.java b/gateway/src/main/java/com/oing/controller/CalendarController.java index 81c0e209..2e3e87a3 100644 --- a/gateway/src/main/java/com/oing/controller/CalendarController.java +++ b/gateway/src/main/java/com/oing/controller/CalendarController.java @@ -10,9 +10,13 @@ import com.oing.service.MemberService; import com.oing.util.OptimizedImageUrlGenerator; import lombok.RequiredArgsConstructor; +import org.springframework.cglib.core.Local; import org.springframework.stereotype.Controller; +import java.time.DayOfWeek; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.WeekFields; import java.util.List; import java.util.stream.IntStream; @@ -64,10 +68,15 @@ private List getCalendarResponses(List familyIds, Loca } @Override - public ArrayResponse getWeeklyCalendar(String yearMonth, Long week) { - List familyIds = getFamilyIds(); - LocalDate startDate = LocalDate.parse(yearMonth + "-01").plusWeeks(week - 1); + public ArrayResponse getWeeklyCalendar(String yearMonth, Integer week) { + if (yearMonth == null) yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); + if (week == null) week = LocalDate.now().get(WeekFields.of(DayOfWeek.MONDAY, 1).weekOfMonth()); + + // 1주 = 해당 주차 (+ 0), 2주 이상 = 주차 추가 (+ (week - 1)) + LocalDate startDate = LocalDate.parse(yearMonth + "-01").plusWeeks(week - 1); // yyyy-MM-dd 패턴으로 파싱 LocalDate endDate = startDate.plusWeeks(1); + List familyIds = getFamilyIds(); + List calendarResponses = getCalendarResponses(familyIds, startDate, endDate); return new ArrayResponse<>(calendarResponses); @@ -75,9 +84,11 @@ public ArrayResponse getWeeklyCalendar(String yearMonth, Long @Override public ArrayResponse getMonthlyCalendar(String yearMonth) { - List familyIds = getFamilyIds(); - LocalDate startDate = LocalDate.parse(yearMonth + "-01"); + if (yearMonth == null) yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); + + LocalDate startDate = LocalDate.parse(yearMonth + "-01"); // yyyy-MM-dd 패턴으로 파싱 LocalDate endDate = startDate.plusMonths(1); + List familyIds = getFamilyIds(); List calendarResponses = getCalendarResponses(familyIds, startDate, endDate); return new ArrayResponse<>(calendarResponses); diff --git a/gateway/src/main/java/com/oing/repository/MemberPostRepositoryImpl.java b/gateway/src/main/java/com/oing/repository/MemberPostRepositoryCustomImpl.java similarity index 96% rename from gateway/src/main/java/com/oing/repository/MemberPostRepositoryImpl.java rename to gateway/src/main/java/com/oing/repository/MemberPostRepositoryCustomImpl.java index 00e2549a..a89e4783 100644 --- a/gateway/src/main/java/com/oing/repository/MemberPostRepositoryImpl.java +++ b/gateway/src/main/java/com/oing/repository/MemberPostRepositoryCustomImpl.java @@ -2,7 +2,6 @@ import com.oing.domain.MemberPost; import com.oing.domain.MemberPostDailyCalendarDTO; -import com.oing.exception.FamilyNotFoundException; import com.querydsl.core.QueryResults; import com.querydsl.core.types.Ops; import com.querydsl.core.types.Projections; @@ -21,7 +20,7 @@ import static com.oing.domain.QMemberPost.memberPost; @RequiredArgsConstructor -public class MemberPostRepositoryImpl implements MemberPostRepositoryCustom { +public class MemberPostRepositoryCustomImpl implements MemberPostRepositoryCustom { private final JPAQueryFactory queryFactory; diff --git a/gateway/src/main/java/com/oing/restapi/CalendarApi.java b/gateway/src/main/java/com/oing/restapi/CalendarApi.java index bd1d06dd..664efa43 100644 --- a/gateway/src/main/java/com/oing/restapi/CalendarApi.java +++ b/gateway/src/main/java/com/oing/restapi/CalendarApi.java @@ -33,7 +33,7 @@ ArrayResponse getWeeklyCalendar( @RequestParam(required = false) @Parameter(description = "조회할 주차", example = "1") - Long week + Integer week ); @Operation(summary = "월별 캘린더 조회", description = "월별 캘린더를 조회합니다.") diff --git a/gateway/src/main/resources/application-test.yaml b/gateway/src/main/resources/application-test.yaml index 6318ef8a..315bb99b 100644 --- a/gateway/src/main/resources/application-test.yaml +++ b/gateway/src/main/resources/application-test.yaml @@ -10,11 +10,12 @@ spring: ddl-auto: create-drop properties: hibernate: - dialect: org.hibernate.dialect.H2Dialect create_empty_composites: enabled: true show_sql: false format_sql: false + dialect: org.hibernate.dialect.MySQL8Dialect + database-platform: org.hibernate.dialect.MySQL8Dialect app: external-urls: slack-webhook: https://www.naver.com # Must Be Replaced diff --git a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java new file mode 100644 index 00000000..69cd0ae7 --- /dev/null +++ b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java @@ -0,0 +1,243 @@ +package com.oing.controller; + +import com.oing.component.TokenAuthenticationHolder; +import com.oing.domain.Member; +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostDailyCalendarDTO; +import com.oing.dto.response.ArrayResponse; +import com.oing.dto.response.CalendarResponse; +import com.oing.service.MemberPostService; +import com.oing.service.MemberService; +import com.oing.util.OptimizedImageUrlGenerator; +import org.assertj.core.groups.Tuple; +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.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class CalendarControllerTest { + + @InjectMocks + private CalendarController calendarController; + + @Mock + private MemberService memberService; + @Mock + private MemberPostService memberPostService; + @Mock + private TokenAuthenticationHolder tokenAuthenticationHolder; + @Mock + private OptimizedImageUrlGenerator optimizedImageUrlGenerator; + + + private final Member testMember1 = new Member( + "testMember1", + "testFamily", + LocalDate.of(1999, 10, 18), + "testMember1", + "profile.com/1", + "1" + ); + + private final Member testMember2 = new Member( + "testMember2", + "testFamily", + LocalDate.of(1999, 10, 18), + "testMember2", + "profile.com/2", + "2" + ); + + private final List familyIds = List.of(testMember1.getId(), testMember2.getId()); + + + @Test + void 주간_캘린더_조회_테스트() { + // Given + String yearMonth = "2023-11"; + Integer week = 1; + + LocalDate startDate = LocalDate.of(2023, 11, 1); + LocalDate endDate = startDate.plusWeeks(1); + MemberPost testPost1 = new MemberPost( + "1", + testMember1.getId(), + "post.com/1", + "1", + "test1" + ); + ReflectionTestUtils.setField(testPost1, "createdAt", LocalDateTime.of(2023, 11, 1, 13, 0)); + MemberPost testPost2 = new MemberPost( + "2", + testMember2.getId(), + "post.com/2", + "2", + "test2" + ); + ReflectionTestUtils.setField(testPost2, "createdAt", LocalDateTime.of(2023, 11, 2, 13, 0)); + List representativePosts = List.of(testPost1, testPost2); + List calendarDTOs = List.of( + new MemberPostDailyCalendarDTO(2L), + new MemberPostDailyCalendarDTO(1L) + ); + when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); + when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); + when(memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate)).thenReturn(representativePosts); + when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); + + // When + ArrayResponse weeklyCalendar = calendarController.getWeeklyCalendar(yearMonth, week); + + // Then + assertThat(weeklyCalendar.results()) + .extracting(CalendarResponse::representativePostId, CalendarResponse::allFamilyMembersUploaded) + .containsExactly( + Tuple.tuple("1", true), + Tuple.tuple("2", false) + ); + } + + @Test + void 주간_캘린더_파라미터_없이_조회_테스트() { + // Given + LocalDate startDate = LocalDate.now(); + LocalDate endDate = startDate.plusWeeks(1); + MemberPost testPost1 = new MemberPost( + "1", + testMember1.getId(), + "post.com/1", + "1", + "test1" + ); + ReflectionTestUtils.setField(testPost1, "createdAt", LocalDateTime.now()); + List representativePosts = List.of(testPost1); + List calendarDTOs = List.of( + new MemberPostDailyCalendarDTO(1L) + ); + when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); + when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); + when(memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate)).thenReturn(representativePosts); + when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); + + // When + ArrayResponse weeklyCalendar = calendarController.getWeeklyCalendar(null, null); + + // Then + assertThat(weeklyCalendar.results()) + .extracting(CalendarResponse::representativePostId, CalendarResponse::allFamilyMembersUploaded) + .containsExactly( + Tuple.tuple("1", false) + ); + } + + @Test + void 월별_캘린더_조회_테스트() { + // Given + String yearMonth = "2023-11"; + + LocalDate startDate = LocalDate.of(2023, 11, 1); + LocalDate endDate = startDate.plusMonths(1); + MemberPost testPost1 = new MemberPost( + "1", + testMember1.getId(), + "post.com/1", + "1", + "test1" + ); + ReflectionTestUtils.setField(testPost1, "createdAt", LocalDateTime.of(2023, 11, 1, 13, 0)); + MemberPost testPost2 = new MemberPost( + "2", + testMember2.getId(), + "post.com/2", + "2", + "test2" + ); + ReflectionTestUtils.setField(testPost2, "createdAt", LocalDateTime.of(2023, 11, 2, 13, 0)); + MemberPost testPost3 = new MemberPost( + "3", + testMember1.getId(), + "post.com/3", + "3", + "test3" + ); + ReflectionTestUtils.setField(testPost3, "createdAt", LocalDateTime.of(2023, 11, 8, 13, 0)); + MemberPost testPost4 = new MemberPost( + "4", + testMember2.getId(), + "post.com/4", + "4", + "test4" + ); + ReflectionTestUtils.setField(testPost4, "createdAt", LocalDateTime.of(2023, 11, 9, 13, 0)); + List representativePosts = List.of(testPost1, testPost2, testPost3, testPost4); + List calendarDTOs = List.of( + new MemberPostDailyCalendarDTO(2L), + new MemberPostDailyCalendarDTO(1L), + new MemberPostDailyCalendarDTO(2L), + new MemberPostDailyCalendarDTO(1L) + ); + when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); + when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); + when(memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate)).thenReturn(representativePosts); + when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); + + // When + ArrayResponse weeklyCalendar = calendarController.getMonthlyCalendar(yearMonth); + + // Then + assertThat(weeklyCalendar.results()) + .extracting(CalendarResponse::representativePostId, CalendarResponse::allFamilyMembersUploaded) + .containsExactly( + Tuple.tuple("1", true), + Tuple.tuple("2", false), + Tuple.tuple("3", true), + Tuple.tuple("4", false) + ); + } + + @Test + void 월별_캘린더_파라미터_없이_조회_테스트() { + // Given + LocalDate startDate = LocalDate.parse(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")) + "-01"); + LocalDate endDate = startDate.plusMonths(1); + MemberPost testPost1 = new MemberPost( + "1", + testMember1.getId(), + "post.com/1", + "1", + "test1" + ); + ReflectionTestUtils.setField(testPost1, "createdAt", LocalDateTime.now()); + List representativePosts = List.of(testPost1); + List calendarDTOs = List.of( + new MemberPostDailyCalendarDTO(1L) + ); + when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); + when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); + when(memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate)).thenReturn(representativePosts); + when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); + + // When + ArrayResponse weeklyCalendar = calendarController.getMonthlyCalendar(null); + + // Then + assertThat(weeklyCalendar.results()) + .extracting(CalendarResponse::representativePostId, CalendarResponse::allFamilyMembersUploaded) + .containsExactly( + Tuple.tuple("1", false) + ); + } +} diff --git a/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java b/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java new file mode 100644 index 00000000..1892a5b6 --- /dev/null +++ b/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java @@ -0,0 +1,113 @@ +package com.oing.repository; + +import com.oing.config.QuerydslConfig; +import com.oing.domain.Family; +import com.oing.domain.Member; +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostDailyCalendarDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // application-test.yaml의 데이터베이스 설정을 적용하기 위해서 필수 +@ActiveProfiles("test") +@Import(QuerydslConfig.class) +class MemberPostRepositoryCustomTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private MemberPostRepositoryCustomImpl memberPostRepositoryCustomImpl; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private FamilyRepository familyRepository; + + + private final Member testMember1 = new Member( + "testMember1", + "testFamily", + LocalDate.of(1999, 10, 18), + "testMember1", + "profile.com/1", + "1" + ); + + private final Member testMember2 = new Member( + "testMember2", + "testFamily", + LocalDate.of(1999, 10, 18), + "testMember2", + "profile.com/2", + "2" + ); + + private final Member testMember3 = new Member( + "testMember3", + "otherFamily", + LocalDate.of(1999, 10, 18), + "testMember3", + "profile.com/3", + "2" + ); + + private final List familyIds = List.of(testMember1.getId(), testMember2.getId()); + + + @BeforeEach + void setup() { + // Family & Members + familyRepository.save(new Family("testFamily")); + memberRepository.save(testMember1); + memberRepository.save(testMember2); + memberRepository.save(testMember3); + + // Posts + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('1', '" + testMember1.getId() + "', 'https://storage.com/images/1', 0, 0, '2023-11-01 14:00:00', '2023-11-01 14:00:00', 'post1111', '1');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('2', '" + testMember2.getId() + "', 'https://storage.com/images/2', 0, 0, '2023-11-01 15:00:00', '2023-11-01 15:00:00', 'post2222', '2');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('3', '" + testMember3.getId() + "', 'https://storage.com/images/3', 0, 0, '2023-11-01 17:00:00', '2023-11-01 17:00:00', 'post3333', '3');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('4', '" + testMember1.getId() + "', 'https://storage.com/images/4', 0, 0, '2023-11-02 14:00:00', '2023-11-02 14:00:00', 'post4444', '4');"); + } + + + @Test + void 각_날짜에서_가장_마지막으로_업로드된_게시글을_조회한다() { + // When + List posts = memberPostRepositoryCustomImpl.findLatestPostOfEveryday(familyIds, LocalDateTime.of(2023, 11, 1, 0, 0, 0), LocalDateTime.of(2023, 12, 1, 0, 0, 0)); + + // Then + assertThat(posts) + .extracting(MemberPost::getId) + .containsExactly("2", "4"); + } + + @Test + void 데일리_게시글_캘린더를_구성하기_위한_정보를_조회한다() { + // when + List postDailyCalendarDTOs = memberPostRepositoryCustomImpl.findPostDailyCalendarDTOs(familyIds, LocalDateTime.of(2023, 11, 1, 0, 0, 0), LocalDateTime.of(2023, 12, 1, 0, 0, 0)); + + // Then + assertThat(postDailyCalendarDTOs) + .extracting(MemberPostDailyCalendarDTO::dailyPostCount) + .containsExactly(2L, 1L); + } +} diff --git a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java index 2b61d225..fac636ca 100644 --- a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java @@ -19,6 +19,8 @@ import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -49,12 +51,12 @@ class CalendarApiTest { @Autowired private TokenGenerator tokenGenerator; - private String TEST_USER1_ID; - private String TEST_USER1_TOKEN; - private String TEST_USER2_ID; - private String TEST_USER2_TOKEN; - private String TEST_USER3_ID; - private String TEST_USER3_TOKEN; + private String TEST_MEMBER1_ID; + private String TEST_MEMBER1_TOKEN; + private String TEST_MEMBER2_ID; + private String TEST_MEMBER2_TOKEN; + private String TEST_MEMBER3_ID; + private String TEST_MEMBER3_TOKEN; private List TEST_FAMILIES_IDS; @Value("${cloud.ncp.image-optimizer-cdn}") @@ -65,7 +67,7 @@ class CalendarApiTest { @BeforeEach void setUp() { - TEST_USER1_ID = memberService.createNewMember( + TEST_MEMBER1_ID = memberService.createNewMember( new CreateNewUserDTO( SocialLoginProvider.fromString("APPLE"), "testUser1", @@ -74,9 +76,9 @@ void setUp() { "profile.com" ) ).getId(); - TEST_USER1_TOKEN = tokenGenerator.generateTokenPair(TEST_USER1_ID).accessToken(); + TEST_MEMBER1_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER1_ID).accessToken(); - TEST_USER2_ID = memberService.createNewMember( + TEST_MEMBER2_ID = memberService.createNewMember( new CreateNewUserDTO( SocialLoginProvider.fromString("APPLE"), "testUser2", @@ -85,9 +87,9 @@ void setUp() { "profile.com" ) ).getId(); - TEST_USER2_TOKEN = tokenGenerator.generateTokenPair(TEST_USER2_ID).accessToken(); + TEST_MEMBER2_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER2_ID).accessToken(); - TEST_USER3_ID = memberService.createNewMember( + TEST_MEMBER3_ID = memberService.createNewMember( new CreateNewUserDTO( SocialLoginProvider.fromString("APPLE"), "testUser3", @@ -96,12 +98,103 @@ void setUp() { "profile.com" ) ).getId(); - TEST_USER3_TOKEN = tokenGenerator.generateTokenPair(TEST_USER3_ID).accessToken(); + TEST_MEMBER3_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER3_ID).accessToken(); - TEST_FAMILIES_IDS = List.of(TEST_USER1_ID, TEST_USER2_ID); + TEST_FAMILIES_IDS = List.of(TEST_MEMBER1_ID, TEST_MEMBER2_ID); } + @Test + void 주간_캘린더_조회_테스트() throws Exception { + // Given + // parameters + String yearMonth = "2023-11"; + Long week = 1L; + + // posts + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '2023-11-01 14:00:00', '2023-11-01 14:00:00', 'post1111', '1');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('2', '" + TEST_MEMBER2_ID + "', 'https://storage.com/images/2', 0, 0, '2023-11-01 15:00:00', '2023-11-01 15:00:00', 'post2222', '2');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('3', '" + TEST_MEMBER3_ID + "', 'https://storage.com/images/3', 0, 0, '2023-11-01 17:00:00', '2023-11-01 17:00:00', 'post3333', '3');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('4', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/4', 0, 0, '2023-11-02 14:00:00', '2023-11-02 14:00:00', 'post4444', '4');"); + + // family + String familyId = objectMapper.readValue( + mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), FamilyResponse.class + ).familyId(); + String inviteCode = objectMapper.readValue( + mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class + ).getLinkId(); + mockMvc.perform(post("/v1/me/join-family") + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) + ).andExpect(status().isOk()); + + + // When & Then + mockMvc.perform(get("/v1/calendar") + .param("type", "WEEKLY") + .param("yearMonth", yearMonth) + .param("week", week.toString()) + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].date").value("2023-11-01")) + .andExpect(jsonPath("$.results[0].representativePostId").value("2")) + .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/2" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[0].allFamilyMembersUploaded").value(true)) + .andExpect(jsonPath("$.results[1].date").value("2023-11-02")) + .andExpect(jsonPath("$.results[1].representativePostId").value("4")) + .andExpect(jsonPath("$.results[1].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/4" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[1].allFamilyMembersUploaded").value(false)); + } + + @Test + void 주간_캘린더_파라미터_없이_조회_테스트() throws Exception { + // Given + // posts + String now = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '" + now + "', '" + now + "', 'post1111', '1');"); + + // family + String familyId = objectMapper.readValue( + mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), FamilyResponse.class + ).familyId(); + String inviteCode = objectMapper.readValue( + mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class + ).getLinkId(); + mockMvc.perform(post("/v1/me/join-family") + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) + ).andExpect(status().isOk()); + + + // When & Then + mockMvc.perform(get("/v1/calendar") + .param("type", "WEEKLY") + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].date").value(LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.results[0].representativePostId").value("1")) + .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/1" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[0].allFamilyMembersUploaded").value(false)); + } + @Test void 월별_캘린더_조회_테스트() throws Exception { // Given @@ -110,27 +203,35 @@ void setUp() { // posts jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('1', '" + TEST_USER1_ID + "', 'https://storage.com/images/1', 0, 0, '2023-11-01 14:00:00', '2023-11-01 14:00:00', 'post1111', '1');"); + "values ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '2023-11-01 14:00:00', '2023-11-01 14:00:00', 'post1111', '1');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('2', '" + TEST_MEMBER2_ID + "', 'https://storage.com/images/2', 0, 0, '2023-11-01 15:00:00', '2023-11-01 15:00:00', 'post2222', '2');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('3', '" + TEST_MEMBER3_ID + "', 'https://storage.com/images/3', 0, 0, '2023-11-01 17:00:00', '2023-11-01 17:00:00', 'post3333', '3');"); jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('2', '" + TEST_USER2_ID + "', 'https://storage.com/images/2', 0, 0, '2023-11-01 15:00:00', '2023-11-01 15:00:00', 'post2222', '2');"); + "values ('4', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/4', 0, 0, '2023-11-02 14:00:00', '2023-11-02 14:00:00', 'post4444', '4');"); jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('3', '" + TEST_USER3_ID + "', 'https://storage.com/images/3', 0, 0, '2023-11-01 17:00:00', '2023-11-01 17:00:00', 'post3333', '3');"); + "values ('5', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/5', 0, 0, '2023-11-29 14:00:00', '2023-11-29 14:00:00', 'post5555', '5');"); jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('4', '" + TEST_USER1_ID + "', 'https://storage.com/images/4', 0, 0, '2023-11-02 14:00:00', '2023-11-02 14:00:00', 'post4444', '4');"); + "values ('6', '" + TEST_MEMBER2_ID + "', 'https://storage.com/images/6', 0, 0, '2023-11-29 15:00:00', '2023-11-29 15:00:00', 'post6666', '6');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('7', '" + TEST_MEMBER3_ID + "', 'https://storage.com/images/7', 0, 0, '2023-11-29 17:00:00', '2023-11-29 17:00:00', 'post7777', '7');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('8', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/8', 0, 0, '2023-11-30 14:00:00', '2023-11-30 14:00:00', 'post8888', '8');"); // family String familyId = objectMapper.readValue( - mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_USER1_TOKEN)) + mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), FamilyResponse.class ).familyId(); String inviteCode = objectMapper.readValue( - mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_USER1_TOKEN)) + mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class ).getLinkId(); mockMvc.perform(post("/v1/me/join-family") - .header("X-AUTH-TOKEN", TEST_USER2_TOKEN) + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) ).andExpect(status().isOk()); @@ -140,7 +241,7 @@ void setUp() { mockMvc.perform(get("/v1/calendar") .param("type", "MONTHLY") .param("yearMonth", yearMonth) - .header("X-AUTH-TOKEN", TEST_USER1_TOKEN) + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.results[0].date").value("2023-11-01")) @@ -150,6 +251,53 @@ void setUp() { .andExpect(jsonPath("$.results[1].date").value("2023-11-02")) .andExpect(jsonPath("$.results[1].representativePostId").value("4")) .andExpect(jsonPath("$.results[1].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/4" + thumbnailOptimizerQuery)) - .andExpect(jsonPath("$.results[1].allFamilyMembersUploaded").value(false)); + .andExpect(jsonPath("$.results[1].allFamilyMembersUploaded").value(false)) + .andExpect(jsonPath("$.results[2].date").value("2023-11-29")) + .andExpect(jsonPath("$.results[2].representativePostId").value("6")) + .andExpect(jsonPath("$.results[2].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/6" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[2].allFamilyMembersUploaded").value(true)) + .andExpect(jsonPath("$.results[3].date").value("2023-11-30")) + .andExpect(jsonPath("$.results[3].representativePostId").value("8")) + .andExpect(jsonPath("$.results[3].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/8" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[3].allFamilyMembersUploaded").value(false)); + + } + + @Test + void 월별_캘린더_파라미터_없이_조회_테스트() throws Exception { + // Given + // posts + String now = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '" + now + "', '" + now + "', 'post1111', '1');"); + + // family + String familyId = objectMapper.readValue( + mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), FamilyResponse.class + ).familyId(); + String inviteCode = objectMapper.readValue( + mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class + ).getLinkId(); + mockMvc.perform(post("/v1/me/join-family") + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) + ).andExpect(status().isOk()); + + + // When & Then + mockMvc.perform(get("/v1/calendar") + .param("type", "MONTHLY") + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].date").value(LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.results[0].representativePostId").value("1")) + .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/1" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[0].allFamilyMembersUploaded").value(false)); } } \ No newline at end of file From e774c533e7fa9c3f6924e93c5e74494a2af488e6 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:09:54 +0900 Subject: [PATCH 13/49] =?UTF-8?q?[OING-151]=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EC=9D=98=20=EB=A6=AC=EC=96=BC=EC=9D=B4=EB=AA=A8=EC=A7=80=20POS?= =?UTF-8?q?T/PUT=20API=20=EA=B5=AC=ED=98=84=20(#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add requestPresignedUrl for RealEmoji * feat: add MemberRealEmoji Post/Put API * feat: add MemberRealEmoji post/put api test * feat: add MemberRealEmoji post/put Integration test * fix: fix MemberRealEmojiControllerTest * test: add additional unit test code * style: add line --- .../java/com/oing/exception/ErrorCode.java | 5 + .../com/oing/util/PreSignedUrlGenerator.java | 2 + .../support/S3PreSignedUrlProvider.java | 8 + .../oing/restapi/MemberRealEmojiApiTest.java | 137 ++++++++++++++++++ gateway/src/test/resources/application.yaml | 2 +- .../controller/MemberRealEmojiController.java | 58 +++++++- .../main/java/com/oing/domain/MemberPost.java | 8 +- .../java/com/oing/domain/MemberRealEmoji.java | 5 + .../dto/request/UpdateMyRealEmojiRequest.java | 13 ++ .../oing/dto/response/RealEmojiResponse.java | 5 + .../DuplicateRealEmojiException.java | 7 + .../exception/RealEmojiNotFoundException.java | 7 + .../repository/MemberRealEmojiRepository.java | 5 + .../com/oing/restapi/MemberRealEmojiApi.java | 11 +- .../oing/service/MemberRealEmojiService.java | 31 ++++ .../MemberRealEmojiControllerTest.java | 130 +++++++++++++++++ 16 files changed, 418 insertions(+), 16 deletions(-) create mode 100644 gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java create mode 100644 post/src/main/java/com/oing/dto/request/UpdateMyRealEmojiRequest.java create mode 100644 post/src/main/java/com/oing/exception/DuplicateRealEmojiException.java create mode 100644 post/src/main/java/com/oing/exception/RealEmojiNotFoundException.java create mode 100644 post/src/main/java/com/oing/service/MemberRealEmojiService.java create mode 100644 post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java diff --git a/common/src/main/java/com/oing/exception/ErrorCode.java b/common/src/main/java/com/oing/exception/ErrorCode.java index 22914b7e..fcaf2029 100644 --- a/common/src/main/java/com/oing/exception/ErrorCode.java +++ b/common/src/main/java/com/oing/exception/ErrorCode.java @@ -54,6 +54,11 @@ public enum ErrorCode { INVALID_UPLOAD_TIME("PO0001", "Invalid Upload Time. The request is outside the valid time range" + "(from 12:00 AM yesterday to 12:00 AM today)."), DUPLICATE_POST_UPLOAD("PO0002", "Duplicate Post Upload"), + /** + * Real-Emoji Related Errors + */ + REAL_EMOJI_NOT_FOUND("RE0001", "Real-Emoji not found"), + DUPLICATE_REAL_EMOJI("RE0002", "Duplicate Real Emoji"), /** * Deep Link Related Errors */ diff --git a/common/src/main/java/com/oing/util/PreSignedUrlGenerator.java b/common/src/main/java/com/oing/util/PreSignedUrlGenerator.java index 536f63e9..eb9732e2 100644 --- a/common/src/main/java/com/oing/util/PreSignedUrlGenerator.java +++ b/common/src/main/java/com/oing/util/PreSignedUrlGenerator.java @@ -7,5 +7,7 @@ public interface PreSignedUrlGenerator { PreSignedUrlResponse getProfileImagePreSignedUrl(String imageName); + PreSignedUrlResponse getRealEmojiPreSignedUrl(String imageName); + String extractImageKey(String imageUrl); } diff --git a/gateway/src/main/java/com/oing/config/support/S3PreSignedUrlProvider.java b/gateway/src/main/java/com/oing/config/support/S3PreSignedUrlProvider.java index a345b1f8..c979435d 100644 --- a/gateway/src/main/java/com/oing/config/support/S3PreSignedUrlProvider.java +++ b/gateway/src/main/java/com/oing/config/support/S3PreSignedUrlProvider.java @@ -46,6 +46,14 @@ public PreSignedUrlResponse getProfileImagePreSignedUrl(String imageName) { return new PreSignedUrlResponse(generatePreSignedUrl(generatePresignedUrlRequest)); } + @Override + public PreSignedUrlResponse getRealEmojiPreSignedUrl(String imageName) { + GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest("real-emoji", + imageName); + + return new PreSignedUrlResponse(generatePreSignedUrl(generatePresignedUrlRequest)); + } + private String generatePreSignedUrl(GeneratePresignedUrlRequest generatePresignedUrlRequest) { String preSignedUrl; try { diff --git a/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java new file mode 100644 index 00000000..3f208995 --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java @@ -0,0 +1,137 @@ +package com.oing.restapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.domain.Emoji; +import com.oing.domain.Member; +import com.oing.domain.MemberRealEmoji; +import com.oing.dto.request.CreateMyRealEmojiRequest; +import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.request.UpdateMyRealEmojiRequest; +import com.oing.repository.MemberRealEmojiRepository; +import com.oing.repository.MemberRepository; +import com.oing.service.TokenGenerator; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class MemberRealEmojiApiTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private TokenGenerator tokenGenerator; + @Autowired + private ObjectMapper objectMapper; + + private String TEST_MEMBER_ID = "01HGW2N7EHJVJ4CJ999RRS2E97"; + private String TEST_MEMBER_REAL_EMOJI_ID = "01HGW2N7EHJVJ4CJ999RRS2A97"; + private String TEST_MEMBER_TOKEN; + + @Autowired + private MemberRepository memberRepository; + @Autowired + private MemberRealEmojiRepository memberRealEmojiRepository; + + @BeforeEach + void setUp() { + memberRepository.save( + new Member( + TEST_MEMBER_ID, + "testUser1", + LocalDate.now(), + "", "", "" + ) + ); + TEST_MEMBER_TOKEN = tokenGenerator + .generateTokenPair(TEST_MEMBER_ID) + .accessToken(); + } + + @Test + void 리얼이모지_이미지_업로드_URL_요청_테스트() throws Exception { + //given + String imageName = "realEmoji.jpg"; + + //when + ResultActions resultActions = mockMvc.perform( + post("/v1/members/{memberId}/real-emoji/image-upload-request", TEST_MEMBER_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new PreSignedUrlRequest(imageName))) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.url").exists()); + } + + @Test + void 회원_리얼이모지_추가_테스트() throws Exception { + //given + String realEmojiImageUrl = "https://test.com/bucket/images/realEmoji.jpg"; + Emoji emoji = Emoji.EMOJI_1; + CreateMyRealEmojiRequest request = new CreateMyRealEmojiRequest(emoji.getTypeKey(), realEmojiImageUrl); + + //when + ResultActions resultActions = mockMvc.perform( + post("/v1/members/{memberId}/real-emoji", TEST_MEMBER_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type").value(emoji.getTypeKey())) + .andExpect(jsonPath("$.imageUrl").value(realEmojiImageUrl)); + } + + @Test + void 회원_리얼이모지_수정_테스트() throws Exception { + //given + String realEmojiImageUrl = "https://test.com/bucket/images/realEmoji.jpg"; + UpdateMyRealEmojiRequest request = new UpdateMyRealEmojiRequest(realEmojiImageUrl); + memberRealEmojiRepository.save( + new MemberRealEmoji( + TEST_MEMBER_REAL_EMOJI_ID, + TEST_MEMBER_ID, + Emoji.EMOJI_1, + "https://test.com/bucket/images/defaultEmoji.jpg", + "images/defaultEmoji.jpg" + ) + ); + + //when + ResultActions resultActions = mockMvc.perform( + put("/v1/members/{memberId}/real-emoji/{realEmojiId}", TEST_MEMBER_ID, TEST_MEMBER_REAL_EMOJI_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.imageUrl").value(realEmojiImageUrl)); + } +} diff --git a/gateway/src/test/resources/application.yaml b/gateway/src/test/resources/application.yaml index e74595d1..dd37bc2a 100644 --- a/gateway/src/test/resources/application.yaml +++ b/gateway/src/test/resources/application.yaml @@ -14,7 +14,7 @@ app: cloud: ncp: region: test - end-point: test + end-point: https://test.com access-key: access-key secret-key: secret-key storage: diff --git a/post/src/main/java/com/oing/controller/MemberRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java index de94bb27..e90b77f3 100644 --- a/post/src/main/java/com/oing/controller/MemberRealEmojiController.java +++ b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java @@ -1,12 +1,22 @@ package com.oing.controller; +import com.oing.domain.Emoji; +import com.oing.domain.MemberRealEmoji; import com.oing.dto.request.CreateMyRealEmojiRequest; import com.oing.dto.request.PreSignedUrlRequest; -import com.oing.dto.response.DefaultResponse; +import com.oing.dto.request.UpdateMyRealEmojiRequest; import com.oing.dto.response.PreSignedUrlResponse; +import com.oing.dto.response.RealEmojiResponse; import com.oing.dto.response.RealEmojisResponse; +import com.oing.exception.AuthorizationFailedException; +import com.oing.exception.DuplicateRealEmojiException; import com.oing.restapi.MemberRealEmojiApi; +import com.oing.service.MemberRealEmojiService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +import com.oing.util.PreSignedUrlGenerator; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; @@ -14,23 +24,59 @@ @Controller public class MemberRealEmojiController implements MemberRealEmojiApi { + private final AuthenticationHolder authenticationHolder; + private final IdentityGenerator identityGenerator; + private final PreSignedUrlGenerator preSignedUrlGenerator; + private final MemberRealEmojiService memberRealEmojiService; + + @Transactional @Override public PreSignedUrlResponse requestPresignedUrl(String memberId, PreSignedUrlRequest request) { - return new PreSignedUrlResponse("https://test/2021-08-22/real-emoji-1.jpg"); + validateMemberId(memberId); + String imageName = request.imageName(); + return preSignedUrlGenerator.getRealEmojiPreSignedUrl(imageName); } + @Transactional @Override - public DefaultResponse createMyRealEmoji(String memberId, CreateMyRealEmojiRequest request) { - return new DefaultResponse(true); + public RealEmojiResponse createMyRealEmoji(String memberId, CreateMyRealEmojiRequest request) { + validateMemberId(memberId); + String emojiId = identityGenerator.generateIdentity(); + String emojiImgKey = preSignedUrlGenerator.extractImageKey(request.imageUrl()); + Emoji emoji = Emoji.fromString(request.type()); + if (isExistsSameRealEmojiType(emoji)) { + throw new DuplicateRealEmojiException(); + } + + MemberRealEmoji realEmoji = new MemberRealEmoji(emojiId, memberId, emoji, request.imageUrl(), emojiImgKey); + MemberRealEmoji addedRealEmoji = memberRealEmojiService.save(realEmoji); + return RealEmojiResponse.from(addedRealEmoji); + } + + private boolean isExistsSameRealEmojiType(Emoji emoji) { + return memberRealEmojiService.findRealEmojiByEmojiType(emoji); } + @Transactional @Override - public DefaultResponse changeMyRealEmoji(String memberId, String realEmojiId, CreateMyRealEmojiRequest request) { - return new DefaultResponse(true); + public RealEmojiResponse changeMyRealEmoji(String memberId, String realEmojiId, UpdateMyRealEmojiRequest request) { + validateMemberId(memberId); + String emojiImgKey = preSignedUrlGenerator.extractImageKey(request.imageUrl()); + + MemberRealEmoji findEmoji = memberRealEmojiService.findRealEmojiById(realEmojiId); + findEmoji.updateRealEmoji(request.imageUrl(), emojiImgKey); + return RealEmojiResponse.from(findEmoji); } @Override public RealEmojisResponse getMyRealEmojis(String memberId) { return new RealEmojisResponse(null); } + + private void validateMemberId(String memberId) { + String loginId = authenticationHolder.getUserId(); + if (!loginId.equals(memberId)) { + throw new AuthorizationFailedException(); + } + } } diff --git a/post/src/main/java/com/oing/domain/MemberPost.java b/post/src/main/java/com/oing/domain/MemberPost.java index 009e3ae4..bccdac6d 100644 --- a/post/src/main/java/com/oing/domain/MemberPost.java +++ b/post/src/main/java/com/oing/domain/MemberPost.java @@ -71,22 +71,22 @@ private void validateContent(String content) { public void addReaction(MemberPostReaction reaction) { this.reactions.add(reaction); - this.reactionCnt += 1; + this.reactionCnt = this.reactions.size(); } public void removeReaction(MemberPostReaction reaction) { this.reactions.remove(reaction); - this.reactionCnt -= 1; + this.reactionCnt = this.reactions.size(); } public void addRealEmoji(MemberPostRealEmoji realEmoji) { this.realEmojis.add(realEmoji); - this.realEmojiCnt += 1; + this.realEmojiCnt = this.realEmojis.size(); } public void removeRealEmoji(MemberPostRealEmoji realEmoji) { this.realEmojis.remove(realEmoji); - this.realEmojiCnt -= 1; + this.realEmojiCnt = this.realEmojis.size(); } public MemberPostComment addComment(MemberPostComment comment) { diff --git a/post/src/main/java/com/oing/domain/MemberRealEmoji.java b/post/src/main/java/com/oing/domain/MemberRealEmoji.java index aa44ef06..75812233 100644 --- a/post/src/main/java/com/oing/domain/MemberRealEmoji.java +++ b/post/src/main/java/com/oing/domain/MemberRealEmoji.java @@ -29,4 +29,9 @@ public class MemberRealEmoji extends BaseAuditEntity { @Column(name = "real_emoji_image_key", nullable = false) private String realEmojiImageKey; + + public void updateRealEmoji(String realEmojiImageUrl, String realEmojiImageKey) { + this.realEmojiImageUrl = realEmojiImageUrl; + this.realEmojiImageKey = realEmojiImageKey; + } } diff --git a/post/src/main/java/com/oing/dto/request/UpdateMyRealEmojiRequest.java b/post/src/main/java/com/oing/dto/request/UpdateMyRealEmojiRequest.java new file mode 100644 index 00000000..186da825 --- /dev/null +++ b/post/src/main/java/com/oing/dto/request/UpdateMyRealEmojiRequest.java @@ -0,0 +1,13 @@ +package com.oing.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "자신의 리얼 이모지 수정 요청") +public record UpdateMyRealEmojiRequest( + + @NotNull + @Schema(description = "리얼 이모지 사진 주소", example = "https://no5ing.com/feed/1.jpg") + String imageUrl +) { +} diff --git a/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java index d1c52db7..7d0369fe 100644 --- a/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java +++ b/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java @@ -1,5 +1,6 @@ package com.oing.dto.response; +import com.oing.domain.MemberRealEmoji; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "자신이 생성한 리얼 이모지 응답") @@ -13,4 +14,8 @@ public record RealEmojiResponse ( @Schema(description = "리얼 이모지 이미지 주소", example = "https://no5ing.com/profile/1.jpg") String imageUrl ){ + public static RealEmojiResponse from(MemberRealEmoji realEmoji) { + return new RealEmojiResponse(realEmoji.getId(), realEmoji.getType().getTypeKey(), + realEmoji.getRealEmojiImageUrl()); + } } diff --git a/post/src/main/java/com/oing/exception/DuplicateRealEmojiException.java b/post/src/main/java/com/oing/exception/DuplicateRealEmojiException.java new file mode 100644 index 00000000..f26d2411 --- /dev/null +++ b/post/src/main/java/com/oing/exception/DuplicateRealEmojiException.java @@ -0,0 +1,7 @@ +package com.oing.exception; + +public class DuplicateRealEmojiException extends DomainException { + public DuplicateRealEmojiException() { + super(ErrorCode.DUPLICATE_REAL_EMOJI); + } +} diff --git a/post/src/main/java/com/oing/exception/RealEmojiNotFoundException.java b/post/src/main/java/com/oing/exception/RealEmojiNotFoundException.java new file mode 100644 index 00000000..54f65323 --- /dev/null +++ b/post/src/main/java/com/oing/exception/RealEmojiNotFoundException.java @@ -0,0 +1,7 @@ +package com.oing.exception; + +public class RealEmojiNotFoundException extends DomainException { + public RealEmojiNotFoundException() { + super(ErrorCode.REAL_EMOJI_NOT_FOUND); + } +} diff --git a/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java index c926bd8b..66585ae1 100644 --- a/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java +++ b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java @@ -1,7 +1,12 @@ package com.oing.repository; +import com.oing.domain.Emoji; import com.oing.domain.MemberRealEmoji; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRealEmojiRepository extends JpaRepository { + + Optional findByType(Emoji emoji); } diff --git a/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java b/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java index 3941a697..9cef8ab8 100644 --- a/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java +++ b/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java @@ -2,9 +2,10 @@ import com.oing.dto.request.CreateMyRealEmojiRequest; import com.oing.dto.request.PreSignedUrlRequest; -import com.oing.dto.response.DefaultResponse; -import com.oing.dto.response.RealEmojisResponse; +import com.oing.dto.request.UpdateMyRealEmojiRequest; import com.oing.dto.response.PreSignedUrlResponse; +import com.oing.dto.response.RealEmojiResponse; +import com.oing.dto.response.RealEmojisResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -31,7 +32,7 @@ PreSignedUrlResponse requestPresignedUrl( @Operation(summary = "자신의 리얼 이모지 추가", description = "자신의 리얼 이모지를 추가합니다.") @PostMapping - DefaultResponse createMyRealEmoji( + RealEmojiResponse createMyRealEmoji( @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable String memberId, @@ -43,7 +44,7 @@ DefaultResponse createMyRealEmoji( @Operation(summary = "자신의 리얼 이모지 변경", description = "자신의 리얼 이모지 사진을 변경합니다.") @PutMapping("/{realEmojiId}") - DefaultResponse changeMyRealEmoji( + RealEmojiResponse changeMyRealEmoji( @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable String memberId, @@ -54,7 +55,7 @@ DefaultResponse changeMyRealEmoji( @Valid @RequestBody - CreateMyRealEmojiRequest request + UpdateMyRealEmojiRequest request ); @Operation(summary = "회원의 리얼 이모지 조회", description = "자신의 리얼 이모지를 조회합니다.") diff --git a/post/src/main/java/com/oing/service/MemberRealEmojiService.java b/post/src/main/java/com/oing/service/MemberRealEmojiService.java new file mode 100644 index 00000000..903b0b43 --- /dev/null +++ b/post/src/main/java/com/oing/service/MemberRealEmojiService.java @@ -0,0 +1,31 @@ +package com.oing.service; + +import com.oing.domain.Emoji; +import com.oing.domain.MemberRealEmoji; +import com.oing.exception.RealEmojiNotFoundException; +import com.oing.repository.MemberRealEmojiRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberRealEmojiService { + + private final MemberRealEmojiRepository memberRealEmojiRepository; + + public MemberRealEmoji save(MemberRealEmoji emoji) { + return memberRealEmojiRepository.save(emoji); + } + + public MemberRealEmoji findRealEmojiById(String realEmojiId) { + return memberRealEmojiRepository + .findById(realEmojiId) + .orElseThrow(RealEmojiNotFoundException::new); + } + + public boolean findRealEmojiByEmojiType(Emoji emoji) { + return memberRealEmojiRepository + .findByType(emoji) + .isPresent(); + } +} diff --git a/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java b/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java new file mode 100644 index 00000000..271b93a6 --- /dev/null +++ b/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java @@ -0,0 +1,130 @@ +package com.oing.controller; + +import com.oing.domain.Emoji; +import com.oing.domain.MemberRealEmoji; +import com.oing.dto.request.CreateMyRealEmojiRequest; +import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.request.UpdateMyRealEmojiRequest; +import com.oing.dto.response.PreSignedUrlResponse; +import com.oing.dto.response.RealEmojiResponse; +import com.oing.exception.AuthorizationFailedException; +import com.oing.exception.DuplicateRealEmojiException; +import com.oing.service.MemberRealEmojiService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +import com.oing.util.PreSignedUrlGenerator; +import jakarta.transaction.Transactional; +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.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@Transactional +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +public class MemberRealEmojiControllerTest { + @InjectMocks + private MemberRealEmojiController memberRealEmojiController; + + @Mock + private AuthenticationHolder authenticationHolder; + @Mock + private IdentityGenerator identityGenerator; + @Mock + private MemberRealEmojiService memberRealEmojiService; + @Mock + private PreSignedUrlGenerator preSignedUrlGenerator; + + @Test + void 리얼이모지_이미지_업로드_URL_요청_테스트() { + // given + String memberId = "1"; + String realEmojiImage = "realEmoji.jpg"; + + // when + when(authenticationHolder.getUserId()).thenReturn(memberId); + PreSignedUrlRequest request = new PreSignedUrlRequest(realEmojiImage); + PreSignedUrlResponse dummyResponse = new PreSignedUrlResponse("https://test.com/presigend-request-url"); + when(preSignedUrlGenerator.getRealEmojiPreSignedUrl(any())).thenReturn(dummyResponse); + PreSignedUrlResponse response = memberRealEmojiController.requestPresignedUrl(memberId, request); + + // then + assertNotNull(response.url()); + } + + @Test + void 회원_리얼이모지_생성_테스트() { + // given + String memberId = "1"; + String realEmojiImageUrl = "https://test.com/realEmoji.jpg"; + Emoji emoji = Emoji.EMOJI_1; + + // when + when(authenticationHolder.getUserId()).thenReturn(memberId); + CreateMyRealEmojiRequest request = new CreateMyRealEmojiRequest(emoji.getTypeKey(), realEmojiImageUrl); + when(memberRealEmojiService.save(any())).thenReturn(new MemberRealEmoji("1", memberId, emoji, + realEmojiImageUrl, "realEmoji.jpg")); + RealEmojiResponse response = memberRealEmojiController.createMyRealEmoji(memberId, request); + + // then + assertEquals(emoji.getTypeKey(), response.type()); + assertEquals(request.imageUrl(), response.imageUrl()); + } + + @Test + void 권한없는_memberId로_리얼이모지_생성_예외_테스트() { + // given + String memberId = "1"; + String realEmojiImageUrl = "https://test.com/realEmoji.jpg"; + Emoji emoji = Emoji.EMOJI_1; + + // when + when(authenticationHolder.getUserId()).thenReturn("2"); + CreateMyRealEmojiRequest request = new CreateMyRealEmojiRequest(emoji.getTypeKey(), realEmojiImageUrl); + + // then + assertThrows(AuthorizationFailedException.class, + () -> memberRealEmojiController.createMyRealEmoji(memberId, request)); + } + + @Test + void 중복된_리얼이모지_생성_예외_테스트() { + // given + String memberId = "1"; + String realEmojiImageUrl = "https://test.com/realEmoji.jpg"; + Emoji emoji = Emoji.EMOJI_1; + + // when + when(authenticationHolder.getUserId()).thenReturn(memberId); + CreateMyRealEmojiRequest request = new CreateMyRealEmojiRequest(emoji.getTypeKey(), realEmojiImageUrl); + when(memberRealEmojiService.findRealEmojiByEmojiType(emoji)).thenReturn(true); + + // then + assertThrows(DuplicateRealEmojiException.class, + () -> memberRealEmojiController.createMyRealEmoji(memberId, request)); + } + + @Test + void 회원_리얼이모지_수정_테스트() { + // given + String memberId = "1"; + String realEmojiId = "1"; + String realEmojiImageUrl = "https://test.com/realEmoji.jpg"; + + // when + when(authenticationHolder.getUserId()).thenReturn(memberId); + UpdateMyRealEmojiRequest request = new UpdateMyRealEmojiRequest(realEmojiImageUrl); + when(memberRealEmojiService.findRealEmojiById(realEmojiId)).thenReturn(new MemberRealEmoji("1", memberId, + Emoji.EMOJI_1, realEmojiImageUrl, "realEmoji.jpg")); + RealEmojiResponse response = memberRealEmojiController.changeMyRealEmoji(memberId, realEmojiId, request); + + // then + assertEquals(request.imageUrl(), response.imageUrl()); + } +} From 326535a610f49387a902665e098066734fd703b9 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Wed, 17 Jan 2024 12:00:29 +0900 Subject: [PATCH 14/49] =?UTF-8?q?docs:=20README,=20LICENSE=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE.md | 20 ++++++++++++++++++++ README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..46737c3a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2012-2024 Scott Chacon and others + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index d009fce6..572b69da 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,56 @@ -## 14th-team-BE +# Bibbi: 하루 한번, 가족에게 보내는 생존 신고 -디프만 14기 팀 백엔드 프로젝트입니다! +` 연락에 대한 부담감과 거부감이 들지 않게 +간편하고 사용하기 쉬운 기능으로 +일상을 공유하게 유도한다 ` -### 환경변수 + + + +
+ + +#### "하루 한 번, 가족과의 소중한 연락!" + +가족은 삶의 중요한 부분이죠. 하지만 빠른 일상에 묻혀 자주 소통하지 못하는 경우가 많습니다. 이제, 삐삐와 함께 '일일 생존 신고' 프로젝트를 시작해보세요! + +매일, 간단한 메세지와 사진을 통해 가족에게 생존을 알리면서 소중한 순간들을 함께 나눌 수 있습니다. 까먹지 않고, 더욱 멋지고 따뜻한 가족 소통의 시작을 만들어보세요. 나중에는 이 작은 노력이 행복한 추억으로 기억될 것입니다. + +가족과의 소중한 시간, 삐삐와 함께라면 언제나 더 특별한 것 같아요! ❤️ + + +> "Once a day, cherish the connection with your family! +Family is an essential part of life, yet amidst the fast-paced routine, meaningful communication often takes a back seat. Now, with the 'Daily Survival Report' project by Pippy, initiate a new era of communication with your loved ones! +Every day, through simple messages and photos, you can share your survival with your family, creating moments of togetherness. Never forget, with Pippy, embark on a journey of stylish and warm family communication. Later on, these small efforts will be remembered as joyful memories. +In the precious time spent with family, everything feels more special with Pippy by your side! ❤️" + + +
+ +### 🎇 Project Contributors + + + + + + + + + +
CChuYong
Yeongmin Song

백엔드 개발(파트장)
CChuYong
Jisoo Lim

백엔드 개발
CChuYong
Soonchan Kwon

백엔드 개발
+ +
+ +### 🖥️ Project Tech Stacks + +- JVM Runtime Amazon Corretto 17 +- SpringBoot 3.1.5 (Servlet MVC) +- Spring Data JPA with QueryDSL +- Stateless Session Management with JWT + Spring Security +- Module Architecture with Gradle Multi-Project +

+ +### 🛠 환경변수 | 이름 | 설명 | |----------------------------|-----------------------------| From d7d046a149604956cf39f696442f39a5d57e04a4 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Wed, 17 Jan 2024 13:23:22 +0900 Subject: [PATCH 15/49] fix: fix saved time issue --- .../java/com/oing/controller/MemberPostCommentController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/post/src/main/java/com/oing/controller/MemberPostCommentController.java b/post/src/main/java/com/oing/controller/MemberPostCommentController.java index e3c08c37..b9f9af27 100644 --- a/post/src/main/java/com/oing/controller/MemberPostCommentController.java +++ b/post/src/main/java/com/oing/controller/MemberPostCommentController.java @@ -51,8 +51,8 @@ public PostCommentResponse createPostComment(String postId, CreatePostCommentReq memberId, request.content() ); - memberPostCommentService.savePostComment(memberPostComment); - MemberPostComment addedComment = memberPost.addComment(memberPostComment); + MemberPostComment savedComment = memberPostCommentService.savePostComment(memberPostComment); + MemberPostComment addedComment = memberPost.addComment(savedComment); return PostCommentResponse.from(addedComment); } From 339aeb1f62369b464aa53c4beb3e810393c91f19 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Wed, 17 Jan 2024 13:26:16 +0900 Subject: [PATCH 16/49] =?UTF-8?q?[OING-156]=20refactor:=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EC=9D=98=20=EC=98=88=EC=83=81=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20url=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20OptimizedImageProvide?= =?UTF-8?q?r=EC=9D=98=20=EB=9F=B0=ED=83=80=EC=9E=84=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=A9=EC=A7=80=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StringEmptyWhiteSpaceException.java | 7 +++++ .../support/OptimizedImageUrlProvider.java | 31 ++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 common/src/main/java/com/oing/exception/StringEmptyWhiteSpaceException.java diff --git a/common/src/main/java/com/oing/exception/StringEmptyWhiteSpaceException.java b/common/src/main/java/com/oing/exception/StringEmptyWhiteSpaceException.java new file mode 100644 index 00000000..75b0a37b --- /dev/null +++ b/common/src/main/java/com/oing/exception/StringEmptyWhiteSpaceException.java @@ -0,0 +1,7 @@ +package com.oing.exception; + +public class StringEmptyWhiteSpaceException extends RuntimeException { + public StringEmptyWhiteSpaceException() { + super(); + } +} diff --git a/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java b/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java index 4192aa75..31881e59 100644 --- a/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java +++ b/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java @@ -1,5 +1,6 @@ package com.oing.config.support; +import com.oing.exception.StringEmptyWhiteSpaceException; import com.oing.util.OptimizedImageUrlGenerator; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -31,12 +32,17 @@ public class OptimizedImageUrlProvider implements OptimizedImageUrlGenerator { */ @Override public String getThumbnailUrlGenerator(String bucketImageUrl) { - if (bucketImageUrl == null) { + try { + validateUrlEmptyOrWhiteSpace(bucketImageUrl); + + String imagePath = bucketImageUrl.substring(bucketImageUrl.indexOf("/images")); + return imageOptimizerCdnUrl + imagePath + THUMBNAIL_OPTIMIZER_QUERY_STRING; + + } catch (StringEmptyWhiteSpaceException e) { return null; + } catch (IndexOutOfBoundsException e) { + return bucketImageUrl; } - - String imagePath = bucketImageUrl.substring(bucketImageUrl.indexOf("/images")); - return imageOptimizerCdnUrl + imagePath + THUMBNAIL_OPTIMIZER_QUERY_STRING; } @@ -47,11 +53,22 @@ public String getThumbnailUrlGenerator(String bucketImageUrl) { */ @Override public String getKBImageUrlGenerator(String bucketImageUrl) { - if (bucketImageUrl == null) { + try { + validateUrlEmptyOrWhiteSpace(bucketImageUrl); + + String imagePath = bucketImageUrl.substring(bucketImageUrl.indexOf("/images")); + return imageOptimizerCdnUrl + imagePath + KB_IMAGE_OPTIMIZER_QUERY_STRING; + + } catch (StringEmptyWhiteSpaceException e) { return null; + } catch (IndexOutOfBoundsException e) { + return bucketImageUrl; } + } - String imagePath = bucketImageUrl.substring(bucketImageUrl.indexOf("/images")); - return imageOptimizerCdnUrl + imagePath + KB_IMAGE_OPTIMIZER_QUERY_STRING; + private void validateUrlEmptyOrWhiteSpace(String url) throws StringEmptyWhiteSpaceException { + if (url == null || url.trim().isEmpty()) { + throw new StringEmptyWhiteSpaceException(); + } } } From da6f91d52bb368d665c9249f9509e1d4783ff889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC=20=28YeongMin=20Song=29?= Date: Wed, 17 Jan 2024 14:20:15 +0900 Subject: [PATCH 17/49] =?UTF-8?q?[OING-146]=20feat:=20=ED=98=84=EC=9E=AC?= =?UTF-8?q?=20=EC=96=B4=ED=94=8C=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=EB=B2=84=EC=A0=84=20=EC=9C=A0=EB=AC=B4=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/oing/component/AppVersionCache.java | 4 ++ .../java/com/oing/config/SpringWebConfig.java | 10 +++++ .../oing/config/support/AppKeyResolver.java | 31 ++++++++++++++ .../oing/config/support/RequestAppKey.java | 9 ++++ .../com/oing/controller/MeController.java | 13 ++++++ .../main/java/com/oing/domain/AppVersion.java | 3 ++ .../oing/dto/response/AppVersionResponse.java | 41 +++++++++++++++++++ .../src/main/java/com/oing/restapi/MeApi.java | 11 +++++ gateway/src/main/resources/application.yaml | 1 + .../V202401142002__add_isLatest_column.sql | 1 + 10 files changed, 124 insertions(+) create mode 100644 gateway/src/main/java/com/oing/config/support/AppKeyResolver.java create mode 100644 gateway/src/main/java/com/oing/config/support/RequestAppKey.java create mode 100644 gateway/src/main/java/com/oing/dto/response/AppVersionResponse.java create mode 100644 gateway/src/main/resources/db/migration/V202401142002__add_isLatest_column.sql diff --git a/gateway/src/main/java/com/oing/component/AppVersionCache.java b/gateway/src/main/java/com/oing/component/AppVersionCache.java index 3528a8d7..bcab9ec3 100644 --- a/gateway/src/main/java/com/oing/component/AppVersionCache.java +++ b/gateway/src/main/java/com/oing/component/AppVersionCache.java @@ -39,4 +39,8 @@ public boolean isServiceable(UUID appKey) { AppVersion appVersion = appVersionMap.get(appKey); return appVersion != null && appVersion.isInService(); } + + public AppVersion getAppVersion(UUID appKey) { + return appVersionMap.get(appKey); + } } diff --git a/gateway/src/main/java/com/oing/config/SpringWebConfig.java b/gateway/src/main/java/com/oing/config/SpringWebConfig.java index ccc967a7..a3ff9442 100644 --- a/gateway/src/main/java/com/oing/config/SpringWebConfig.java +++ b/gateway/src/main/java/com/oing/config/SpringWebConfig.java @@ -4,14 +4,17 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import com.oing.config.filter.WebRequestInterceptor; +import com.oing.config.support.AppKeyResolver; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Collections; +import java.util.List; /** * no5ing-server @@ -23,6 +26,8 @@ @Configuration public class SpringWebConfig implements WebMvcConfigurer { final WebRequestInterceptor webRequestInterceptor; + final AppKeyResolver appKeyResolver; + @Value("${app.oauth.google-client-id}") private String googleClientId; @@ -37,4 +42,9 @@ public GoogleIdTokenVerifier googleIdTokenVerifier() { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(webRequestInterceptor); } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(appKeyResolver); + } } diff --git a/gateway/src/main/java/com/oing/config/support/AppKeyResolver.java b/gateway/src/main/java/com/oing/config/support/AppKeyResolver.java new file mode 100644 index 00000000..5ab218cf --- /dev/null +++ b/gateway/src/main/java/com/oing/config/support/AppKeyResolver.java @@ -0,0 +1,31 @@ +package com.oing.config.support; + +import com.google.common.base.Preconditions; +import com.oing.config.properties.WebProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.util.UUID; + +@RequiredArgsConstructor +@Component +public class AppKeyResolver implements HandlerMethodArgumentResolver { + private final WebProperties webProperties; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(RequestAppKey.class) != null; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String appKey = webRequest.getHeader(webProperties.headerNames().appKeyHeader()); + Preconditions.checkNotNull(appKey, "App key is null"); + return UUID.fromString(appKey); + } +} diff --git a/gateway/src/main/java/com/oing/config/support/RequestAppKey.java b/gateway/src/main/java/com/oing/config/support/RequestAppKey.java new file mode 100644 index 00000000..cb11a15b --- /dev/null +++ b/gateway/src/main/java/com/oing/config/support/RequestAppKey.java @@ -0,0 +1,9 @@ +package com.oing.config.support; + +import java.lang.annotation.*; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_PARAMETER, ElementType.PARAMETER}) +public @interface RequestAppKey { +} diff --git a/gateway/src/main/java/com/oing/controller/MeController.java b/gateway/src/main/java/com/oing/controller/MeController.java index cb021be0..dd2f7079 100644 --- a/gateway/src/main/java/com/oing/controller/MeController.java +++ b/gateway/src/main/java/com/oing/controller/MeController.java @@ -1,10 +1,13 @@ package com.oing.controller; +import com.oing.component.AppVersionCache; +import com.oing.domain.AppVersion; import com.oing.domain.Family; import com.oing.domain.FamilyInviteLink; import com.oing.domain.Member; import com.oing.dto.request.AddFcmTokenRequest; import com.oing.dto.request.JoinFamilyRequest; +import com.oing.dto.response.AppVersionResponse; import com.oing.dto.response.DefaultResponse; import com.oing.dto.response.FamilyResponse; import com.oing.dto.response.MemberResponse; @@ -22,6 +25,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; +import java.util.UUID; + @RequiredArgsConstructor @Controller public class MeController implements MeApi { @@ -30,6 +35,7 @@ public class MeController implements MeApi { private final MemberDeviceService memberDeviceService; private final FamilyService familyService; private final FamilyInviteLinkService familyInviteLinkService; + private final AppVersionCache appVersionCache; @Override public MemberResponse getMe() { @@ -85,6 +91,13 @@ public FamilyResponse createFamilyAndJoin() { return FamilyResponse.of(family); } + + @Override + public AppVersionResponse getCurrentAppVersion(UUID appKey) { + AppVersion appVersion = appVersionCache.getAppVersion(appKey); + return AppVersionResponse.from(appVersion); + } + @Transactional @Override public DefaultResponse quitFamily() { diff --git a/gateway/src/main/java/com/oing/domain/AppVersion.java b/gateway/src/main/java/com/oing/domain/AppVersion.java index d731826f..0baf8768 100644 --- a/gateway/src/main/java/com/oing/domain/AppVersion.java +++ b/gateway/src/main/java/com/oing/domain/AppVersion.java @@ -35,4 +35,7 @@ public class AppVersion extends BaseAuditEntity { @Column(name = "in_review") private boolean inReview; + + @Column(name = "is_latest") + private boolean isLatest; } diff --git a/gateway/src/main/java/com/oing/dto/response/AppVersionResponse.java b/gateway/src/main/java/com/oing/dto/response/AppVersionResponse.java new file mode 100644 index 00000000..fa4975b2 --- /dev/null +++ b/gateway/src/main/java/com/oing/dto/response/AppVersionResponse.java @@ -0,0 +1,41 @@ +package com.oing.dto.response; + +import com.oing.domain.AppVersion; +import com.oing.domain.DeepLinkType; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.util.Map; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@ToString +@Schema(description = "앱 버전 응답") +public class AppVersionResponse { + @Parameter(description = "앱 키", example = "5a80edc0-5b7e-4b7e-9b7e-5b7e4b7e9b7e") + private String appKey; + + @Parameter(description = "앱 버전", example = "1.0.0") + private String appVersion; + + @Parameter(description = "현재 서비스 유무", example = "true") + private boolean isInService; + + @Parameter(description = "현재 심사중 유무", example = "true") + private boolean isInReview; + + @Parameter(description = "현재 최신버전 유무", example = "true") + private boolean isLatest; + + public static AppVersionResponse from(AppVersion appVersion) { + return new AppVersionResponse( + appVersion.getAppKey().toString(), + appVersion.getAppVersion(), + appVersion.isInService(), + appVersion.isInReview(), + appVersion.isLatest() + ); + } +} diff --git a/gateway/src/main/java/com/oing/restapi/MeApi.java b/gateway/src/main/java/com/oing/restapi/MeApi.java index 65201126..e944282f 100644 --- a/gateway/src/main/java/com/oing/restapi/MeApi.java +++ b/gateway/src/main/java/com/oing/restapi/MeApi.java @@ -1,7 +1,9 @@ package com.oing.restapi; +import com.oing.config.support.RequestAppKey; import com.oing.dto.request.AddFcmTokenRequest; import com.oing.dto.request.JoinFamilyRequest; +import com.oing.dto.response.AppVersionResponse; import com.oing.dto.response.DefaultResponse; import com.oing.dto.response.FamilyResponse; import com.oing.dto.response.MemberResponse; @@ -11,6 +13,8 @@ import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; +import java.util.UUID; + /** * no5ing-server * User: CChuYong @@ -55,7 +59,14 @@ FamilyResponse joinFamily( @PostMapping("/create-family") FamilyResponse createFamilyAndJoin(); + @Operation(summary = "내 접속 버전 조회", description = "현재 버전 정보를 조회합니다.") + @GetMapping("/app-version") + AppVersionResponse getCurrentAppVersion( + @RequestAppKey UUID appKey + ); + @Operation(summary = "가족 탈퇴", description = "가족을 탈퇴합니다.") @PostMapping("/quit-family") DefaultResponse quitFamily(); + } diff --git a/gateway/src/main/resources/application.yaml b/gateway/src/main/resources/application.yaml index ab2a3ce1..14f6a679 100644 --- a/gateway/src/main/resources/application.yaml +++ b/gateway/src/main/resources/application.yaml @@ -43,6 +43,7 @@ app: - /v3/api-docs - /error - /v1/links/* + - /v1/me/app-version version-check-whitelists: - /actuator/** - /swagger-ui.html diff --git a/gateway/src/main/resources/db/migration/V202401142002__add_isLatest_column.sql b/gateway/src/main/resources/db/migration/V202401142002__add_isLatest_column.sql new file mode 100644 index 00000000..09a4d386 --- /dev/null +++ b/gateway/src/main/resources/db/migration/V202401142002__add_isLatest_column.sql @@ -0,0 +1 @@ +ALTER TABLE `app_version` ADD COLUMN (`is_latest` BOOL NOT NULL DEFAULT FALSE); From 4ed358598c7f3654737d0b795e71f66310195575 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 17 Jan 2024 21:05:02 +0900 Subject: [PATCH 18/49] =?UTF-8?q?[OING-154]=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EC=9D=98=20=EB=A6=AC=EC=96=BC=EC=9D=B4=EB=AA=A8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: fix createPostComment test * feat: add getMemberRealEmoji api * style: modify description * test: add getMemberRealEmoji test * test: add getMemberRealEmoji integration test --- .../oing/restapi/MemberRealEmojiApiTest.java | 32 ++++++++++++- .../controller/MemberRealEmojiController.java | 17 +++++-- .../oing/dto/response/RealEmojiResponse.java | 2 +- .../oing/dto/response/RealEmojisResponse.java | 4 +- .../repository/MemberRealEmojiRepository.java | 3 ++ .../com/oing/restapi/MemberRealEmojiApi.java | 12 ++--- .../oing/service/MemberRealEmojiService.java | 6 +++ .../MemberPostCommentControllerTest.java | 1 + .../MemberRealEmojiControllerTest.java | 46 +++++++++++++++++-- 9 files changed, 104 insertions(+), 19 deletions(-) diff --git a/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java index 3f208995..d5c97f3d 100644 --- a/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java @@ -23,8 +23,7 @@ import java.time.LocalDate; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -134,4 +133,33 @@ void setUp() { .andExpect(status().isOk()) .andExpect(jsonPath("$.imageUrl").value(realEmojiImageUrl)); } + + @Test + void 회원_리얼이모지_조회_테스트() throws Exception { + //given + String realEmojiImageUrl = "https://test.com/bucket/images/realEmoji.jpg"; + memberRealEmojiRepository.save( + new MemberRealEmoji( + TEST_MEMBER_REAL_EMOJI_ID, + TEST_MEMBER_ID, + Emoji.EMOJI_1, + realEmojiImageUrl, + "images/defaultEmoji.jpg" + ) + ); + + //when + ResultActions resultActions = mockMvc.perform( + get("/v1/members/{memberId}/real-emoji", TEST_MEMBER_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.myRealEmojiList[0].realEmojiId").value(TEST_MEMBER_REAL_EMOJI_ID)) + .andExpect(jsonPath("$.myRealEmojiList[0].type").value(Emoji.EMOJI_1.getTypeKey())) + .andExpect(jsonPath("$.myRealEmojiList[0].imageUrl").value(realEmojiImageUrl)); + } } diff --git a/post/src/main/java/com/oing/controller/MemberRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java index e90b77f3..97e8d909 100644 --- a/post/src/main/java/com/oing/controller/MemberRealEmojiController.java +++ b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java @@ -20,6 +20,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; +import java.util.List; +import java.util.stream.Collectors; + @RequiredArgsConstructor @Controller public class MemberRealEmojiController implements MemberRealEmojiApi { @@ -39,7 +42,7 @@ public PreSignedUrlResponse requestPresignedUrl(String memberId, PreSignedUrlReq @Transactional @Override - public RealEmojiResponse createMyRealEmoji(String memberId, CreateMyRealEmojiRequest request) { + public RealEmojiResponse createMemberRealEmoji(String memberId, CreateMyRealEmojiRequest request) { validateMemberId(memberId); String emojiId = identityGenerator.generateIdentity(); String emojiImgKey = preSignedUrlGenerator.extractImageKey(request.imageUrl()); @@ -59,7 +62,7 @@ private boolean isExistsSameRealEmojiType(Emoji emoji) { @Transactional @Override - public RealEmojiResponse changeMyRealEmoji(String memberId, String realEmojiId, UpdateMyRealEmojiRequest request) { + public RealEmojiResponse changeMemberRealEmoji(String memberId, String realEmojiId, UpdateMyRealEmojiRequest request) { validateMemberId(memberId); String emojiImgKey = preSignedUrlGenerator.extractImageKey(request.imageUrl()); @@ -69,8 +72,14 @@ public RealEmojiResponse changeMyRealEmoji(String memberId, String realEmojiId, } @Override - public RealEmojisResponse getMyRealEmojis(String memberId) { - return new RealEmojisResponse(null); + public RealEmojisResponse getMemberRealEmojis(String memberId) { + validateMemberId(memberId); + + List realEmojis = memberRealEmojiService.findRealEmojisByMemberId(memberId); + List emojiResponses = realEmojis.stream() + .map(RealEmojiResponse::from) + .collect(Collectors.toList()); + return new RealEmojisResponse(emojiResponses); } private void validateMemberId(String memberId) { diff --git a/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java index 7d0369fe..9ab83b29 100644 --- a/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java +++ b/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java @@ -3,7 +3,7 @@ import com.oing.domain.MemberRealEmoji; import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "자신이 생성한 리얼 이모지 응답") +@Schema(description = "회원이 생성한 리얼 이모지 응답") public record RealEmojiResponse ( @Schema(description = "리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") String realEmojiId, diff --git a/post/src/main/java/com/oing/dto/response/RealEmojisResponse.java b/post/src/main/java/com/oing/dto/response/RealEmojisResponse.java index 85f84230..5535be3c 100644 --- a/post/src/main/java/com/oing/dto/response/RealEmojisResponse.java +++ b/post/src/main/java/com/oing/dto/response/RealEmojisResponse.java @@ -4,9 +4,9 @@ import java.util.List; -@Schema(description = "자신이 생성한 리얼 이모지 리스트 응답") +@Schema(description = "회원이 생성한 리얼 이모지 리스트 응답") public record RealEmojisResponse( - @Schema(description = "자신이 생성한 리얼 이모지 정보") + @Schema(description = "회원이 생성한 리얼 이모지 정보") List myRealEmojiList ) { } diff --git a/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java index 66585ae1..5eace7ee 100644 --- a/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java +++ b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java @@ -4,9 +4,12 @@ import com.oing.domain.MemberRealEmoji; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface MemberRealEmojiRepository extends JpaRepository { Optional findByType(Emoji emoji); + + List findAllByMemberId(String memberId); } diff --git a/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java b/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java index 9cef8ab8..0279d30d 100644 --- a/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java +++ b/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java @@ -30,9 +30,9 @@ PreSignedUrlResponse requestPresignedUrl( PreSignedUrlRequest request ); - @Operation(summary = "자신의 리얼 이모지 추가", description = "자신의 리얼 이모지를 추가합니다.") + @Operation(summary = "회원의 리얼 이모지 추가", description = "회원의 리얼 이모지를 추가합니다.") @PostMapping - RealEmojiResponse createMyRealEmoji( + RealEmojiResponse createMemberRealEmoji( @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable String memberId, @@ -42,9 +42,9 @@ RealEmojiResponse createMyRealEmoji( CreateMyRealEmojiRequest request ); - @Operation(summary = "자신의 리얼 이모지 변경", description = "자신의 리얼 이모지 사진을 변경합니다.") + @Operation(summary = "회원의 리얼 이모지 변경", description = "회원의 리얼 이모지 사진을 변경합니다.") @PutMapping("/{realEmojiId}") - RealEmojiResponse changeMyRealEmoji( + RealEmojiResponse changeMemberRealEmoji( @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable String memberId, @@ -58,9 +58,9 @@ RealEmojiResponse changeMyRealEmoji( UpdateMyRealEmojiRequest request ); - @Operation(summary = "회원의 리얼 이모지 조회", description = "자신의 리얼 이모지를 조회합니다.") + @Operation(summary = "회원의 리얼 이모지 조회", description = "회원의 리얼 이모지를 조회합니다.") @GetMapping - RealEmojisResponse getMyRealEmojis( + RealEmojisResponse getMemberRealEmojis( @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable String memberId diff --git a/post/src/main/java/com/oing/service/MemberRealEmojiService.java b/post/src/main/java/com/oing/service/MemberRealEmojiService.java index 903b0b43..640c19ea 100644 --- a/post/src/main/java/com/oing/service/MemberRealEmojiService.java +++ b/post/src/main/java/com/oing/service/MemberRealEmojiService.java @@ -7,6 +7,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor public class MemberRealEmojiService { @@ -28,4 +30,8 @@ public boolean findRealEmojiByEmojiType(Emoji emoji) { .findByType(emoji) .isPresent(); } + + public List findRealEmojisByMemberId(String memberId) { + return memberRealEmojiRepository.findAllByMemberId(memberId); + } } diff --git a/post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java b/post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java index d7f59829..1ff42774 100644 --- a/post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java +++ b/post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java @@ -66,6 +66,7 @@ public class MemberPostCommentControllerTest { when(authenticationHolder.getUserId()).thenReturn("1"); when(memberBridge.isInSameFamily("1", "1")).thenReturn(true); when(identityGenerator.generateIdentity()).thenReturn(memberPost.getId()); + when(memberPostCommentService.savePostComment(any())).thenReturn(memberPostComment); //when PostCommentResponse response = memberPostCommentController.createPostComment( diff --git a/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java b/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java index 271b93a6..e0515e62 100644 --- a/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java +++ b/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java @@ -7,6 +7,7 @@ import com.oing.dto.request.UpdateMyRealEmojiRequest; import com.oing.dto.response.PreSignedUrlResponse; import com.oing.dto.response.RealEmojiResponse; +import com.oing.dto.response.RealEmojisResponse; import com.oing.exception.AuthorizationFailedException; import com.oing.exception.DuplicateRealEmojiException; import com.oing.service.MemberRealEmojiService; @@ -21,6 +22,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.context.ActiveProfiles; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -70,7 +73,7 @@ public class MemberRealEmojiControllerTest { CreateMyRealEmojiRequest request = new CreateMyRealEmojiRequest(emoji.getTypeKey(), realEmojiImageUrl); when(memberRealEmojiService.save(any())).thenReturn(new MemberRealEmoji("1", memberId, emoji, realEmojiImageUrl, "realEmoji.jpg")); - RealEmojiResponse response = memberRealEmojiController.createMyRealEmoji(memberId, request); + RealEmojiResponse response = memberRealEmojiController.createMemberRealEmoji(memberId, request); // then assertEquals(emoji.getTypeKey(), response.type()); @@ -90,7 +93,7 @@ public class MemberRealEmojiControllerTest { // then assertThrows(AuthorizationFailedException.class, - () -> memberRealEmojiController.createMyRealEmoji(memberId, request)); + () -> memberRealEmojiController.createMemberRealEmoji(memberId, request)); } @Test @@ -107,7 +110,7 @@ public class MemberRealEmojiControllerTest { // then assertThrows(DuplicateRealEmojiException.class, - () -> memberRealEmojiController.createMyRealEmoji(memberId, request)); + () -> memberRealEmojiController.createMemberRealEmoji(memberId, request)); } @Test @@ -122,9 +125,44 @@ public class MemberRealEmojiControllerTest { UpdateMyRealEmojiRequest request = new UpdateMyRealEmojiRequest(realEmojiImageUrl); when(memberRealEmojiService.findRealEmojiById(realEmojiId)).thenReturn(new MemberRealEmoji("1", memberId, Emoji.EMOJI_1, realEmojiImageUrl, "realEmoji.jpg")); - RealEmojiResponse response = memberRealEmojiController.changeMyRealEmoji(memberId, realEmojiId, request); + RealEmojiResponse response = memberRealEmojiController.changeMemberRealEmoji(memberId, realEmojiId, request); // then assertEquals(request.imageUrl(), response.imageUrl()); } + + @Test + void 회원_리얼이모지_조회_테스트() { + // given + String memberId = "1"; + String realEmojiImageUrl1 = "https://test.com/realEmoji1.jpg"; + String realEmojiImageUrl2 = "https://test.com/realEmoji2.jpg"; + Emoji emoji1 = Emoji.EMOJI_1; + Emoji emoji2 = Emoji.EMOJI_4; + when(authenticationHolder.getUserId()).thenReturn(memberId); + CreateMyRealEmojiRequest request1 = new CreateMyRealEmojiRequest(emoji1.getTypeKey(), realEmojiImageUrl1); + CreateMyRealEmojiRequest request2 = new CreateMyRealEmojiRequest(emoji1.getTypeKey(), realEmojiImageUrl2); + when(memberRealEmojiService.save(any())).thenReturn(new MemberRealEmoji("1", memberId, emoji1, + realEmojiImageUrl1, "realEmoji1.jpg")); + memberRealEmojiController.createMemberRealEmoji(memberId, request1); + when(memberRealEmojiService.save(any())).thenReturn(new MemberRealEmoji("2", memberId, emoji2, + realEmojiImageUrl2, "realEmoji2.jpg")); + memberRealEmojiController.createMemberRealEmoji(memberId, request2); + + // when + when(memberRealEmojiService.findRealEmojisByMemberId(memberId)).thenReturn(List.of( + new MemberRealEmoji("1", memberId, emoji1, realEmojiImageUrl1, "realEmoji1.jpg"), + new MemberRealEmoji("2", memberId, emoji2, realEmojiImageUrl2, "realEmoji2.jpg") + )); + RealEmojisResponse response = memberRealEmojiController.getMemberRealEmojis(memberId); + + // then + assertEquals(2, response.myRealEmojiList().size()); + assertEquals("1", response.myRealEmojiList().get(0).realEmojiId()); + assertEquals("2", response.myRealEmojiList().get(1).realEmojiId()); + assertEquals(emoji1.getTypeKey(), response.myRealEmojiList().get(0).type()); + assertEquals(emoji2.getTypeKey(), response.myRealEmojiList().get(1).type()); + assertEquals(request1.imageUrl(), response.myRealEmojiList().get(0).imageUrl()); + assertEquals(request2.imageUrl(), response.myRealEmojiList().get(1).imageUrl()); + } } From 95548709ddd1b26d3d5f99445db1ebaa6738748c Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 17 Jan 2024 21:05:45 +0900 Subject: [PATCH 19/49] =?UTF-8?q?[OING-161]=20test:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=20=EB=A6=AC=EC=95=A1=EC=85=98=20=EB=93=B1=EB=A1=9D/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C/=EB=82=A8=EA=B8=B4=20=EB=A9=A4=EB=B2=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add MemberPostReactionController unit test * test: add MemberPostReactionController integration test --- .../restapi/MemberPostReactionApiTest.java | 139 ++++++++++++++++++ .../MemberPostReactionController.java | 12 +- .../oing/dto/request/PostReactionRequest.java | 3 +- .../MemberPostReactionControllerTest.java | 134 +++++++++++++++++ 4 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 gateway/src/test/java/com/oing/restapi/MemberPostReactionApiTest.java create mode 100644 post/src/test/java/com/oing/controller/MemberPostReactionControllerTest.java diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostReactionApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostReactionApiTest.java new file mode 100644 index 00000000..fdc96658 --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MemberPostReactionApiTest.java @@ -0,0 +1,139 @@ +package com.oing.restapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.domain.Emoji; +import com.oing.domain.Member; +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostReaction; +import com.oing.dto.request.PostReactionRequest; +import com.oing.repository.MemberPostReactionRepository; +import com.oing.repository.MemberPostRepository; +import com.oing.repository.MemberRepository; +import com.oing.service.TokenGenerator; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class MemberPostReactionApiTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private TokenGenerator tokenGenerator; + @Autowired + private ObjectMapper objectMapper; + + private String TEST_MEMBER_ID = "01HGW2N7EHJVJ4CJ999RRS2E97"; + private String TEST_POST_ID = "01HGW2N7EHJVJ4CJ999RRS2A97"; + private String TEST_MEMBER_TOKEN; + + @Autowired + private MemberRepository memberRepository; + @Autowired + private MemberPostRepository memberPostRepository; + @Autowired + private MemberPostReactionRepository memberPostReactionRepository; + + @BeforeEach + void setUp() { + memberRepository.save( + new Member( + TEST_MEMBER_ID, + "testUser1", + LocalDate.now(), + "", "", "" + ) + ); + TEST_MEMBER_TOKEN = tokenGenerator + .generateTokenPair(TEST_MEMBER_ID) + .accessToken(); + memberPostRepository.save( + new MemberPost( + TEST_POST_ID, + TEST_MEMBER_ID, + "img", + "img", + "content" + ) + ); + } + + @Test + void 게시물_리액션_추가_테스트() throws Exception { + //given + Emoji emoji = Emoji.EMOJI_1; + PostReactionRequest request = new PostReactionRequest(emoji.getTypeKey()); + + //when + ResultActions resultActions = mockMvc.perform( + post("/v1/posts/{postId}/reactions", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + void 게시물_리액션_삭제_테스트() throws Exception { + //given + Emoji emoji = Emoji.EMOJI_1; + PostReactionRequest request = new PostReactionRequest(emoji.getTypeKey()); + memberPostReactionRepository.save(new MemberPostReaction("1", memberPostRepository.getReferenceById(TEST_POST_ID), + TEST_MEMBER_ID, emoji)); + + //when + ResultActions resultActions = mockMvc.perform( + delete("/v1/posts/{postId}/reactions", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + void 게시물_리액션_남긴_멤버_조회() throws Exception { + //given + Emoji emoji = Emoji.EMOJI_3; + memberPostReactionRepository.save(new MemberPostReaction("1", memberPostRepository.getReferenceById(TEST_POST_ID), + TEST_MEMBER_ID, emoji)); + + //when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/{postId}/reactions/member", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.emojiMemberIdsList.emoji_3[0]").value(TEST_MEMBER_ID)); + } +} diff --git a/post/src/main/java/com/oing/controller/MemberPostReactionController.java b/post/src/main/java/com/oing/controller/MemberPostReactionController.java index 2223d492..d95dad82 100644 --- a/post/src/main/java/com/oing/controller/MemberPostReactionController.java +++ b/post/src/main/java/com/oing/controller/MemberPostReactionController.java @@ -66,6 +66,12 @@ public DefaultResponse deletePostReaction(String postId, PostReactionRequest req return DefaultResponse.ok(); } + private void validatePostReactionForDeletion(MemberPost post, String memberId, Emoji emoji) { + if (!memberPostReactionService.isMemberPostReactionExists(post, memberId, emoji)) { + throw new EmojiNotFoundException(); + } + } + @Override @Transactional public PostReactionSummaryResponse getPostReactionSummary(String postId) { @@ -113,10 +119,4 @@ public PostReactionsResponse getPostReactionMembers(String postId) { return new PostReactionsResponse(emojiMemberIdsMap); } - - private void validatePostReactionForDeletion(MemberPost post, String memberId, Emoji emoji) { - if (!memberPostReactionService.isMemberPostReactionExists(post, memberId, emoji)) { - throw new EmojiNotFoundException(); - } - } } diff --git a/post/src/main/java/com/oing/dto/request/PostReactionRequest.java b/post/src/main/java/com/oing/dto/request/PostReactionRequest.java index 7b42da9d..d4b056bc 100644 --- a/post/src/main/java/com/oing/dto/request/PostReactionRequest.java +++ b/post/src/main/java/com/oing/dto/request/PostReactionRequest.java @@ -12,8 +12,7 @@ @Schema(description = "피드 게시물 반응 생성 및 삭제 요청") public record PostReactionRequest( @NotBlank - @Schema(description = "이모지", example = "smile", - allowableValues = {"heart", "slightly_smiling_face", "shining_face", "smiling_face", "smile"}) + @Schema(description = "이모지", example = "emoji_1") String content ) { } diff --git a/post/src/test/java/com/oing/controller/MemberPostReactionControllerTest.java b/post/src/test/java/com/oing/controller/MemberPostReactionControllerTest.java new file mode 100644 index 00000000..542da9e1 --- /dev/null +++ b/post/src/test/java/com/oing/controller/MemberPostReactionControllerTest.java @@ -0,0 +1,134 @@ +package com.oing.controller; + +import com.oing.domain.Emoji; +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostReaction; +import com.oing.dto.request.PostReactionRequest; +import com.oing.dto.response.PostReactionsResponse; +import com.oing.exception.EmojiAlreadyExistsException; +import com.oing.exception.EmojiNotFoundException; +import com.oing.service.MemberPostReactionService; +import com.oing.service.MemberPostService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +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 java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MemberPostReactionControllerTest { + @InjectMocks + private MemberPostReactionController memberPostReactionController; + + @Mock + private AuthenticationHolder authenticationHolder; + @Mock + private IdentityGenerator identityGenerator; + @Mock + private MemberPostService memberPostService; + @Mock + private MemberPostReactionService memberPostReactionService; + + @Test + void 게시물_리액션_생성_테스트() { + //given + String memberId = "1"; + when(authenticationHolder.getUserId()).thenReturn(memberId); + MemberPost post = new MemberPost("1", memberId, "1", "1", "1"); + MemberPostReaction reaction = new MemberPostReaction("1", post, memberId, Emoji.EMOJI_1); + when(memberPostService.findMemberPostById(post.getId())).thenReturn(post); + when(memberPostReactionService.isMemberPostReactionExists(post, memberId, Emoji.EMOJI_1)).thenReturn(false); + when(identityGenerator.generateIdentity()).thenReturn(reaction.getId()); + when(memberPostReactionService.createPostReaction(reaction.getId(), post, memberId, Emoji.EMOJI_1)).thenReturn(reaction); + + //when + PostReactionRequest request = new PostReactionRequest("emoji_1"); + memberPostReactionController.createPostReaction(post.getId(), request); + + //then + //nothing. just check no exception + } + + @Test + void 게시물_중복된_리액션_등록_예외_테스트() { + //given + String memberId = "1"; + when(authenticationHolder.getUserId()).thenReturn(memberId); + MemberPost post = new MemberPost("1", memberId, "1", "1", "1"); + when(memberPostService.findMemberPostById(post.getId())).thenReturn(post); + + //when + when(memberPostReactionService.isMemberPostReactionExists(post, memberId, Emoji.EMOJI_1)).thenReturn(true); + PostReactionRequest request = new PostReactionRequest("emoji_1"); + + //then + assertThrows(EmojiAlreadyExistsException.class, + () -> memberPostReactionController.createPostReaction(post.getId(), request)); + } + + @Test + void 게시물_리액션_삭제_테스트() { + //given + String memberId = "1"; + when(authenticationHolder.getUserId()).thenReturn(memberId); + MemberPost post = new MemberPost("1", memberId, "1", "1", "1"); + MemberPostReaction reaction = new MemberPostReaction("1", post, memberId, Emoji.EMOJI_1); + when(memberPostService.findMemberPostById(post.getId())).thenReturn(post); + when(memberPostReactionService.isMemberPostReactionExists(post, memberId, Emoji.EMOJI_1)).thenReturn(true); + when(memberPostReactionService.findReaction(post, memberId, Emoji.EMOJI_1)).thenReturn(reaction); + + //when + PostReactionRequest request = new PostReactionRequest("emoji_1"); + memberPostReactionController.deletePostReaction(post.getId(), request); + + //then + //nothing. just check no exception + } + + @Test + void 게시물_존재하지_않는_리액션_삭제_예외_테스트() { + //given + String memberId = "1"; + when(authenticationHolder.getUserId()).thenReturn(memberId); + MemberPost post = new MemberPost("1", memberId, "1", "1", "1"); + when(memberPostService.findMemberPostById(post.getId())).thenReturn(post); + + //when + when(memberPostReactionService.isMemberPostReactionExists(post, memberId, Emoji.EMOJI_1)).thenReturn(false); + PostReactionRequest request = new PostReactionRequest("emoji_1"); + + //then + assertThrows(EmojiNotFoundException.class, + () -> memberPostReactionController.deletePostReaction(post.getId(), request)); + } + + @Test + void 리액션_남긴_멤버_조회_테스트() { + //given + String memberId = "1"; + MemberPost post = new MemberPost("1", memberId, "1", "1", "1"); + List mockReactions = Arrays.asList( + new MemberPostReaction("1", post, memberId, Emoji.EMOJI_1), + new MemberPostReaction("2", post, memberId, Emoji.EMOJI_2) + ); + when(memberPostReactionService.getMemberPostReactionsByPostId(post.getId())).thenReturn(mockReactions); + + //when + PostReactionsResponse response = memberPostReactionController.getPostReactionMembers(post.getId()); + + //then + assertTrue(response.emojiMemberIdsList().get(Emoji.EMOJI_1.getTypeKey()).contains(memberId)); + assertTrue(response.emojiMemberIdsList().get(Emoji.EMOJI_2.getTypeKey()).contains(memberId)); + assertFalse(response.emojiMemberIdsList().get(Emoji.EMOJI_3.getTypeKey()).contains(memberId)); + assertFalse(response.emojiMemberIdsList().get(Emoji.EMOJI_4.getTypeKey()).contains(memberId)); + assertFalse(response.emojiMemberIdsList().get(Emoji.EMOJI_5.getTypeKey()).contains(memberId)); + } +} From 3c121572e2ad1907dd1d4e8b90a8f1bb592a5096 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 17 Jan 2024 21:06:19 +0900 Subject: [PATCH 20/49] =?UTF-8?q?[OING-159]=20feat:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EB=93=B1=EB=A1=9D=EB=90=9C=20=EB=A6=AC=EC=96=BC?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EB=AA=A8=ED=82=B9=20(#110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MemberPostRealEmojiController.java | 34 ++++++++----------- .../dto/response/PostRealEmojiResponse.java | 6 ++-- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java index b6bc44af..98f9c2ec 100644 --- a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java +++ b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java @@ -1,29 +1,16 @@ package com.oing.controller; -import com.oing.domain.MemberPost; -import com.oing.domain.PaginationDTO; -import com.oing.dto.request.CreatePostRequest; import com.oing.dto.request.PostRealEmojiRequest; -import com.oing.dto.request.PreSignedUrlRequest; -import com.oing.dto.response.*; -import com.oing.exception.DuplicatePostUploadException; -import com.oing.exception.InvalidUploadTimeException; -import com.oing.restapi.MemberPostApi; +import com.oing.dto.response.ArrayResponse; +import com.oing.dto.response.DefaultResponse; +import com.oing.dto.response.PostRealEmojiResponse; import com.oing.restapi.MemberPostRealEmojiApi; -import com.oing.service.MemberBridge; -import com.oing.service.MemberPostService; -import com.oing.util.AuthenticationHolder; -import com.oing.util.IdentityGenerator; -import com.oing.util.PreSignedUrlGenerator; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZonedDateTime; -import java.util.Collections; +import java.util.Arrays; +import java.util.List; @RequiredArgsConstructor @Controller @@ -41,6 +28,15 @@ public DefaultResponse deleteRealEmoji(String postId, String realEmojiId) { @Override public ArrayResponse getPostRealEmojis(String postId) { - return ArrayResponse.of(Collections.emptyList()); + List mockResponses = Arrays.asList( + new PostRealEmojiResponse("01HGW2N7EHJVJ4CJ999RRS2E97", "emoji_1", postId, + "01HUUDFAOHJVJ4CJ999RRS2E97", "http://test.com/images/test1.jpg"), + new PostRealEmojiResponse("01HGW2N7EHJVJ4CJ999RRS2E97", "emoji_2", postId, + "01HGW2N7EHJVJ4CJ999RRS2E97", "http://test.com/images/test2.jpg"), + new PostRealEmojiResponse("01DGW2N7EFFFEDFAG9RRS2E976", "emoji_2", postId, + "01HGW2N7EHJVJEEFD99RRS2E97", "http://test.com/images/test2.jpg") + ); + + return ArrayResponse.of(mockResponses); } } diff --git a/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java index 61a25db6..465bb258 100644 --- a/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java +++ b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java @@ -1,11 +1,13 @@ package com.oing.dto.response; -import com.oing.domain.MemberPostReaction; import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "피드 게시물 응답") +@Schema(description = "피드 게시물 리얼 이모지 응답") public record PostRealEmojiResponse( @Schema(description = "피드 게시물 리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String postRealEmojiId, + + @Schema(description = "리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") String realEmojiId, @Schema(description = "피드 게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") From 2cad568ca23424701b1961dbed874dcb0ad662c3 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 17 Jan 2024 21:12:16 +0900 Subject: [PATCH 21/49] =?UTF-8?q?[OING-153]=20feat:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=20=EB=A6=AC=EC=96=BC=EC=9D=B4=EB=AA=A8=EC=A7=80=20POST/DELETE?= =?UTF-8?q?=20API=20=EA=B5=AC=ED=98=84=20(#106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add createPostRealEmoji Api * feat: add deletePostRealEmoji Api * test: add MemberPostRealEmoji unit test * test: add MemberPostRealEmoji integration test --------- Co-authored-by: 송영민 (YeongMin Song) --- .../java/com/oing/exception/ErrorCode.java | 4 +- .../restapi/MemberPostRealEmojiApiTest.java | 112 +++++++++++++ gateway/src/test/resources/application.yaml | 2 +- .../MemberPostRealEmojiController.java | 74 ++++++++- .../dto/response/PostRealEmojiResponse.java | 16 +- .../RealEmojiAlreadyExistsException.java | 7 + .../RegisteredRealEmojiNotFoundException.java | 7 + .../MemberPostRealEmojiRepository.java | 7 + .../oing/restapi/MemberPostRealEmojiApi.java | 4 +- .../service/MemberPostRealEmojiService.java | 55 +++++++ .../oing/service/MemberRealEmojiService.java | 7 + .../MemberPostRealEmojiControllerTest.java | 151 ++++++++++++++++++ 12 files changed, 433 insertions(+), 13 deletions(-) create mode 100644 gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java create mode 100644 post/src/main/java/com/oing/exception/RealEmojiAlreadyExistsException.java create mode 100644 post/src/main/java/com/oing/exception/RegisteredRealEmojiNotFoundException.java create mode 100644 post/src/main/java/com/oing/service/MemberPostRealEmojiService.java create mode 100644 post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java diff --git a/common/src/main/java/com/oing/exception/ErrorCode.java b/common/src/main/java/com/oing/exception/ErrorCode.java index fcaf2029..823e5587 100644 --- a/common/src/main/java/com/oing/exception/ErrorCode.java +++ b/common/src/main/java/com/oing/exception/ErrorCode.java @@ -58,7 +58,9 @@ public enum ErrorCode { * Real-Emoji Related Errors */ REAL_EMOJI_NOT_FOUND("RE0001", "Real-Emoji not found"), - DUPLICATE_REAL_EMOJI("RE0002", "Duplicate Real Emoji"), + REAL_EMOJI_ALREADY_EXISTS("RE0002", "Real-Emoji already exists"), + REGISTERED_REAL_EMOJI_NOT_FOUND("RE0003", "Registered Real-Emoji not found"), + DUPLICATE_REAL_EMOJI("RE0004", "Duplicate Real Emoji"), /** * Deep Link Related Errors */ diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java new file mode 100644 index 00000000..1cec15e5 --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java @@ -0,0 +1,112 @@ +package com.oing.restapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.domain.*; +import com.oing.dto.request.PostRealEmojiRequest; +import com.oing.repository.MemberPostRealEmojiRepository; +import com.oing.repository.MemberPostRepository; +import com.oing.repository.MemberRealEmojiRepository; +import com.oing.repository.MemberRepository; +import com.oing.service.TokenGenerator; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class MemberPostRealEmojiApiTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private TokenGenerator tokenGenerator; + @Autowired + private ObjectMapper objectMapper; + + private String TEST_MEMBER_ID = "01HGW2N7EHJVJ4CJ999RRS2E97"; + private String TEST_POST_ID = "01HGW2N7EHJVJ4CJ999RRS2A97"; + private String TEST_REAL_EMOJI_ID = "01HGW2N7EHJVJ4CJ999RRS2A97"; + private String TEST_MEMBER_TOKEN; + + @Autowired + private MemberRepository memberRepository; + @Autowired + private MemberPostRepository memberPostRepository; + @Autowired + private MemberRealEmojiRepository memberRealEmojiRepository; + @Autowired + private MemberPostRealEmojiRepository memberPostRealEmojiRepository; + + @BeforeEach + void setUp() { + memberRepository.save(new Member(TEST_MEMBER_ID, "testUser1", LocalDate.now(), "", + "", "")); + TEST_MEMBER_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER_ID).accessToken(); + + memberPostRepository.save(new MemberPost(TEST_POST_ID, TEST_MEMBER_ID, "img", "img", + "content")); + + memberRealEmojiRepository.save(new MemberRealEmoji(TEST_REAL_EMOJI_ID, TEST_MEMBER_ID, Emoji.EMOJI_1, + "https://test.com/bucket/real-emoji.jpg", "bucket/real-emoji.jpg")); + + } + + @Test + void 게시물_리얼이모지_추가_테스트() throws Exception { + //given + PostRealEmojiRequest request = new PostRealEmojiRequest(TEST_REAL_EMOJI_ID); + String emojiImageUrl = "https://test.com/bucket/real-emoji.jpg"; + + //when + ResultActions resultActions = mockMvc.perform( + post("/v1/posts/{postId}/real-emoji", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.postId").value(TEST_POST_ID)) + .andExpect(jsonPath("$.memberId").value(TEST_MEMBER_ID)) + .andExpect(jsonPath("$.realEmojiId").value(TEST_REAL_EMOJI_ID)) + .andExpect(jsonPath("$.emojiImageUrl").value(emojiImageUrl)); + } + + @Test + void 게시물_리얼이모지_삭제_테스트() throws Exception { + //given + MemberRealEmoji realEmoji = memberRealEmojiRepository.findById(TEST_REAL_EMOJI_ID).orElseThrow(); + MemberPost post = memberPostRepository.findById(TEST_POST_ID).orElseThrow(); + memberPostRealEmojiRepository.save(new MemberPostRealEmoji("1", realEmoji, post, TEST_MEMBER_ID)); + + //when + ResultActions resultActions = mockMvc.perform( + delete("/v1/posts/{postId}/real-emoji/{realEmojiId}", TEST_POST_ID, TEST_REAL_EMOJI_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } +} diff --git a/gateway/src/test/resources/application.yaml b/gateway/src/test/resources/application.yaml index dd37bc2a..096be448 100644 --- a/gateway/src/test/resources/application.yaml +++ b/gateway/src/test/resources/application.yaml @@ -14,7 +14,7 @@ app: cloud: ncp: region: test - end-point: https://test.com + end-point: https://test.com/ access-key: access-key secret-key: secret-key storage: diff --git a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java index 98f9c2ec..bac39ce9 100644 --- a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java +++ b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java @@ -1,10 +1,28 @@ package com.oing.controller; +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostRealEmoji; +import com.oing.domain.MemberRealEmoji; import com.oing.dto.request.PostRealEmojiRequest; import com.oing.dto.response.ArrayResponse; import com.oing.dto.response.DefaultResponse; import com.oing.dto.response.PostRealEmojiResponse; +import com.oing.exception.AuthorizationFailedException; +import com.oing.exception.RealEmojiAlreadyExistsException; +import com.oing.exception.RegisteredRealEmojiNotFoundException; +import com.oing.restapi.MemberPostRealEmojiApi; +import com.oing.service.MemberBridge; +import com.oing.service.MemberPostRealEmojiService; +import com.oing.service.MemberPostService; +import com.oing.service.MemberRealEmojiService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; + +import java.util.Collections; import com.oing.restapi.MemberPostRealEmojiApi; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; @@ -16,14 +34,62 @@ @Controller public class MemberPostRealEmojiController implements MemberPostRealEmojiApi { + private final AuthenticationHolder authenticationHolder; + private final IdentityGenerator identityGenerator; + private final MemberPostService memberPostService; + private final MemberPostRealEmojiService memberPostRealEmojiService; + private final MemberRealEmojiService memberRealEmojiService; + private final MemberBridge memberBridge; + + /** + * 게시물에 리얼 이모지를 등록합니다 + * @param postId 게시물 ID + * @param request 리얼 이모지 등록 요청 + * @return 생성된 리얼 이모지 + * @throws AuthorizationFailedException 내 가족이 올린 게시물이 아닌 경우 + * @throws RealEmojiAlreadyExistsException 이미 등록된 리얼 이모지인 경우 + */ + @Transactional @Override - public DefaultResponse createRealEmoji(String postId, PostRealEmojiRequest request) { - return new DefaultResponse(true); + public PostRealEmojiResponse createPostRealEmoji(String postId, PostRealEmojiRequest request) { + String memberId = authenticationHolder.getUserId(); + MemberPost post = memberPostService.getMemberPostById(postId); + if (!memberBridge.isInSameFamily(memberId, post.getMemberId())) + throw new AuthorizationFailedException(); + + MemberRealEmoji realEmoji = memberRealEmojiService.getMemberRealEmojiById(request.realEmojiId()); + validatePostRealEmojiForAddition(post, memberId, realEmoji); + MemberPostRealEmoji postRealEmoji = new MemberPostRealEmoji(identityGenerator.generateIdentity(), realEmoji, + post, memberId); + MemberPostRealEmoji addedPostRealEmoji = memberPostRealEmojiService.savePostRealEmoji(postRealEmoji); + post.addRealEmoji(postRealEmoji); + return PostRealEmojiResponse.from(addedPostRealEmoji); + } + + private void validatePostRealEmojiForAddition(MemberPost post, String memberId, MemberRealEmoji emoji) { + if (memberPostRealEmojiService.isMemberPostRealEmojiExists(post, memberId, emoji)) { + throw new RealEmojiAlreadyExistsException(); + } } + /** + * 게시물에 등록된 리얼 이모지를 삭제합니다 + * @param postId 게시물 ID + * @param realEmojiId 리얼 이모지 ID + * @return 삭제 결과 + * @throws RegisteredRealEmojiNotFoundException 등록한 리얼 이모지가 없는 경우 + */ + @Transactional @Override - public DefaultResponse deleteRealEmoji(String postId, String realEmojiId) { - return new DefaultResponse(true); + public DefaultResponse deletePostRealEmoji(String postId, String realEmojiId) { + String memberId = authenticationHolder.getUserId(); + MemberPost post = memberPostService.getMemberPostById(postId); + MemberPostRealEmoji postRealEmoji = memberPostRealEmojiService + .getMemberPostRealEmojiByRealEmojiIdAndMemberId(realEmojiId, memberId); + + memberPostRealEmojiService.deletePostRealEmoji(postRealEmoji); + post.removeRealEmoji(postRealEmoji); + return DefaultResponse.ok(); } @Override diff --git a/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java index 465bb258..4078b388 100644 --- a/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java +++ b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java @@ -1,5 +1,7 @@ package com.oing.dto.response; + +import com.oing.domain.MemberPostRealEmoji; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "피드 게시물 리얼 이모지 응답") @@ -7,16 +9,20 @@ public record PostRealEmojiResponse( @Schema(description = "피드 게시물 리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") String postRealEmojiId, - @Schema(description = "리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") - String realEmojiId, - - @Schema(description = "피드 게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @Schema(description = "피드 게시물 ID", example = "01HGW2N7EHJUUDIF99RRS2E97") String postId, - @Schema(description = "반응 작성 사용자 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @Schema(description = "리얼 이모지 작성 사용자 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") String memberId, + @Schema(description = "리얼 이모지 ID", example = "01HGW2N7EHJVEFEFEEEEES2E97") + String realEmojiId, + @Schema(description = "피드 게시물 리얼 이모지 이미지 주소", example = "http://test.com/test-profile.jpg") String emojiImageUrl ) { + public static PostRealEmojiResponse from(MemberPostRealEmoji postRealEmoji) { + return new PostRealEmojiResponse(postRealEmoji.getId(), postRealEmoji.getPost().getId(), postRealEmoji.getMemberId(), + postRealEmoji.getRealEmoji().getId(), postRealEmoji.getRealEmoji().getRealEmojiImageUrl()); + } } diff --git a/post/src/main/java/com/oing/exception/RealEmojiAlreadyExistsException.java b/post/src/main/java/com/oing/exception/RealEmojiAlreadyExistsException.java new file mode 100644 index 00000000..cab0884c --- /dev/null +++ b/post/src/main/java/com/oing/exception/RealEmojiAlreadyExistsException.java @@ -0,0 +1,7 @@ +package com.oing.exception; + +public class RealEmojiAlreadyExistsException extends DomainException { + public RealEmojiAlreadyExistsException() { + super(ErrorCode.REAL_EMOJI_ALREADY_EXISTS); + } +} diff --git a/post/src/main/java/com/oing/exception/RegisteredRealEmojiNotFoundException.java b/post/src/main/java/com/oing/exception/RegisteredRealEmojiNotFoundException.java new file mode 100644 index 00000000..a2bbb05f --- /dev/null +++ b/post/src/main/java/com/oing/exception/RegisteredRealEmojiNotFoundException.java @@ -0,0 +1,7 @@ +package com.oing.exception; + +public class RegisteredRealEmojiNotFoundException extends DomainException { + public RegisteredRealEmojiNotFoundException() { + super(ErrorCode.REGISTERED_REAL_EMOJI_NOT_FOUND); + } +} diff --git a/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java index 87a815f9..10661f45 100644 --- a/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java @@ -1,7 +1,14 @@ package com.oing.repository; +import com.oing.domain.MemberPost; import com.oing.domain.MemberPostRealEmoji; +import com.oing.domain.MemberRealEmoji; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberPostRealEmojiRepository extends JpaRepository { + boolean existsByPostAndMemberIdAndRealEmoji(MemberPost post, String memberId, MemberRealEmoji emoji); + + Optional findByRealEmojiIdAndMemberId(String realEmojiId, String memberId); } diff --git a/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java b/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java index 3a2754f7..44fdb15b 100644 --- a/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java +++ b/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java @@ -18,7 +18,7 @@ public interface MemberPostRealEmojiApi { @Operation(summary = "게시물에 리얼 이모지 등록", description = "게시물에 리얼 이모지를 추가합니다.") @PostMapping - DefaultResponse createRealEmoji( + PostRealEmojiResponse createPostRealEmoji( @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable String postId, @@ -30,7 +30,7 @@ DefaultResponse createRealEmoji( @Operation(summary = "게시물에서 리얼 이모지 삭제", description = "게시물에서 리얼 이모지를 삭제합니다.") @DeleteMapping("/{realEmojiId}") - DefaultResponse deleteRealEmoji( + DefaultResponse deletePostRealEmoji( @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable String postId, diff --git a/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java b/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java new file mode 100644 index 00000000..e84e73e3 --- /dev/null +++ b/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java @@ -0,0 +1,55 @@ +package com.oing.service; + +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostRealEmoji; +import com.oing.domain.MemberRealEmoji; +import com.oing.exception.RegisteredRealEmojiNotFoundException; +import com.oing.repository.MemberPostRealEmojiRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberPostRealEmojiService { + private final MemberPostRealEmojiRepository memberPostRealEmojiRepository; + + /** + * 게시물에 리얼 이모지를 저장합니다 + * @param postRealEmoji 리얼 이모지 + * @return 저장된 리얼 이모지 + */ + public MemberPostRealEmoji savePostRealEmoji(MemberPostRealEmoji postRealEmoji) { + return memberPostRealEmojiRepository.save(postRealEmoji); + } + + /** + * 게시물에 등록된 리얼 이모지가 있는지 조회 + * @param post 조회할 포스트 + * @param memberId 회원 아이디 + * @param realEmoji 조회 대상 리얼 이모지 + * @return 존재 여부 + */ + public boolean isMemberPostRealEmojiExists(MemberPost post, String memberId, MemberRealEmoji realEmoji) { + return memberPostRealEmojiRepository.existsByPostAndMemberIdAndRealEmoji(post, memberId, realEmoji); + } + + /** + * 게시물에 등록된 리얼 이모지를 반환 + * @param realEmojiId 리얼 이모지 아이디 + * @param memberId 회원 아이디 + * @return 게시물에 등록된 리얼 이모지 + * @throws RegisteredRealEmojiNotFoundException 등록된 리얼 이모지가 없는 경우 + */ + public MemberPostRealEmoji getMemberPostRealEmojiByRealEmojiIdAndMemberId(String realEmojiId, String memberId) { + return memberPostRealEmojiRepository.findByRealEmojiIdAndMemberId(realEmojiId, memberId) + .orElseThrow(RegisteredRealEmojiNotFoundException::new); + } + + /** + * 게시물에 등록된 리얼 이모지를 삭제 + * @param postRealEmoji 리얼 이모지 + */ + public void deletePostRealEmoji(MemberPostRealEmoji postRealEmoji) { + memberPostRealEmojiRepository.delete(postRealEmoji); + } +} diff --git a/post/src/main/java/com/oing/service/MemberRealEmojiService.java b/post/src/main/java/com/oing/service/MemberRealEmojiService.java index 640c19ea..14597d6b 100644 --- a/post/src/main/java/com/oing/service/MemberRealEmojiService.java +++ b/post/src/main/java/com/oing/service/MemberRealEmojiService.java @@ -15,6 +15,12 @@ public class MemberRealEmojiService { private final MemberRealEmojiRepository memberRealEmojiRepository; + + public MemberRealEmoji getMemberRealEmojiById(String realEmojiId) { + return memberRealEmojiRepository.findById(realEmojiId) + .orElseThrow(RealEmojiNotFoundException::new); + } + public MemberRealEmoji save(MemberRealEmoji emoji) { return memberRealEmojiRepository.save(emoji); } @@ -34,4 +40,5 @@ public boolean findRealEmojiByEmojiType(Emoji emoji) { public List findRealEmojisByMemberId(String memberId) { return memberRealEmojiRepository.findAllByMemberId(memberId); } + } diff --git a/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java b/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java new file mode 100644 index 00000000..1ae091fa --- /dev/null +++ b/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java @@ -0,0 +1,151 @@ +package com.oing.controller; + +import com.oing.domain.Emoji; +import com.oing.domain.MemberPost; +import com.oing.domain.MemberPostRealEmoji; +import com.oing.domain.MemberRealEmoji; +import com.oing.dto.request.PostRealEmojiRequest; +import com.oing.dto.response.PostRealEmojiResponse; +import com.oing.exception.AuthorizationFailedException; +import com.oing.exception.RealEmojiAlreadyExistsException; +import com.oing.exception.RegisteredRealEmojiNotFoundException; +import com.oing.service.MemberBridge; +import com.oing.service.MemberPostRealEmojiService; +import com.oing.service.MemberPostService; +import com.oing.service.MemberRealEmojiService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MemberPostRealEmojiControllerTest { + @InjectMocks + private MemberPostRealEmojiController memberPostRealEmojiController; + + @Mock + private AuthenticationHolder authenticationHolder; + @Mock + private IdentityGenerator identityGenerator; + @Mock + private MemberPostService memberPostService; + @Mock + private MemberPostRealEmojiService memberPostRealEmojiService; + @Mock + private MemberRealEmojiService memberRealEmojiService; + @Mock + private MemberBridge memberBridge; + + @Test + void 게시물_리얼이모지_등록_테스트() { + //given + String memberId = "1"; + when(authenticationHolder.getUserId()).thenReturn(memberId); + when(memberBridge.isInSameFamily(memberId, memberId)).thenReturn(true); + MemberPost post = new MemberPost("1", memberId, "https://oing.com/post.jpg", "post.jpg", + "안녕.오잉."); + MemberRealEmoji realEmoji = new MemberRealEmoji("1", memberId, + Emoji.EMOJI_1, "https://oing.com/emoji.jpg", "emoji.jpg"); + when(memberPostService.getMemberPostById(post.getId())).thenReturn(post); + when(memberRealEmojiService.getMemberRealEmojiById(realEmoji.getId())).thenReturn(realEmoji); + + MemberPostRealEmoji postRealEmoji = new MemberPostRealEmoji("1", realEmoji, post, memberId); + when(memberPostRealEmojiService.savePostRealEmoji(any(MemberPostRealEmoji.class))).thenReturn(postRealEmoji); + when(identityGenerator.generateIdentity()).thenReturn(postRealEmoji.getId()); + PostRealEmojiRequest request = new PostRealEmojiRequest(realEmoji.getId()); + + //when + PostRealEmojiResponse response = memberPostRealEmojiController.createPostRealEmoji(post.getId(), request); + + //then + assertEquals(post.getId(), response.postId()); + assertEquals(request.realEmojiId(), response.realEmojiId()); + } + + @Test + void 권한없는_memberId로_게시물_리얼이모지_등록_예외_테스트() { + // given + String memberId = "1"; + when(authenticationHolder.getUserId()).thenReturn(memberId); + MemberPost post = new MemberPost("1", memberId, "https://oing.com/post.jpg", "post.jpg", + "안녕.오잉."); + MemberRealEmoji realEmoji = new MemberRealEmoji("1", memberId, + Emoji.EMOJI_1, "https://oing.com/emoji.jpg", "emoji.jpg"); + when(memberPostService.getMemberPostById(post.getId())).thenReturn(post); + + //when + when(memberBridge.isInSameFamily(memberId, memberId)).thenReturn(false); + PostRealEmojiRequest request = new PostRealEmojiRequest(realEmoji.getId()); + + // then + assertThrows(AuthorizationFailedException.class, + () -> memberPostRealEmojiController.createPostRealEmoji(post.getId(), request)); + } + + @Test + void 게시물_중복된_리얼이모지_등록_예외_테스트() { + //given + String memberId = "1"; + when(authenticationHolder.getUserId()).thenReturn(memberId); + when(memberBridge.isInSameFamily(memberId, memberId)).thenReturn(true); + MemberPost post = new MemberPost("1", memberId, "https://oing.com/post.jpg", "post.jpg", + "안녕.오잉."); + MemberRealEmoji realEmoji = new MemberRealEmoji("1", memberId, Emoji.EMOJI_1, + "https://oing.com/emoji.jpg", "emoji.jpg"); + when(memberPostService.getMemberPostById(post.getId())).thenReturn(post); + when(memberRealEmojiService.getMemberRealEmojiById(realEmoji.getId())).thenReturn(realEmoji); + + //when + when(memberPostRealEmojiService.isMemberPostRealEmojiExists(post, memberId, realEmoji)).thenReturn(true); + PostRealEmojiRequest request = new PostRealEmojiRequest(realEmoji.getId()); + + //then + assertThrows(RealEmojiAlreadyExistsException.class, + () -> memberPostRealEmojiController.createPostRealEmoji(post.getId(), request)); + } + + @Test + void 게시물_리얼이모지_삭제_테스트() { + //given + String memberId = "1"; + MemberPost post = new MemberPost("1", memberId, "https://oing.com/post.jpg", "post.jpg", + "안녕.오잉."); + MemberRealEmoji realEmoji = new MemberRealEmoji("1", memberId, + Emoji.EMOJI_1, "https://oing.com/emoji.jpg", "emoji.jpg"); + when(memberPostService.getMemberPostById(post.getId())).thenReturn(post); + + //when + memberPostRealEmojiController.deletePostRealEmoji(post.getId(), realEmoji.getId()); + + //then + //nothing. just check no exception + } + + @Test + void 게시물_등록되지_않은_리얼이모지_삭제_예외_테스트() { + //given + String memberId = "1"; + when(authenticationHolder.getUserId()).thenReturn(memberId); + MemberPost post = new MemberPost("1", memberId, "https://oing.com/post.jpg", "post.jpg", + "안녕.오잉."); + MemberRealEmoji realEmoji = new MemberRealEmoji("1", memberId, + Emoji.EMOJI_1, "https://oing.com/emoji.jpg", "emoji.jpg"); + when(memberPostService.getMemberPostById(post.getId())).thenReturn(post); + + //when + when(memberPostRealEmojiService.getMemberPostRealEmojiByRealEmojiIdAndMemberId("1", memberId)) + .thenThrow(RegisteredRealEmojiNotFoundException.class); + + //then + assertThrows(RegisteredRealEmojiNotFoundException.class, + () -> memberPostRealEmojiController.deletePostRealEmoji(post.getId(), realEmoji.getId())); + } +} From c6940b2c10865280566690f515fd1a85c54c728f Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Thu, 18 Jan 2024 09:53:46 +0900 Subject: [PATCH 22/49] =?UTF-8?q?[OING-158]=20feat:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=20=EB=A6=AC=EC=96=BC=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20(#113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add getMemberPostRealEmoji api * style: change dto name * refactor: refact getPostRealEmojiMembers code * test: add MemberPostRealEmojiController get api unit test * test: add MemberPostRealEmojiController get api integration test --- .../restapi/MemberPostRealEmojiApiTest.java | 80 ++++++++++++++++- .../MemberPostReactionController.java | 4 +- .../MemberPostRealEmojiController.java | 88 +++++++++++++++---- ...e.java => PostReactionMemberResponse.java} | 2 +- .../response/PostRealEmojiMemberResponse.java | 13 +++ .../PostRealEmojiSummaryResponse.java | 25 ++++++ .../oing/restapi/MemberPostReactionApi.java | 2 +- .../oing/restapi/MemberPostRealEmojiApi.java | 20 ++++- .../MemberPostReactionControllerTest.java | 4 +- .../MemberPostRealEmojiControllerTest.java | 55 ++++++++++++ 10 files changed, 265 insertions(+), 28 deletions(-) rename post/src/main/java/com/oing/dto/response/{PostReactionsResponse.java => PostReactionMemberResponse.java} (88%) create mode 100644 post/src/main/java/com/oing/dto/response/PostRealEmojiMemberResponse.java create mode 100644 post/src/main/java/com/oing/dto/response/PostRealEmojiSummaryResponse.java diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java index 1cec15e5..9fa60d1a 100644 --- a/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java @@ -21,8 +21,7 @@ import java.time.LocalDate; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -109,4 +108,81 @@ void setUp() { .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)); } + + @Test + void 게시물_리얼이모지_요약_조회_테스트() throws Exception { + //given + PostRealEmojiRequest request = new PostRealEmojiRequest(TEST_REAL_EMOJI_ID); + mockMvc.perform( + post("/v1/posts/{postId}/real-emoji", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/{postId}/real-emoji/summary", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.postId").value(TEST_POST_ID)) + .andExpect(jsonPath("$.results[0].realEmojiId").value(TEST_REAL_EMOJI_ID)) + .andExpect(jsonPath("$.results[0].count").value(1)); + } + + @Test + void 게시물_리얼이모지_목록_조회_테스트() throws Exception { + //given + PostRealEmojiRequest request = new PostRealEmojiRequest(TEST_REAL_EMOJI_ID); + mockMvc.perform( + post("/v1/posts/{postId}/real-emoji", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/{postId}/real-emoji", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].postId").value(TEST_POST_ID)) + .andExpect(jsonPath("$.results[0].memberId").value(TEST_MEMBER_ID)) + .andExpect(jsonPath("$.results[0].realEmojiId").value(TEST_REAL_EMOJI_ID)) + .andExpect(jsonPath("$.results[0].emojiImageUrl").value("https://test.com/bucket/real-emoji.jpg")); + } + + @Test + void 게시물_리얼이모지_남긴_멤버_조회_테스트() throws Exception { + //given + PostRealEmojiRequest request = new PostRealEmojiRequest(TEST_REAL_EMOJI_ID); + mockMvc.perform( + post("/v1/posts/{postId}/real-emoji", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/{postId}/real-emoji/member", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.emojiMemberIdsList['01HGW2N7EHJVJ4CJ999RRS2A97'][0]").value(TEST_MEMBER_ID)); + } } diff --git a/post/src/main/java/com/oing/controller/MemberPostReactionController.java b/post/src/main/java/com/oing/controller/MemberPostReactionController.java index d95dad82..94961aeb 100644 --- a/post/src/main/java/com/oing/controller/MemberPostReactionController.java +++ b/post/src/main/java/com/oing/controller/MemberPostReactionController.java @@ -106,7 +106,7 @@ public ArrayResponse getPostReactions(String postId) { @Override @Transactional - public PostReactionsResponse getPostReactionMembers(String postId) { + public PostReactionMemberResponse getPostReactionMembers(String postId) { List reactions = memberPostReactionService.getMemberPostReactionsByPostId(postId); List emojiList = Emoji.getEmojiList(); @@ -117,6 +117,6 @@ public PostReactionsResponse getPostReactionMembers(String postId) { )); emojiList.forEach(emoji -> emojiMemberIdsMap.putIfAbsent(emoji.getTypeKey(), Collections.emptyList())); - return new PostReactionsResponse(emojiMemberIdsMap); + return new PostReactionMemberResponse(emojiMemberIdsMap); } } diff --git a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java index bac39ce9..a5d3626c 100644 --- a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java +++ b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java @@ -5,9 +5,7 @@ import com.oing.domain.MemberPostRealEmoji; import com.oing.domain.MemberRealEmoji; import com.oing.dto.request.PostRealEmojiRequest; -import com.oing.dto.response.ArrayResponse; -import com.oing.dto.response.DefaultResponse; -import com.oing.dto.response.PostRealEmojiResponse; +import com.oing.dto.response.*; import com.oing.exception.AuthorizationFailedException; import com.oing.exception.RealEmojiAlreadyExistsException; import com.oing.exception.RegisteredRealEmojiNotFoundException; @@ -22,13 +20,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; -import java.util.Collections; -import com.oing.restapi.MemberPostRealEmojiApi; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Controller; - -import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @RequiredArgsConstructor @Controller @@ -92,17 +86,77 @@ public DefaultResponse deletePostRealEmoji(String postId, String realEmojiId) { return DefaultResponse.ok(); } + /** + * 게시물에 등록된 리얼 이모지 요약을 조회합니다 + * @param postId 게시물 ID + * @return 리얼 이모지 요약 + */ + @Override + @Transactional + public PostRealEmojiSummaryResponse getPostRealEmojiSummary(String postId) { + MemberPost post = memberPostService.findMemberPostById(postId); + List results = post.getRealEmojis() + .stream() + .collect(Collectors.groupingBy(MemberPostRealEmoji::getRealEmoji)) + .values() + .stream().map(element -> + new PostRealEmojiSummaryResponse.PostRealEmojiSummaryResponseElement( + element.get(0).getRealEmoji().getId(), + element.size() + ) + ) + .toList(); + return new PostRealEmojiSummaryResponse( + post.getId(), + results + ); + } + + /** + * 게시물에 등록된 리얼 이모지 목록을 조회합니다 + * @param postId 게시물 ID + * @return 리얼 이모지 목록 + */ + @Transactional @Override public ArrayResponse getPostRealEmojis(String postId) { - List mockResponses = Arrays.asList( - new PostRealEmojiResponse("01HGW2N7EHJVJ4CJ999RRS2E97", "emoji_1", postId, - "01HUUDFAOHJVJ4CJ999RRS2E97", "http://test.com/images/test1.jpg"), - new PostRealEmojiResponse("01HGW2N7EHJVJ4CJ999RRS2E97", "emoji_2", postId, - "01HGW2N7EHJVJ4CJ999RRS2E97", "http://test.com/images/test2.jpg"), - new PostRealEmojiResponse("01DGW2N7EFFFEDFAG9RRS2E976", "emoji_2", postId, - "01HGW2N7EHJVJEEFD99RRS2E97", "http://test.com/images/test2.jpg") + MemberPost post = memberPostService.getMemberPostById(postId); + return ArrayResponse.of(post.getRealEmojis().stream() + .map(PostRealEmojiResponse::from) + .toList() ); + } + + /** + * 게시물에 등록된 리얼 이모지를 남긴 멤버 목록을 조회합니다 + * @param postId 게시물 ID + * @return 리얼 이모지를 남긴 멤버 목록 + */ + @Transactional + @Override + public PostRealEmojiMemberResponse getPostRealEmojiMembers(String postId) { + MemberPost post = memberPostService.getMemberPostById(postId); - return ArrayResponse.of(mockResponses); + Map> realEmojiMemberMap = groupByRealEmoji(post.getRealEmojis()); + Map> result = realEmojiMemberMap.entrySet() + .stream() + .collect(Collectors.toMap( + entry -> entry.getKey().getId(), + Map.Entry::getValue + )); + return new PostRealEmojiMemberResponse(result); + } + + /** + * 리얼 이모지를 남긴 멤버 목록을 리얼 이모지 별로 그룹화합니다 + * @param realEmojis 리얼 이모지 목록 + * @return 리얼 이모지 별로 그룹화된 멤버 목록 + */ + private Map> groupByRealEmoji(List realEmojis) { + return realEmojis.stream() + .collect(Collectors.groupingBy( + MemberPostRealEmoji::getRealEmoji, + Collectors.mapping(MemberPostRealEmoji::getMemberId, Collectors.toList()) + )); } } diff --git a/post/src/main/java/com/oing/dto/response/PostReactionsResponse.java b/post/src/main/java/com/oing/dto/response/PostReactionMemberResponse.java similarity index 88% rename from post/src/main/java/com/oing/dto/response/PostReactionsResponse.java rename to post/src/main/java/com/oing/dto/response/PostReactionMemberResponse.java index d95c8367..ab7446ce 100644 --- a/post/src/main/java/com/oing/dto/response/PostReactionsResponse.java +++ b/post/src/main/java/com/oing/dto/response/PostReactionMemberResponse.java @@ -6,7 +6,7 @@ import java.util.Map; @Schema(description = "피드 게시물 이모지 응답") -public record PostReactionsResponse( +public record PostReactionMemberResponse( @Schema(description = "이모지를 누른 사용자 ID 목록") Map> emojiMemberIdsList ) { diff --git a/post/src/main/java/com/oing/dto/response/PostRealEmojiMemberResponse.java b/post/src/main/java/com/oing/dto/response/PostRealEmojiMemberResponse.java new file mode 100644 index 00000000..7bb426b8 --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/PostRealEmojiMemberResponse.java @@ -0,0 +1,13 @@ +package com.oing.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; +import java.util.Map; + +@Schema(description = "피드 게시물 이모지 응답") +public record PostRealEmojiMemberResponse( + @Schema(description = "이모지를 누른 사용자 ID 목록") + Map> emojiMemberIdsList +) { +} diff --git a/post/src/main/java/com/oing/dto/response/PostRealEmojiSummaryResponse.java b/post/src/main/java/com/oing/dto/response/PostRealEmojiSummaryResponse.java new file mode 100644 index 00000000..b839e255 --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/PostRealEmojiSummaryResponse.java @@ -0,0 +1,25 @@ +package com.oing.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "피드 게시물 리얼 이모지 요약") +public record PostRealEmojiSummaryResponse( + @Schema(description = "피드 게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String postId, + + @Schema(description = "피드 게시물 리얼 이모지 요약", example = "") + List results +) { + @Schema(description = "피드 게시물 반응 요약 내용") + public static record PostRealEmojiSummaryResponseElement( + @Schema(description = "리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String realEmojiId, + + @Schema(description = "반응 개수", example = "3") + int count + ) { + + } +} diff --git a/post/src/main/java/com/oing/restapi/MemberPostReactionApi.java b/post/src/main/java/com/oing/restapi/MemberPostReactionApi.java index 2757f1c0..9d1133ca 100644 --- a/post/src/main/java/com/oing/restapi/MemberPostReactionApi.java +++ b/post/src/main/java/com/oing/restapi/MemberPostReactionApi.java @@ -55,7 +55,7 @@ ArrayResponse getPostReactions( @Operation(summary = "게시물 반응을 남긴 전체 멤버 조회", description = "게시물에 반응을 남긴 모든 멤버 목록을 조회합니다.") @GetMapping("/member") - PostReactionsResponse getPostReactionMembers( + PostReactionMemberResponse getPostReactionMembers( @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") @PathVariable String postId diff --git a/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java b/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java index 44fdb15b..640da1be 100644 --- a/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java +++ b/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java @@ -1,9 +1,7 @@ package com.oing.restapi; import com.oing.dto.request.PostRealEmojiRequest; -import com.oing.dto.response.ArrayResponse; -import com.oing.dto.response.DefaultResponse; -import com.oing.dto.response.PostRealEmojiResponse; +import com.oing.dto.response.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -40,6 +38,14 @@ DefaultResponse deletePostRealEmoji( String realEmojiId ); + @Operation(summary = "게시물의 리얼 이모지 요약 조회", description = "게시물에 달린 리얼 이모지 요약을 조회합니다.") + @GetMapping("/summary") + PostRealEmojiSummaryResponse getPostRealEmojiSummary( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId + ); + @Operation(summary = "게시물의 리얼 이모지 전체 조회", description = "게시물에 달린 모든 리얼 이모지 목록을 조회합니다.") @GetMapping ArrayResponse getPostRealEmojis( @@ -47,4 +53,12 @@ ArrayResponse getPostRealEmojis( @PathVariable String postId ); + + @Operation(summary = "게시물의 리얼 이모지를 남긴 전체 멤버 조회", description = "게시물에 리얼 이모지를 남긴 모든 멤버 목록을 조회합니다.") + @GetMapping("/member") + PostRealEmojiMemberResponse getPostRealEmojiMembers( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId + ); } diff --git a/post/src/test/java/com/oing/controller/MemberPostReactionControllerTest.java b/post/src/test/java/com/oing/controller/MemberPostReactionControllerTest.java index 542da9e1..0b3c7398 100644 --- a/post/src/test/java/com/oing/controller/MemberPostReactionControllerTest.java +++ b/post/src/test/java/com/oing/controller/MemberPostReactionControllerTest.java @@ -4,7 +4,7 @@ import com.oing.domain.MemberPost; import com.oing.domain.MemberPostReaction; import com.oing.dto.request.PostReactionRequest; -import com.oing.dto.response.PostReactionsResponse; +import com.oing.dto.response.PostReactionMemberResponse; import com.oing.exception.EmojiAlreadyExistsException; import com.oing.exception.EmojiNotFoundException; import com.oing.service.MemberPostReactionService; @@ -122,7 +122,7 @@ public class MemberPostReactionControllerTest { when(memberPostReactionService.getMemberPostReactionsByPostId(post.getId())).thenReturn(mockReactions); //when - PostReactionsResponse response = memberPostReactionController.getPostReactionMembers(post.getId()); + PostReactionMemberResponse response = memberPostReactionController.getPostReactionMembers(post.getId()); //then assertTrue(response.emojiMemberIdsList().get(Emoji.EMOJI_1.getTypeKey()).contains(memberId)); diff --git a/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java b/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java index 1ae091fa..c0313fab 100644 --- a/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java +++ b/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java @@ -5,7 +5,10 @@ import com.oing.domain.MemberPostRealEmoji; import com.oing.domain.MemberRealEmoji; import com.oing.dto.request.PostRealEmojiRequest; +import com.oing.dto.response.ArrayResponse; +import com.oing.dto.response.PostRealEmojiMemberResponse; import com.oing.dto.response.PostRealEmojiResponse; +import com.oing.dto.response.PostRealEmojiSummaryResponse; import com.oing.exception.AuthorizationFailedException; import com.oing.exception.RealEmojiAlreadyExistsException; import com.oing.exception.RegisteredRealEmojiNotFoundException; @@ -15,17 +18,23 @@ import com.oing.service.MemberRealEmojiService; import com.oing.util.AuthenticationHolder; import com.oing.util.IdentityGenerator; +import jakarta.transaction.Transactional; 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.springframework.test.context.ActiveProfiles; + +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +@Transactional +@ActiveProfiles("test") @ExtendWith(MockitoExtension.class) public class MemberPostRealEmojiControllerTest { @InjectMocks @@ -148,4 +157,50 @@ public class MemberPostRealEmojiControllerTest { assertThrows(RegisteredRealEmojiNotFoundException.class, () -> memberPostRealEmojiController.deletePostRealEmoji(post.getId(), realEmoji.getId())); } + + @Test + void 게시물_리얼이모지_요약_조회_테스트() { + //given + String memberId = "1"; + MemberPost post = new MemberPost("1", memberId, "https://oing.com/post.jpg", "post.jpg", + "안녕.오잉."); + when(memberPostService.findMemberPostById(post.getId())).thenReturn(post); + + //when + PostRealEmojiSummaryResponse summary = memberPostRealEmojiController.getPostRealEmojiSummary(post.getId()); + + //then + assertEquals(0, summary.results().size()); + } + + @Test + void 게시물_리얼이모지_목록_조회_테스트() { + //given + String memberId = "1"; + MemberPost post = new MemberPost("1", memberId, "https://oing.com/post.jpg", "post.jpg", + "안녕.오잉."); + when(memberPostService.getMemberPostById(post.getId())).thenReturn(post); + + //when + ArrayResponse response = memberPostRealEmojiController.getPostRealEmojis(post.getId()); + + //then + assertEquals(0, response.results().size()); + assertEquals(List.of(), response.results()); + } + + @Test + void 게시물_리얼이모지_멤버_조회_테스트() { + //given + String memberId = "1"; + MemberPost post = new MemberPost("1", memberId, "https://oing.com/post.jpg", "post.jpg", + "안녕.오잉."); + when(memberPostService.getMemberPostById(post.getId())).thenReturn(post); + + //when + PostRealEmojiMemberResponse response = memberPostRealEmojiController.getPostRealEmojiMembers(post.getId()); + + //then + assertEquals(0, response.emojiMemberIdsList().size()); + } } From 653e87f4a9add3d6142c3565214ac05b9bcb8dbc Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:17:30 +0900 Subject: [PATCH 23/49] =?UTF-8?q?[OING-162]=20test:=20Post=20PresignedUrl/?= =?UTF-8?q?POST/DELETE=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add MemberPostController test * test: add MemberPostController integration test * test: add MemberPostRepository test --- .../MemberPostRepositoryCustomTest.java | 24 ++++ .../com/oing/restapi/MemberPostApiTest.java | 126 ++++++++++++++++++ .../oing/controller/MemberPostController.java | 1 + .../controller/MemberPostControllerTest.java | 64 +++++++++ 4 files changed, 215 insertions(+) create mode 100644 gateway/src/test/java/com/oing/restapi/MemberPostApiTest.java create mode 100644 post/src/test/java/com/oing/controller/MemberPostControllerTest.java diff --git a/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java b/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java index 1892a5b6..632b89f0 100644 --- a/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java +++ b/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java @@ -110,4 +110,28 @@ void setup() { .extracting(MemberPostDailyCalendarDTO::dailyPostCount) .containsExactly(2L, 1L); } + + @Test + void 특정_날짜에_게시글이_존재하는지_확인한다() { + // given + LocalDate postDate = LocalDate.of(2023, 11, 1); + + // when + boolean exists = memberPostRepositoryCustomImpl.existsByMemberIdAndCreatedAt(testMember1.getId(), postDate); + + // then + assertThat(exists).isTrue(); + } + + @Test + void 특정_날짜에_게시글이_존재하지_않는지_확인한다() { + // given + LocalDate postDate = LocalDate.of(2023, 11, 8); + + // when + boolean exists = memberPostRepositoryCustomImpl.existsByMemberIdAndCreatedAt(testMember1.getId(), postDate); + + // then + assertThat(exists).isFalse(); + } } diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostApiTest.java new file mode 100644 index 00000000..a42fe7e4 --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MemberPostApiTest.java @@ -0,0 +1,126 @@ +package com.oing.restapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.domain.Member; +import com.oing.domain.MemberPost; +import com.oing.dto.request.CreatePostRequest; +import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.repository.MemberPostRepository; +import com.oing.repository.MemberRepository; +import com.oing.service.TokenGenerator; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.time.ZonedDateTime; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class MemberPostApiTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private TokenGenerator tokenGenerator; + @Autowired + private ObjectMapper objectMapper; + + private String TEST_MEMBER_ID = "01HGW2N7EHJVJ4CJ999RRS2E97"; + private String TEST_POST_ID = "01HGW2N7EHJVJ4CJ999RRS2A97"; + private String TEST_MEMBER_TOKEN; + + @Autowired + private MemberRepository memberRepository; + @Autowired + private MemberPostRepository memberPostRepository; + + @BeforeEach + void setUp() { + memberRepository.save( + new Member( + TEST_MEMBER_ID, + "testUser1", + LocalDate.now(), + "", "", "" + ) + ); + TEST_MEMBER_TOKEN = tokenGenerator + .generateTokenPair(TEST_MEMBER_ID) + .accessToken(); + } + + @Test + void 게시물_이미지_업로드_URL_요청_테스트() throws Exception { + //given + String imageName = "feed.jpg"; + + //when + ResultActions resultActions = mockMvc.perform( + post("/v1/posts/image-upload-request") + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new PreSignedUrlRequest(imageName))) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.url").exists()); + } + + @Test + void 게시물_추가_테스트() throws Exception { + //given + CreatePostRequest request = new CreatePostRequest("https://test.com/bucket/images/feed.jpg", + "content", ZonedDateTime.now()); + + //when + ResultActions resultActions = mockMvc.perform( + post("/v1/posts") + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.authorId").value(TEST_MEMBER_ID)) + .andExpect(jsonPath("$.imageUrl").value(request.imageUrl())) + .andExpect(jsonPath("$.content").value(request.content())); + } + + @Test + void 게시물_삭제_테스트() throws Exception { + //given + memberPostRepository.save(new MemberPost(TEST_POST_ID, TEST_MEMBER_ID, "img", "img", + "content")); + + //when + ResultActions resultActions = mockMvc.perform( + delete("/v1/posts/{postId}", TEST_POST_ID) + .header("X-AUTH-TOKEN", TEST_MEMBER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } +} diff --git a/post/src/main/java/com/oing/controller/MemberPostController.java b/post/src/main/java/com/oing/controller/MemberPostController.java index 92369b8a..29a8d617 100644 --- a/post/src/main/java/com/oing/controller/MemberPostController.java +++ b/post/src/main/java/com/oing/controller/MemberPostController.java @@ -41,6 +41,7 @@ public class MemberPostController implements MemberPostApi { private final MemberPostService memberPostService; private final MemberBridge memberBridge; + @Transactional @Override public PreSignedUrlResponse requestPresignedUrl(PreSignedUrlRequest request) { String imageName = request.imageName(); diff --git a/post/src/test/java/com/oing/controller/MemberPostControllerTest.java b/post/src/test/java/com/oing/controller/MemberPostControllerTest.java new file mode 100644 index 00000000..1472cbe0 --- /dev/null +++ b/post/src/test/java/com/oing/controller/MemberPostControllerTest.java @@ -0,0 +1,64 @@ +package com.oing.controller; + +import com.oing.domain.MemberPost; +import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.dto.response.PreSignedUrlResponse; +import com.oing.service.MemberBridge; +import com.oing.service.MemberPostService; +import com.oing.util.AuthenticationHolder; +import com.oing.util.IdentityGenerator; +import com.oing.util.PreSignedUrlGenerator; +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 static org.hibernate.validator.internal.util.Contracts.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MemberPostControllerTest { + @InjectMocks + private MemberPostController memberPostController; + + @Mock + private AuthenticationHolder authenticationHolder; + @Mock + private IdentityGenerator identityGenerator; + @Mock + private MemberPostService memberPostService; + @Mock + private MemberBridge memberBridge; + @Mock + private PreSignedUrlGenerator preSignedUrlGenerator; + + @Test + void 피드_이미지_업로드_URL_요청_테스트() { + // given + String newFeedImage = "feed.jpg"; + + // when + PreSignedUrlRequest request = new PreSignedUrlRequest(newFeedImage); + PreSignedUrlResponse dummyResponse = new PreSignedUrlResponse("https://test.com/presigend-request-url"); + when(preSignedUrlGenerator.getFeedPreSignedUrl(any())).thenReturn(dummyResponse); + PreSignedUrlResponse response = memberPostController.requestPresignedUrl(request); + + // then + assertNotNull(response.url()); + } + + @Test + void 피드_삭제_테스트() { + // given + String memberId = "1"; + MemberPost post = new MemberPost("1", memberId, "1", "1", "1"); + + // when + memberPostController.deletePost(post.getId()); + + // then + // nothing. just check no exception + } +} From fc5cc972d47920f0350681a8fc48966ccb112c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC=20=28YeongMin=20Song=29?= Date: Sat, 20 Jan 2024 15:16:32 +0900 Subject: [PATCH 24/49] docs: update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 572b69da..f418b1a8 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ In the precious time spent with family, everything feels more special with Pippy CChuYong
Yeongmin Song

백엔드 개발(파트장) - CChuYong
Jisoo Lim

백엔드 개발 - CChuYong
Soonchan Kwon

백엔드 개발 + CChuYong
Jisoo Lim

백엔드 개발 + CChuYong
Soonchan Kwon

백엔드 개발 From 02f47537a2beddabd05504c6ca51cbbb3f247870 Mon Sep 17 00:00:00 2001 From: Kwon770 Date: Sun, 21 Jan 2024 00:52:52 +0900 Subject: [PATCH 25/49] fix: Remove not used request to fix broken test code --- .../oing/controller/CalendarController.java | 18 ---- .../java/com/oing/restapi/CalendarApi.java | 13 --- .../controller/CalendarControllerTest.java | 79 ---------------- .../com/oing/restapi/CalendarApiTest.java | 91 ------------------- 4 files changed, 201 deletions(-) diff --git a/gateway/src/main/java/com/oing/controller/CalendarController.java b/gateway/src/main/java/com/oing/controller/CalendarController.java index 2e3e87a3..f3b1b2c4 100644 --- a/gateway/src/main/java/com/oing/controller/CalendarController.java +++ b/gateway/src/main/java/com/oing/controller/CalendarController.java @@ -10,13 +10,10 @@ import com.oing.service.MemberService; import com.oing.util.OptimizedImageUrlGenerator; import lombok.RequiredArgsConstructor; -import org.springframework.cglib.core.Local; import org.springframework.stereotype.Controller; -import java.time.DayOfWeek; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.time.temporal.WeekFields; import java.util.List; import java.util.stream.IntStream; @@ -67,21 +64,6 @@ private List getCalendarResponses(List familyIds, Loca return mapPostToCalendar(representativePosts, calendarDTOs, familyIds.size()); } - @Override - public ArrayResponse getWeeklyCalendar(String yearMonth, Integer week) { - if (yearMonth == null) yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); - if (week == null) week = LocalDate.now().get(WeekFields.of(DayOfWeek.MONDAY, 1).weekOfMonth()); - - // 1주 = 해당 주차 (+ 0), 2주 이상 = 주차 추가 (+ (week - 1)) - LocalDate startDate = LocalDate.parse(yearMonth + "-01").plusWeeks(week - 1); // yyyy-MM-dd 패턴으로 파싱 - LocalDate endDate = startDate.plusWeeks(1); - List familyIds = getFamilyIds(); - - - List calendarResponses = getCalendarResponses(familyIds, startDate, endDate); - return new ArrayResponse<>(calendarResponses); - } - @Override public ArrayResponse getMonthlyCalendar(String yearMonth) { if (yearMonth == null) yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); diff --git a/gateway/src/main/java/com/oing/restapi/CalendarApi.java b/gateway/src/main/java/com/oing/restapi/CalendarApi.java index 664efa43..50b6b31d 100644 --- a/gateway/src/main/java/com/oing/restapi/CalendarApi.java +++ b/gateway/src/main/java/com/oing/restapi/CalendarApi.java @@ -22,19 +22,6 @@ @Valid @RequestMapping("/v1/calendar") public interface CalendarApi { - @Operation(summary = "주별 캘린더 조회", description = "주별 캘린더를 조회합니다.", parameters = { - @Parameter(name = "type", description = "캘린더 타입 (WEEKLY, MONTHLY)", example = "WEEKLY", required = true) - }) - @GetMapping(params = {"type=WEEKLY"}) - ArrayResponse getWeeklyCalendar( - @RequestParam(required = false) - @Parameter(description = "조회할 년월", example = "2021-12") - String yearMonth, - - @RequestParam(required = false) - @Parameter(description = "조회할 주차", example = "1") - Integer week - ); @Operation(summary = "월별 캘린더 조회", description = "월별 캘린더를 조회합니다.") @GetMapping(params = {"type=MONTHLY"}) diff --git a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java index 69cd0ae7..93ec0a2e 100644 --- a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java +++ b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java @@ -64,85 +64,6 @@ class CalendarControllerTest { private final List familyIds = List.of(testMember1.getId(), testMember2.getId()); - @Test - void 주간_캘린더_조회_테스트() { - // Given - String yearMonth = "2023-11"; - Integer week = 1; - - LocalDate startDate = LocalDate.of(2023, 11, 1); - LocalDate endDate = startDate.plusWeeks(1); - MemberPost testPost1 = new MemberPost( - "1", - testMember1.getId(), - "post.com/1", - "1", - "test1" - ); - ReflectionTestUtils.setField(testPost1, "createdAt", LocalDateTime.of(2023, 11, 1, 13, 0)); - MemberPost testPost2 = new MemberPost( - "2", - testMember2.getId(), - "post.com/2", - "2", - "test2" - ); - ReflectionTestUtils.setField(testPost2, "createdAt", LocalDateTime.of(2023, 11, 2, 13, 0)); - List representativePosts = List.of(testPost1, testPost2); - List calendarDTOs = List.of( - new MemberPostDailyCalendarDTO(2L), - new MemberPostDailyCalendarDTO(1L) - ); - when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); - when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); - when(memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate)).thenReturn(representativePosts); - when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); - - // When - ArrayResponse weeklyCalendar = calendarController.getWeeklyCalendar(yearMonth, week); - - // Then - assertThat(weeklyCalendar.results()) - .extracting(CalendarResponse::representativePostId, CalendarResponse::allFamilyMembersUploaded) - .containsExactly( - Tuple.tuple("1", true), - Tuple.tuple("2", false) - ); - } - - @Test - void 주간_캘린더_파라미터_없이_조회_테스트() { - // Given - LocalDate startDate = LocalDate.now(); - LocalDate endDate = startDate.plusWeeks(1); - MemberPost testPost1 = new MemberPost( - "1", - testMember1.getId(), - "post.com/1", - "1", - "test1" - ); - ReflectionTestUtils.setField(testPost1, "createdAt", LocalDateTime.now()); - List representativePosts = List.of(testPost1); - List calendarDTOs = List.of( - new MemberPostDailyCalendarDTO(1L) - ); - when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); - when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); - when(memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate)).thenReturn(representativePosts); - when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); - - // When - ArrayResponse weeklyCalendar = calendarController.getWeeklyCalendar(null, null); - - // Then - assertThat(weeklyCalendar.results()) - .extracting(CalendarResponse::representativePostId, CalendarResponse::allFamilyMembersUploaded) - .containsExactly( - Tuple.tuple("1", false) - ); - } - @Test void 월별_캘린더_조회_테스트() { // Given diff --git a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java index fac636ca..c1ae3b44 100644 --- a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java @@ -104,97 +104,6 @@ void setUp() { } - @Test - void 주간_캘린더_조회_테스트() throws Exception { - // Given - // parameters - String yearMonth = "2023-11"; - Long week = 1L; - - // posts - jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '2023-11-01 14:00:00', '2023-11-01 14:00:00', 'post1111', '1');"); - jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('2', '" + TEST_MEMBER2_ID + "', 'https://storage.com/images/2', 0, 0, '2023-11-01 15:00:00', '2023-11-01 15:00:00', 'post2222', '2');"); - jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('3', '" + TEST_MEMBER3_ID + "', 'https://storage.com/images/3', 0, 0, '2023-11-01 17:00:00', '2023-11-01 17:00:00', 'post3333', '3');"); - jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('4', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/4', 0, 0, '2023-11-02 14:00:00', '2023-11-02 14:00:00', 'post4444', '4');"); - - // family - String familyId = objectMapper.readValue( - mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), FamilyResponse.class - ).familyId(); - String inviteCode = objectMapper.readValue( - mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class - ).getLinkId(); - mockMvc.perform(post("/v1/me/join-family") - .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) - ).andExpect(status().isOk()); - - - // When & Then - mockMvc.perform(get("/v1/calendar") - .param("type", "WEEKLY") - .param("yearMonth", yearMonth) - .param("week", week.toString()) - .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) - ) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.results[0].date").value("2023-11-01")) - .andExpect(jsonPath("$.results[0].representativePostId").value("2")) - .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/2" + thumbnailOptimizerQuery)) - .andExpect(jsonPath("$.results[0].allFamilyMembersUploaded").value(true)) - .andExpect(jsonPath("$.results[1].date").value("2023-11-02")) - .andExpect(jsonPath("$.results[1].representativePostId").value("4")) - .andExpect(jsonPath("$.results[1].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/4" + thumbnailOptimizerQuery)) - .andExpect(jsonPath("$.results[1].allFamilyMembersUploaded").value(false)); - } - - @Test - void 주간_캘린더_파라미터_없이_조회_테스트() throws Exception { - // Given - // posts - String now = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '" + now + "', '" + now + "', 'post1111', '1');"); - - // family - String familyId = objectMapper.readValue( - mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), FamilyResponse.class - ).familyId(); - String inviteCode = objectMapper.readValue( - mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class - ).getLinkId(); - mockMvc.perform(post("/v1/me/join-family") - .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) - ).andExpect(status().isOk()); - - - // When & Then - mockMvc.perform(get("/v1/calendar") - .param("type", "WEEKLY") - .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) - ) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.results[0].date").value(LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE))) - .andExpect(jsonPath("$.results[0].representativePostId").value("1")) - .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/1" + thumbnailOptimizerQuery)) - .andExpect(jsonPath("$.results[0].allFamilyMembersUploaded").value(false)); - } - @Test void 월별_캘린더_조회_테스트() throws Exception { // Given From a336c6e587a7a9222b2e8bcb23f34d98daddca53 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Mon, 22 Jan 2024 09:02:46 +0900 Subject: [PATCH 26/49] =?UTF-8?q?[OING-165]=20feat:=20=EC=B6=94=EC=96=B5?= =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EC=83=88=20=EB=B0=B0=EB=84=88=20?= =?UTF-8?q?API=20=EC=84=A4=EA=B3=84=20=EB=B0=8F=20=EB=AA=A8=ED=82=B9=20(#1?= =?UTF-8?q?17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oing/controller/CalendarController.java | 11 ++++ .../com/oing/dto/response/BannerResponse.java | 13 +++++ .../java/com/oing/restapi/CalendarApi.java | 9 ++++ .../com/oing/restapi/CalendarApiTest.java | 53 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 gateway/src/main/java/com/oing/dto/response/BannerResponse.java diff --git a/gateway/src/main/java/com/oing/controller/CalendarController.java b/gateway/src/main/java/com/oing/controller/CalendarController.java index f3b1b2c4..9d7ca52b 100644 --- a/gateway/src/main/java/com/oing/controller/CalendarController.java +++ b/gateway/src/main/java/com/oing/controller/CalendarController.java @@ -4,6 +4,7 @@ import com.oing.domain.MemberPost; import com.oing.domain.MemberPostDailyCalendarDTO; import com.oing.dto.response.ArrayResponse; +import com.oing.dto.response.BannerResponse; import com.oing.dto.response.CalendarResponse; import com.oing.restapi.CalendarApi; import com.oing.service.MemberPostService; @@ -15,6 +16,7 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Random; import java.util.stream.IntStream; @Controller @@ -75,4 +77,13 @@ public ArrayResponse getMonthlyCalendar(String yearMonth) { List calendarResponses = getCalendarResponses(familyIds, startDate, endDate); return new ArrayResponse<>(calendarResponses); } + + + @Override + public BannerResponse getBanner(String yearMonth) { + return new BannerResponse( + new Random().nextInt(0, 101), + new Random().nextInt(0, 28) + ); + } } diff --git a/gateway/src/main/java/com/oing/dto/response/BannerResponse.java b/gateway/src/main/java/com/oing/dto/response/BannerResponse.java new file mode 100644 index 00000000..ac8e0ab0 --- /dev/null +++ b/gateway/src/main/java/com/oing/dto/response/BannerResponse.java @@ -0,0 +1,13 @@ +package com.oing.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "배너 응답") +public record BannerResponse( + @Schema(description = "가족 활성도의 상위 백분율", example = "50") + Integer familyTopPercentage, + + @Schema(description = "가족 구성원 모두가 업로드한 날의 수", example = "3") + Integer allFamilyMembersUploadedDays +) { +} diff --git a/gateway/src/main/java/com/oing/restapi/CalendarApi.java b/gateway/src/main/java/com/oing/restapi/CalendarApi.java index 50b6b31d..787ac14e 100644 --- a/gateway/src/main/java/com/oing/restapi/CalendarApi.java +++ b/gateway/src/main/java/com/oing/restapi/CalendarApi.java @@ -1,6 +1,7 @@ package com.oing.restapi; import com.oing.dto.response.ArrayResponse; +import com.oing.dto.response.BannerResponse; import com.oing.dto.response.CalendarResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -30,4 +31,12 @@ ArrayResponse getMonthlyCalendar( @Parameter(description = "조회할 년월", example = "2021-12") String yearMonth ); + + @Operation(summary = "캘린더 베너 조회", description = "캘린더 상단의 베너를 조회합니다.") + @GetMapping("/banner") + BannerResponse getBanner( + @RequestParam(required = false) + @Parameter(description = "조회할 년월", example = "2021-12") + String yearMonth + ); } diff --git a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java index c1ae3b44..36dba0b9 100644 --- a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java @@ -22,6 +22,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.regex.Pattern; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -209,4 +210,56 @@ void setUp() { .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/1" + thumbnailOptimizerQuery)) .andExpect(jsonPath("$.results[0].allFamilyMembersUploaded").value(false)); } + + + @Test + void 캘린더_배너_조회_태스트() throws Exception { + // parameters + String yearMonth = "2023-11"; + + // posts + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '2023-11-01 14:00:00', '2023-11-01 14:00:00', 'post1111', '1');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('2', '" + TEST_MEMBER2_ID + "', 'https://storage.com/images/2', 0, 0, '2023-11-01 15:00:00', '2023-11-01 15:00:00', 'post2222', '2');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('3', '" + TEST_MEMBER3_ID + "', 'https://storage.com/images/3', 0, 0, '2023-11-01 17:00:00', '2023-11-01 17:00:00', 'post3333', '3');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('4', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/4', 0, 0, '2023-11-02 14:00:00', '2023-11-02 14:00:00', 'post4444', '4');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('5', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/5', 0, 0, '2023-11-29 14:00:00', '2023-11-29 14:00:00', 'post5555', '5');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('6', '" + TEST_MEMBER2_ID + "', 'https://storage.com/images/6', 0, 0, '2023-11-29 15:00:00', '2023-11-29 15:00:00', 'post6666', '6');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('7', '" + TEST_MEMBER3_ID + "', 'https://storage.com/images/7', 0, 0, '2023-11-29 17:00:00', '2023-11-29 17:00:00', 'post7777', '7');"); + jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + + "values ('8', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/8', 0, 0, '2023-11-30 14:00:00', '2023-11-30 14:00:00', 'post8888', '8');"); + + // family + String familyId = objectMapper.readValue( + mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), FamilyResponse.class + ).familyId(); + String inviteCode = objectMapper.readValue( + mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class + ).getLinkId(); + mockMvc.perform(post("/v1/me/join-family") + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) + ).andExpect(status().isOk()); + + + // When & Then + mockMvc.perform(get("/v1/calendar/banner") + .param("yearMonth", yearMonth) + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.familyTopPercentage").isNumber()) + .andExpect(jsonPath("$.allFamilyMembersUploadedDays").isNumber()); + } } \ No newline at end of file From ea58a80674a3d81722ec8798f8fbb9ef7efe3ef2 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Tue, 23 Jan 2024 22:41:07 +0900 Subject: [PATCH 27/49] =?UTF-8?q?[OING-166]=20feat:=20=EC=9C=84=EC=A0=AF?= =?UTF-8?q?=EC=9D=B4=20=EC=9E=90=EC=8B=A0=EC=9D=98=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=EB=8F=84=20=ED=8F=AC=ED=95=A8=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=A0=95=EC=B1=85=20=EB=B3=80=EA=B2=BD=20(#118)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/oing/controller/WidgetController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gateway/src/main/java/com/oing/controller/WidgetController.java b/gateway/src/main/java/com/oing/controller/WidgetController.java index fd913fa6..03d89fe3 100644 --- a/gateway/src/main/java/com/oing/controller/WidgetController.java +++ b/gateway/src/main/java/com/oing/controller/WidgetController.java @@ -30,14 +30,14 @@ public class WidgetController implements WidgetApi { @Override public ResponseEntity getSingleRecentFamilyPostWidget(String date) { String myId = tokenAuthenticationHolder.getUserId(); - List familyIdsExceptMe = memberService.findFamilyMembersIdByMemberId(myId).stream().filter(id -> !id.equals(myId)).toList(); + List familyIds = memberService.findFamilyMembersIdByMemberId(myId); Optional dateString = Optional.ofNullable(date); LocalDate startDate = dateString.map(LocalDate::parse).orElse(LocalDate.now()); LocalDate endDate = startDate.plusDays(1); - List latestPosts = memberPostService.findLatestPostOfEveryday(familyIdsExceptMe, startDate, endDate); + List latestPosts = memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate); if (latestPosts.isEmpty()) { return ResponseEntity.noContent().build(); } From 626c206aa39d6628dcbd18d2bcf1a8ec54126c2e Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Tue, 23 Jan 2024 22:41:48 +0900 Subject: [PATCH 28/49] =?UTF-8?q?en=20[OING-168]=20fix:=20=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EA=B0=9C=EC=88=98=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- post/src/main/java/com/oing/dto/response/PostResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/post/src/main/java/com/oing/dto/response/PostResponse.java b/post/src/main/java/com/oing/dto/response/PostResponse.java index 9d19b4dd..dfc94a2b 100644 --- a/post/src/main/java/com/oing/dto/response/PostResponse.java +++ b/post/src/main/java/com/oing/dto/response/PostResponse.java @@ -40,7 +40,7 @@ public static PostResponse from(MemberPost post) { post.getId(), post.getMemberId(), post.getCommentCnt(), - post.getReactionCnt(), + post.getReactionCnt() + post.getRealEmojiCnt(), post.getPostImgUrl(), post.getContent(), post.getCreatedAt().atZone(ZoneId.systemDefault()) From 9015a45eff83526ad38035432e8ed10b0a68a748 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Tue, 23 Jan 2024 23:05:32 +0900 Subject: [PATCH 29/49] =?UTF-8?q?=20[OING-167]=20chore:=20=EA=B3=A0?= =?UTF-8?q?=EC=9E=A5=EB=82=9C=20SonarCloud=20=EB=B0=8F=20jacoco=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BB=A4=EB=B2=84=EB=A6=AC?= =?UTF-8?q?=EC=A7=80=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=88=98=EB=A6=AC=20(#119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/oing/dto/response/PreSignedUrlResponse.java | 12 ------------ .../config/support/S3PreSignedUrlProviderTest.java | 4 ++-- 2 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 gateway/src/main/java/com/oing/dto/response/PreSignedUrlResponse.java diff --git a/gateway/src/main/java/com/oing/dto/response/PreSignedUrlResponse.java b/gateway/src/main/java/com/oing/dto/response/PreSignedUrlResponse.java deleted file mode 100644 index 30b57055..00000000 --- a/gateway/src/main/java/com/oing/dto/response/PreSignedUrlResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.oing.dto.response; - -import lombok.*; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@ToString -public class PreSignedUrlResponse { - - private String url; -} diff --git a/gateway/src/test/java/com/oing/config/support/S3PreSignedUrlProviderTest.java b/gateway/src/test/java/com/oing/config/support/S3PreSignedUrlProviderTest.java index 4e262195..adb78819 100644 --- a/gateway/src/test/java/com/oing/config/support/S3PreSignedUrlProviderTest.java +++ b/gateway/src/test/java/com/oing/config/support/S3PreSignedUrlProviderTest.java @@ -19,7 +19,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class S3PreSignedUrlProviderTest extends InfraTest { +class S3PreSignedUrlProviderTest extends InfraTest { private S3PreSignedUrlProvider provider; @@ -46,7 +46,7 @@ void getPreSignedUrl() { // then Assertions.assertAll( () -> assertNotNull(response), - () -> assertEquals(mockPresignedUrl.toString(), response.getUrl()) + () -> assertEquals(mockPresignedUrl.toString(), response.url()) ); } } From 81ef139cfb4e40d92250325347fd262880505bc9 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 24 Jan 2024 18:25:52 +0900 Subject: [PATCH 30/49] =?UTF-8?q?[OING-169]=20hotfix:=20=EB=8F=99=EC=9D=BC?= =?UTF-8?q?=ED=95=9C=20=ED=83=80=EC=9E=85=EC=9D=98=20=EB=A6=AC=EC=96=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=83=9D=EC=84=B1=ED=96=88?= =?UTF-8?q?=EB=8A=94=EC=A7=80=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: fix findRealEmojiByEmojiTypeAndMemberId logic * test: fix test --- .../java/com/oing/controller/MemberRealEmojiController.java | 6 +++--- .../java/com/oing/repository/MemberRealEmojiRepository.java | 2 +- .../main/java/com/oing/service/MemberRealEmojiService.java | 4 ++-- .../com/oing/controller/MemberRealEmojiControllerTest.java | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/post/src/main/java/com/oing/controller/MemberRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java index 97e8d909..55853b18 100644 --- a/post/src/main/java/com/oing/controller/MemberRealEmojiController.java +++ b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java @@ -47,7 +47,7 @@ public RealEmojiResponse createMemberRealEmoji(String memberId, CreateMyRealEmoj String emojiId = identityGenerator.generateIdentity(); String emojiImgKey = preSignedUrlGenerator.extractImageKey(request.imageUrl()); Emoji emoji = Emoji.fromString(request.type()); - if (isExistsSameRealEmojiType(emoji)) { + if (isExistsSameRealEmojiType(emoji, memberId)) { throw new DuplicateRealEmojiException(); } @@ -56,8 +56,8 @@ public RealEmojiResponse createMemberRealEmoji(String memberId, CreateMyRealEmoj return RealEmojiResponse.from(addedRealEmoji); } - private boolean isExistsSameRealEmojiType(Emoji emoji) { - return memberRealEmojiService.findRealEmojiByEmojiType(emoji); + private boolean isExistsSameRealEmojiType(Emoji emoji, String memberId) { + return memberRealEmojiService.findRealEmojiByEmojiTypeAndMemberId(emoji, memberId); } @Transactional diff --git a/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java index 5eace7ee..27f871bd 100644 --- a/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java +++ b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java @@ -9,7 +9,7 @@ public interface MemberRealEmojiRepository extends JpaRepository { - Optional findByType(Emoji emoji); + Optional findByTypeAndMemberId(Emoji emoji, String memberId); List findAllByMemberId(String memberId); } diff --git a/post/src/main/java/com/oing/service/MemberRealEmojiService.java b/post/src/main/java/com/oing/service/MemberRealEmojiService.java index 14597d6b..9ca4b08a 100644 --- a/post/src/main/java/com/oing/service/MemberRealEmojiService.java +++ b/post/src/main/java/com/oing/service/MemberRealEmojiService.java @@ -31,9 +31,9 @@ public MemberRealEmoji findRealEmojiById(String realEmojiId) { .orElseThrow(RealEmojiNotFoundException::new); } - public boolean findRealEmojiByEmojiType(Emoji emoji) { + public boolean findRealEmojiByEmojiTypeAndMemberId(Emoji emoji, String memberId) { return memberRealEmojiRepository - .findByType(emoji) + .findByTypeAndMemberId(emoji, memberId) .isPresent(); } diff --git a/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java b/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java index e0515e62..db09fe88 100644 --- a/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java +++ b/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java @@ -106,7 +106,7 @@ public class MemberRealEmojiControllerTest { // when when(authenticationHolder.getUserId()).thenReturn(memberId); CreateMyRealEmojiRequest request = new CreateMyRealEmojiRequest(emoji.getTypeKey(), realEmojiImageUrl); - when(memberRealEmojiService.findRealEmojiByEmojiType(emoji)).thenReturn(true); + when(memberRealEmojiService.findRealEmojiByEmojiTypeAndMemberId(emoji, memberId)).thenReturn(true); // then assertThrows(DuplicateRealEmojiException.class, From 0c58e9952419d05c120b04f9e7502e76c01b2bc5 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 24 Jan 2024 18:40:23 +0900 Subject: [PATCH 31/49] =?UTF-8?q?[OING-164]=20refactor:=20Redis=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C(Monthly)=20API=20=EC=BA=90=EC=8B=B1=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add redis dependency * feat: add RedisConfig file * chore: add dependency * feat: add @FamilyId resolver * feat: add caching to getMonthlyCalendar api * fix: add JsonSerialize to response field * feat: add CacheEvict to createPost api * test: add EmbeddedRedisConfig * docs: update readme --- README.md | 3 ++ build.gradle | 3 ++ .../exception/FamilyIdNotFoundException.java | 7 +++ .../java/com/oing/util/security/FamilyId.java | 11 ++++ .../security/FamilyIdArgumentResolver.java | 39 ++++++++++++++ .../com/oing/config/RedisCacheConfig.java | 42 +++++++++++++++ .../java/com/oing/config/RedisConfig.java | 38 ++++++++++++++ .../java/com/oing/config/SpringWebConfig.java | 3 ++ .../oing/controller/CalendarController.java | 5 +- .../com/oing/controller/MeController.java | 2 - .../java/com/oing/restapi/CalendarApi.java | 8 ++- .../src/main/resources/application-dev.yaml | 4 ++ .../src/main/resources/application-prod.yaml | 4 ++ .../src/main/resources/application-test.yaml | 4 ++ gateway/src/main/resources/application.yaml | 5 ++ .../controller/CalendarControllerTest.java | 36 +------------ .../com/oing/restapi/CalendarApiTest.java | 52 ++++--------------- .../com/oing/support/EmbeddedRedisConfig.java | 36 +++++++++++++ .../oing/controller/MemberPostController.java | 8 ++- .../oing/dto/response/CalendarResponse.java | 6 +++ .../java/com/oing/restapi/MemberPostApi.java | 6 ++- 21 files changed, 237 insertions(+), 85 deletions(-) create mode 100644 common/src/main/java/com/oing/exception/FamilyIdNotFoundException.java create mode 100644 common/src/main/java/com/oing/util/security/FamilyId.java create mode 100644 common/src/main/java/com/oing/util/security/FamilyIdArgumentResolver.java create mode 100644 gateway/src/main/java/com/oing/config/RedisCacheConfig.java create mode 100644 gateway/src/main/java/com/oing/config/RedisConfig.java create mode 100644 gateway/src/test/java/com/oing/support/EmbeddedRedisConfig.java diff --git a/README.md b/README.md index f418b1a8..13ca1379 100644 --- a/README.md +++ b/README.md @@ -64,4 +64,7 @@ In the precious time spent with family, everything feels more special with Pippy | OBJECT_STORAGE_ACCESS_KEY | NCP 액세스 키 입니다. | | OBJECT_STORAGE_SECRET_KEY | NCP 시크릿 키 입니다. | | OBJECT_STORAGE_BUCKET_NAME | NCP ObjectStroage 버킷명 입니다. | +| IMAGE_OPTIMIZER_CDN_URL | NCP ImageOptimizer CDN URL 입니다. | | GOOGLE_CLIENT_ID | 구글 로그인 클라이언트 ID 입니다. | +| REDIS_HOST | Redis Host 입니다. | +| REDIS_PORT | Redis Port 입니다. | diff --git a/build.gradle b/build.gradle index cb98ef48..ff83a63c 100644 --- a/build.gradle +++ b/build.gradle @@ -118,6 +118,8 @@ subprojects { implementation 'com.github.f4b6a3:ulid-creator:5.2.2' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'com.google.api-client:google-api-client:1.32.1' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation("com.nimbusds:nimbus-jose-jwt:9.37") @@ -128,6 +130,7 @@ subprojects { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'it.ozimov:embedded-redis:0.7.2' } } diff --git a/common/src/main/java/com/oing/exception/FamilyIdNotFoundException.java b/common/src/main/java/com/oing/exception/FamilyIdNotFoundException.java new file mode 100644 index 00000000..1792a2fd --- /dev/null +++ b/common/src/main/java/com/oing/exception/FamilyIdNotFoundException.java @@ -0,0 +1,7 @@ +package com.oing.exception; + +public class FamilyIdNotFoundException extends DomainException { + public FamilyIdNotFoundException() { + super(ErrorCode.FAMILY_NOT_FOUND); + } +} diff --git a/common/src/main/java/com/oing/util/security/FamilyId.java b/common/src/main/java/com/oing/util/security/FamilyId.java new file mode 100644 index 00000000..593b4556 --- /dev/null +++ b/common/src/main/java/com/oing/util/security/FamilyId.java @@ -0,0 +1,11 @@ +package com.oing.util.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface FamilyId { +} diff --git a/common/src/main/java/com/oing/util/security/FamilyIdArgumentResolver.java b/common/src/main/java/com/oing/util/security/FamilyIdArgumentResolver.java new file mode 100644 index 00000000..c860fda8 --- /dev/null +++ b/common/src/main/java/com/oing/util/security/FamilyIdArgumentResolver.java @@ -0,0 +1,39 @@ +package com.oing.util.security; + +import com.oing.exception.FamilyIdNotFoundException; +import com.oing.service.MemberBridge; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class FamilyIdArgumentResolver implements HandlerMethodArgumentResolver { + + private final MemberBridge memberBridge; + + @Autowired + public FamilyIdArgumentResolver(MemberBridge memberBridge) { + this.memberBridge = memberBridge; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(FamilyId.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String memberId = SecurityContextHolder.getContext().getAuthentication().getName(); + String familyId = memberBridge.getFamilyIdByMemberId(memberId); + if (familyId == null) { + throw new FamilyIdNotFoundException(); + } + return familyId; + } +} diff --git a/gateway/src/main/java/com/oing/config/RedisCacheConfig.java b/gateway/src/main/java/com/oing/config/RedisCacheConfig.java new file mode 100644 index 00000000..183ec9d1 --- /dev/null +++ b/gateway/src/main/java/com/oing/config/RedisCacheConfig.java @@ -0,0 +1,42 @@ +package com.oing.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class RedisCacheConfig { + + @Bean + @Primary + public CacheManager monthlyCalendarCacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration redisCacheConfiguration = generateCacheConfiguration() + .entryTtl(Duration.ofHours(5L)); + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); + } + + private RedisCacheConfiguration generateCacheConfiguration() { + return RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())); + } +} diff --git a/gateway/src/main/java/com/oing/config/RedisConfig.java b/gateway/src/main/java/com/oing/config/RedisConfig.java new file mode 100644 index 00000000..7e910b80 --- /dev/null +++ b/gateway/src/main/java/com/oing/config/RedisConfig.java @@ -0,0 +1,38 @@ +package com.oing.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHost, redisPort)); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + + /* Java 기본 직렬화가 아닌 JSON 직렬화 설정 */ + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return redisTemplate; + } +} diff --git a/gateway/src/main/java/com/oing/config/SpringWebConfig.java b/gateway/src/main/java/com/oing/config/SpringWebConfig.java index a3ff9442..1e36ea9a 100644 --- a/gateway/src/main/java/com/oing/config/SpringWebConfig.java +++ b/gateway/src/main/java/com/oing/config/SpringWebConfig.java @@ -5,6 +5,7 @@ import com.google.api.client.json.gson.GsonFactory; import com.oing.config.filter.WebRequestInterceptor; import com.oing.config.support.AppKeyResolver; +import com.oing.util.security.FamilyIdArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -27,6 +28,7 @@ public class SpringWebConfig implements WebMvcConfigurer { final WebRequestInterceptor webRequestInterceptor; final AppKeyResolver appKeyResolver; + final FamilyIdArgumentResolver familyIdArgumentResolver; @Value("${app.oauth.google-client-id}") private String googleClientId; @@ -46,5 +48,6 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addArgumentResolvers(List resolvers) { resolvers.add(appKeyResolver); + resolvers.add(familyIdArgumentResolver); } } diff --git a/gateway/src/main/java/com/oing/controller/CalendarController.java b/gateway/src/main/java/com/oing/controller/CalendarController.java index 9d7ca52b..abc39ff2 100644 --- a/gateway/src/main/java/com/oing/controller/CalendarController.java +++ b/gateway/src/main/java/com/oing/controller/CalendarController.java @@ -11,6 +11,7 @@ import com.oing.service.MemberService; import com.oing.util.OptimizedImageUrlGenerator; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Controller; import java.time.LocalDate; @@ -67,7 +68,8 @@ private List getCalendarResponses(List familyIds, Loca } @Override - public ArrayResponse getMonthlyCalendar(String yearMonth) { + @Cacheable(value = "calendarCache", key = "#familyId.concat(':').concat(#yearMonth)", cacheManager = "monthlyCalendarCacheManager") + public ArrayResponse getMonthlyCalendar(String yearMonth, String familyId) { if (yearMonth == null) yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); LocalDate startDate = LocalDate.parse(yearMonth + "-01"); // yyyy-MM-dd 패턴으로 파싱 @@ -78,7 +80,6 @@ public ArrayResponse getMonthlyCalendar(String yearMonth) { return new ArrayResponse<>(calendarResponses); } - @Override public BannerResponse getBanner(String yearMonth) { return new BannerResponse( diff --git a/gateway/src/main/java/com/oing/controller/MeController.java b/gateway/src/main/java/com/oing/controller/MeController.java index dd2f7079..cb182a8b 100644 --- a/gateway/src/main/java/com/oing/controller/MeController.java +++ b/gateway/src/main/java/com/oing/controller/MeController.java @@ -12,8 +12,6 @@ import com.oing.dto.response.FamilyResponse; import com.oing.dto.response.MemberResponse; import com.oing.exception.AlreadyInFamilyException; -import com.oing.exception.DomainException; -import com.oing.exception.ErrorCode; import com.oing.exception.FamilyNotFoundException; import com.oing.restapi.MeApi; import com.oing.service.FamilyInviteLinkService; diff --git a/gateway/src/main/java/com/oing/restapi/CalendarApi.java b/gateway/src/main/java/com/oing/restapi/CalendarApi.java index 787ac14e..8c076379 100644 --- a/gateway/src/main/java/com/oing/restapi/CalendarApi.java +++ b/gateway/src/main/java/com/oing/restapi/CalendarApi.java @@ -3,6 +3,7 @@ import com.oing.dto.response.ArrayResponse; import com.oing.dto.response.BannerResponse; import com.oing.dto.response.CalendarResponse; +import com.oing.util.security.FamilyId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -27,9 +28,12 @@ public interface CalendarApi { @Operation(summary = "월별 캘린더 조회", description = "월별 캘린더를 조회합니다.") @GetMapping(params = {"type=MONTHLY"}) ArrayResponse getMonthlyCalendar( - @RequestParam(required = false) + @RequestParam @Parameter(description = "조회할 년월", example = "2021-12") - String yearMonth + String yearMonth, + + @FamilyId + String familyId ); @Operation(summary = "캘린더 베너 조회", description = "캘린더 상단의 베너를 조회합니다.") diff --git a/gateway/src/main/resources/application-dev.yaml b/gateway/src/main/resources/application-dev.yaml index d8fc2c8c..dfa4535b 100644 --- a/gateway/src/main/resources/application-dev.yaml +++ b/gateway/src/main/resources/application-dev.yaml @@ -29,3 +29,7 @@ spring: h2: console: enabled: false + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} diff --git a/gateway/src/main/resources/application-prod.yaml b/gateway/src/main/resources/application-prod.yaml index b81a6806..009cb6c2 100644 --- a/gateway/src/main/resources/application-prod.yaml +++ b/gateway/src/main/resources/application-prod.yaml @@ -29,6 +29,10 @@ spring: h2: console: enabled: false + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} logging: level: diff --git a/gateway/src/main/resources/application-test.yaml b/gateway/src/main/resources/application-test.yaml index 315bb99b..f52781eb 100644 --- a/gateway/src/main/resources/application-test.yaml +++ b/gateway/src/main/resources/application-test.yaml @@ -16,6 +16,10 @@ spring: format_sql: false dialect: org.hibernate.dialect.MySQL8Dialect database-platform: org.hibernate.dialect.MySQL8Dialect + data: + redis: + host: localhost + port: 16379 app: external-urls: slack-webhook: https://www.naver.com # Must Be Replaced diff --git a/gateway/src/main/resources/application.yaml b/gateway/src/main/resources/application.yaml index 14f6a679..8a35d671 100644 --- a/gateway/src/main/resources/application.yaml +++ b/gateway/src/main/resources/application.yaml @@ -20,6 +20,11 @@ spring: enabled: true settings: web-allow-others: true + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + app: oauth: google-client-id: ${GOOGLE_CLIENT_ID} diff --git a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java index 93ec0a2e..2ce08729 100644 --- a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java +++ b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java @@ -20,7 +20,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -116,7 +115,7 @@ class CalendarControllerTest { when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); // When - ArrayResponse weeklyCalendar = calendarController.getMonthlyCalendar(yearMonth); + ArrayResponse weeklyCalendar = calendarController.getMonthlyCalendar(yearMonth, testMember1.getFamilyId()); // Then assertThat(weeklyCalendar.results()) @@ -128,37 +127,4 @@ class CalendarControllerTest { Tuple.tuple("4", false) ); } - - @Test - void 월별_캘린더_파라미터_없이_조회_테스트() { - // Given - LocalDate startDate = LocalDate.parse(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")) + "-01"); - LocalDate endDate = startDate.plusMonths(1); - MemberPost testPost1 = new MemberPost( - "1", - testMember1.getId(), - "post.com/1", - "1", - "test1" - ); - ReflectionTestUtils.setField(testPost1, "createdAt", LocalDateTime.now()); - List representativePosts = List.of(testPost1); - List calendarDTOs = List.of( - new MemberPostDailyCalendarDTO(1L) - ); - when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); - when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); - when(memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate)).thenReturn(representativePosts); - when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); - - // When - ArrayResponse weeklyCalendar = calendarController.getMonthlyCalendar(null); - - // Then - assertThat(weeklyCalendar.results()) - .extracting(CalendarResponse::representativePostId, CalendarResponse::allFamilyMembersUploaded) - .containsExactly( - Tuple.tuple("1", false) - ); - } } diff --git a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java index 36dba0b9..f0594750 100644 --- a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java @@ -1,7 +1,8 @@ package com.oing.restapi; import com.fasterxml.jackson.databind.ObjectMapper; -import com.oing.domain.*; +import com.oing.domain.CreateNewUserDTO; +import com.oing.domain.SocialLoginProvider; import com.oing.dto.request.JoinFamilyRequest; import com.oing.dto.response.DeepLinkResponse; import com.oing.dto.response.FamilyResponse; @@ -13,6 +14,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; @@ -24,8 +26,10 @@ import java.util.List; import java.util.regex.Pattern; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @@ -51,6 +55,8 @@ class CalendarApiTest { private DeepLinkService deepLinkService; @Autowired private TokenGenerator tokenGenerator; + @Autowired + private RedisTemplate redisTemplate; private String TEST_MEMBER1_ID; private String TEST_MEMBER1_TOKEN; @@ -151,6 +157,7 @@ void setUp() { mockMvc.perform(get("/v1/calendar") .param("type", "MONTHLY") .param("yearMonth", yearMonth) + .param("familyId", familyId) .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) ) .andExpect(status().isOk()) @@ -173,45 +180,6 @@ void setUp() { } - @Test - void 월별_캘린더_파라미터_없이_조회_테스트() throws Exception { - // Given - // posts - String now = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - jdbcTemplate.execute("insert into member_post (post_id, member_id, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) " + - "values ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '" + now + "', '" + now + "', 'post1111', '1');"); - - // family - String familyId = objectMapper.readValue( - mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), FamilyResponse.class - ).familyId(); - String inviteCode = objectMapper.readValue( - mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class - ).getLinkId(); - mockMvc.perform(post("/v1/me/join-family") - .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) - ).andExpect(status().isOk()); - - - // When & Then - mockMvc.perform(get("/v1/calendar") - .param("type", "MONTHLY") - .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) - ) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.results[0].date").value(LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE))) - .andExpect(jsonPath("$.results[0].representativePostId").value("1")) - .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/1" + thumbnailOptimizerQuery)) - .andExpect(jsonPath("$.results[0].allFamilyMembersUploaded").value(false)); - } - - @Test void 캘린더_배너_조회_태스트() throws Exception { // parameters diff --git a/gateway/src/test/java/com/oing/support/EmbeddedRedisConfig.java b/gateway/src/test/java/com/oing/support/EmbeddedRedisConfig.java new file mode 100644 index 00000000..a6fbec60 --- /dev/null +++ b/gateway/src/test/java/com/oing/support/EmbeddedRedisConfig.java @@ -0,0 +1,36 @@ +package com.oing.support; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import redis.embedded.RedisServer; + +@Configuration +@Profile("test") +public class EmbeddedRedisConfig { + + @Value("${spring.data.redis.port}") + private int port; + + private RedisServer redisServer; + + @PostConstruct + public void startRedis() { + try { + redisServer = RedisServer.builder() + .port(port) + .setting("maxmemory 256M") + .build(); + redisServer.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @PreDestroy + public void stopRedis() { + redisServer.stop(); + } +} diff --git a/post/src/main/java/com/oing/controller/MemberPostController.java b/post/src/main/java/com/oing/controller/MemberPostController.java index 29a8d617..7879d397 100644 --- a/post/src/main/java/com/oing/controller/MemberPostController.java +++ b/post/src/main/java/com/oing/controller/MemberPostController.java @@ -19,11 +19,13 @@ import com.oing.util.PreSignedUrlGenerator; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Controller; import java.time.LocalDate; import java.time.LocalTime; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; /** * no5ing-server @@ -41,6 +43,8 @@ public class MemberPostController implements MemberPostApi { private final MemberPostService memberPostService; private final MemberBridge memberBridge; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); + @Transactional @Override public PreSignedUrlResponse requestPresignedUrl(PreSignedUrlRequest request) { @@ -63,7 +67,9 @@ public PaginationResponse fetchDailyFeeds(Integer page, Integer si @Transactional @Override - public PostResponse createPost(CreatePostRequest request) { + @CacheEvict(value = "calendarCache", + key = "#familyId.concat(':').concat(T(java.time.format.DateTimeFormatter).ofPattern('yyyy-MM').format(#request.uploadTime()))") + public PostResponse createPost(CreatePostRequest request, String familyId) { String memberId = authenticationHolder.getUserId(); String postId = identityGenerator.generateIdentity(); ZonedDateTime uploadTime = request.uploadTime(); diff --git a/post/src/main/java/com/oing/dto/response/CalendarResponse.java b/post/src/main/java/com/oing/dto/response/CalendarResponse.java index a91b113a..ab53a75f 100644 --- a/post/src/main/java/com/oing/dto/response/CalendarResponse.java +++ b/post/src/main/java/com/oing/dto/response/CalendarResponse.java @@ -1,5 +1,9 @@ package com.oing.dto.response; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.format.annotation.DateTimeFormat; @@ -9,6 +13,8 @@ @Schema(description = "캘린더 응답") public record CalendarResponse( @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) @Parameter(description = "오늘의 날짜", example = "2023-12-05") LocalDate date, diff --git a/post/src/main/java/com/oing/restapi/MemberPostApi.java b/post/src/main/java/com/oing/restapi/MemberPostApi.java index 844c67ad..10983435 100644 --- a/post/src/main/java/com/oing/restapi/MemberPostApi.java +++ b/post/src/main/java/com/oing/restapi/MemberPostApi.java @@ -6,6 +6,7 @@ import com.oing.dto.response.PaginationResponse; import com.oing.dto.response.PostResponse; import com.oing.dto.response.PreSignedUrlResponse; +import com.oing.util.security.FamilyId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -67,7 +68,10 @@ PaginationResponse fetchDailyFeeds( PostResponse createPost( @Valid @RequestBody - CreatePostRequest request + CreatePostRequest request, + + @FamilyId + String familyId ); @Operation(summary = "단일 게시물 조회", description = "ID를 통해 게시물을 조회합니다.") From d1a067c71feaad2c7f0e1c2628214666c0f36fd5 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 24 Jan 2024 18:44:01 +0900 Subject: [PATCH 32/49] =?UTF-8?q?[OING-170]=20refactor:=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=AC=BC=EC=97=90=20=EB=8B=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=96=BC=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=20(#122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/oing/dto/response/PostRealEmojiResponse.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java index 4078b388..0e620b77 100644 --- a/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java +++ b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java @@ -15,6 +15,9 @@ public record PostRealEmojiResponse( @Schema(description = "리얼 이모지 작성 사용자 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") String memberId, + @Schema(description = "리얼 이모지 타입", example = "emoji_1") + String emojiType, + @Schema(description = "리얼 이모지 ID", example = "01HGW2N7EHJVEFEFEEEEES2E97") String realEmojiId, @@ -23,6 +26,7 @@ public record PostRealEmojiResponse( ) { public static PostRealEmojiResponse from(MemberPostRealEmoji postRealEmoji) { return new PostRealEmojiResponse(postRealEmoji.getId(), postRealEmoji.getPost().getId(), postRealEmoji.getMemberId(), - postRealEmoji.getRealEmoji().getId(), postRealEmoji.getRealEmoji().getRealEmojiImageUrl()); + postRealEmoji.getRealEmoji().getType().getTypeKey(), postRealEmoji.getRealEmoji().getId(), + postRealEmoji.getRealEmoji().getRealEmojiImageUrl()); } } From 675bdc80171d4283d8d01dd28305aeaf58d1a2ed Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Thu, 25 Jan 2024 00:08:14 +0900 Subject: [PATCH 33/49] fix: fix deletePostRealEmoji logic (#124) --- .../com/oing/controller/MemberPostRealEmojiController.java | 2 +- .../com/oing/repository/MemberPostRealEmojiRepository.java | 2 +- .../java/com/oing/service/MemberPostRealEmojiService.java | 5 +++-- .../oing/controller/MemberPostRealEmojiControllerTest.java | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java index a5d3626c..36508bc3 100644 --- a/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java +++ b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java @@ -79,7 +79,7 @@ public DefaultResponse deletePostRealEmoji(String postId, String realEmojiId) { String memberId = authenticationHolder.getUserId(); MemberPost post = memberPostService.getMemberPostById(postId); MemberPostRealEmoji postRealEmoji = memberPostRealEmojiService - .getMemberPostRealEmojiByRealEmojiIdAndMemberId(realEmojiId, memberId); + .getMemberPostRealEmojiByRealEmojiIdAndMemberIdAndPostId(realEmojiId, memberId, postId); memberPostRealEmojiService.deletePostRealEmoji(postRealEmoji); post.removeRealEmoji(postRealEmoji); diff --git a/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java index 10661f45..a9e0987f 100644 --- a/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java @@ -10,5 +10,5 @@ public interface MemberPostRealEmojiRepository extends JpaRepository { boolean existsByPostAndMemberIdAndRealEmoji(MemberPost post, String memberId, MemberRealEmoji emoji); - Optional findByRealEmojiIdAndMemberId(String realEmojiId, String memberId); + Optional findByRealEmojiIdAndMemberIdAndPostId(String realEmojiId, String memberId, String postId); } diff --git a/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java b/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java index e84e73e3..8163a817 100644 --- a/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java +++ b/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java @@ -40,8 +40,9 @@ public boolean isMemberPostRealEmojiExists(MemberPost post, String memberId, Mem * @return 게시물에 등록된 리얼 이모지 * @throws RegisteredRealEmojiNotFoundException 등록된 리얼 이모지가 없는 경우 */ - public MemberPostRealEmoji getMemberPostRealEmojiByRealEmojiIdAndMemberId(String realEmojiId, String memberId) { - return memberPostRealEmojiRepository.findByRealEmojiIdAndMemberId(realEmojiId, memberId) + public MemberPostRealEmoji getMemberPostRealEmojiByRealEmojiIdAndMemberIdAndPostId(String realEmojiId, String memberId, + String postId) { + return memberPostRealEmojiRepository.findByRealEmojiIdAndMemberIdAndPostId(realEmojiId, memberId, postId) .orElseThrow(RegisteredRealEmojiNotFoundException::new); } diff --git a/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java b/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java index c0313fab..cf235aa3 100644 --- a/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java +++ b/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java @@ -150,7 +150,7 @@ public class MemberPostRealEmojiControllerTest { when(memberPostService.getMemberPostById(post.getId())).thenReturn(post); //when - when(memberPostRealEmojiService.getMemberPostRealEmojiByRealEmojiIdAndMemberId("1", memberId)) + when(memberPostRealEmojiService.getMemberPostRealEmojiByRealEmojiIdAndMemberIdAndPostId("1", memberId, post.getId())) .thenThrow(RegisteredRealEmojiNotFoundException.class); //then From b1fa2841829fc86d9f7e20f5be0bef75bc212023 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Thu, 25 Jan 2024 01:28:57 +0900 Subject: [PATCH 34/49] feat: Change getBanner api mocking according to policy changes of banner (#125) --- .../com/oing/controller/CalendarController.java | 6 +++++- .../main/java/com/oing/domain/BannerImageType.java | 14 ++++++++++++++ .../java/com/oing/dto/response/BannerResponse.java | 9 ++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 gateway/src/main/java/com/oing/domain/BannerImageType.java diff --git a/gateway/src/main/java/com/oing/controller/CalendarController.java b/gateway/src/main/java/com/oing/controller/CalendarController.java index abc39ff2..f5c815e3 100644 --- a/gateway/src/main/java/com/oing/controller/CalendarController.java +++ b/gateway/src/main/java/com/oing/controller/CalendarController.java @@ -1,6 +1,7 @@ package com.oing.controller; import com.oing.component.TokenAuthenticationHolder; +import com.oing.domain.BannerImageType; import com.oing.domain.MemberPost; import com.oing.domain.MemberPostDailyCalendarDTO; import com.oing.dto.response.ArrayResponse; @@ -16,6 +17,7 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.stream.IntStream; @@ -84,7 +86,9 @@ public ArrayResponse getMonthlyCalendar(String yearMonth, Stri public BannerResponse getBanner(String yearMonth) { return new BannerResponse( new Random().nextInt(0, 101), - new Random().nextInt(0, 28) + new Random().nextInt(0, 28), + new Random().nextInt(1, 5), + BannerImageType.values()[new Random().nextInt(BannerImageType.values().length)] ); } } diff --git a/gateway/src/main/java/com/oing/domain/BannerImageType.java b/gateway/src/main/java/com/oing/domain/BannerImageType.java new file mode 100644 index 00000000..38787d89 --- /dev/null +++ b/gateway/src/main/java/com/oing/domain/BannerImageType.java @@ -0,0 +1,14 @@ +package com.oing.domain; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum BannerImageType { + SKULL_FLAG("SKULL_FLAG"), + ALONE_WALING("ALONE_WALING"), + WE_ARE_FRIENDS("WE_ARE_FRIENDS"), + JEWELRY_TREASURE("JEWELRY_TREASURE"), + ; + + private final String imageCode; +} diff --git a/gateway/src/main/java/com/oing/dto/response/BannerResponse.java b/gateway/src/main/java/com/oing/dto/response/BannerResponse.java index ac8e0ab0..530be13b 100644 --- a/gateway/src/main/java/com/oing/dto/response/BannerResponse.java +++ b/gateway/src/main/java/com/oing/dto/response/BannerResponse.java @@ -1,5 +1,6 @@ package com.oing.dto.response; +import com.oing.domain.BannerImageType; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "배너 응답") @@ -8,6 +9,12 @@ public record BannerResponse( Integer familyTopPercentage, @Schema(description = "가족 구성원 모두가 업로드한 날의 수", example = "3") - Integer allFamilyMembersUploadedDays + Integer allFamilyMembersUploadedDays, + + @Schema(description = "가족 활성도 레벨 (1 ~ 4)", example = "1") + Integer familyLevel, + + @Schema(description = "배너 이미지 타입", example = "SKULL_FLAG") + BannerImageType bannerImageType ) { } From 3e0166bd341fac8283abc90a75ab45350a885262 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:38:47 +0900 Subject: [PATCH 35/49] =?UTF-8?q?[OING-172]=20hotfix:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=8B=9C,=20profileImgUrl=EC=9D=B4=20?= =?UTF-8?q?=EC=9E=88=EB=8B=A4=EB=A9=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=82=A4=20=EC=B6=94=EC=B6=9C=ED=95=B4=EC=84=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add setProfileImgKey logic to createNewMember * test: change profileImgUrl in test --- .../src/test/java/com/oing/restapi/CalendarApiTest.java | 9 +++------ member/src/main/java/com/oing/domain/Member.java | 4 ++++ member/src/main/java/com/oing/service/MemberService.java | 6 ++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java index f0594750..236f225e 100644 --- a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java @@ -21,10 +21,7 @@ import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.regex.Pattern; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -80,7 +77,7 @@ void setUp() { "testUser1", "testUser1", LocalDate.of(1999, 10, 18), - "profile.com" + "https://bucket.com/image.jpg" ) ).getId(); TEST_MEMBER1_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER1_ID).accessToken(); @@ -91,7 +88,7 @@ void setUp() { "testUser2", "testUser2", LocalDate.of(2000, 10, 18), - "profile.com" + "https://bucket.com/image.jpg" ) ).getId(); TEST_MEMBER2_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER2_ID).accessToken(); @@ -102,7 +99,7 @@ void setUp() { "testUser3", "testUser3", LocalDate.of(2001, 10, 18), - "profile.com" + "https://bucket.com/image.jpg" ) ).getId(); TEST_MEMBER3_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER3_ID).accessToken(); diff --git a/member/src/main/java/com/oing/domain/Member.java b/member/src/main/java/com/oing/domain/Member.java index 5163404d..da21643f 100644 --- a/member/src/main/java/com/oing/domain/Member.java +++ b/member/src/main/java/com/oing/domain/Member.java @@ -39,6 +39,10 @@ public class Member extends DeletableBaseAuditEntity { @Column(name = "profile_img_key") private String profileImgKey; + public void setProfileImgKey(String profileImgKey) { + this.profileImgKey = profileImgKey; + } + public void updateProfileImg(String profileImgUrl, String profileImgKey) { this.profileImgUrl = profileImgUrl; this.profileImgKey = profileImgKey; diff --git a/member/src/main/java/com/oing/service/MemberService.java b/member/src/main/java/com/oing/service/MemberService.java index c7f8583f..ba214d75 100644 --- a/member/src/main/java/com/oing/service/MemberService.java +++ b/member/src/main/java/com/oing/service/MemberService.java @@ -11,6 +11,7 @@ import com.oing.repository.MemberRepository; import com.oing.repository.SocialMemberRepository; import com.oing.util.IdentityGenerator; +import com.oing.util.PreSignedUrlGenerator; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -29,6 +30,7 @@ public class MemberService { private final SocialMemberRepository socialMemberRepository; private final IdentityGenerator identityGenerator; + private final PreSignedUrlGenerator preSignedUrlGenerator; public Member findMemberById(String memberId) { return memberRepository @@ -71,6 +73,10 @@ public Member createNewMember(CreateNewUserDTO createNewUserDTO) { member); socialMemberRepository.save(socialMember); + if (createNewUserDTO.profileImgUrl() != null) { + member.setProfileImgKey(preSignedUrlGenerator.extractImageKey(createNewUserDTO.profileImgUrl())); + } + return member; } From 955f35d1792858ce1faf69acdd3e4b8c48c15872 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Sat, 27 Jan 2024 00:15:13 +0900 Subject: [PATCH 36/49] feat: add body to slack alert --- .../java/com/oing/config/SpringWebExceptionHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gateway/src/main/java/com/oing/config/SpringWebExceptionHandler.java b/gateway/src/main/java/com/oing/config/SpringWebExceptionHandler.java index aa381abe..b4da55bb 100644 --- a/gateway/src/main/java/com/oing/config/SpringWebExceptionHandler.java +++ b/gateway/src/main/java/com/oing/config/SpringWebExceptionHandler.java @@ -1,5 +1,6 @@ package com.oing.config; +import com.google.common.io.CharStreams; import com.oing.domain.ErrorReportDTO; import com.oing.dto.response.ErrorResponse; import com.oing.exception.TokenNotValidException; @@ -143,6 +144,13 @@ private StringBuilder dumpRequest(HttpServletRequest request) { } } + dump.append("\n Body : "); + try { + dump.append("\n ").append(CharStreams.toString(request.getReader())); + }catch(Exception ex) { + dump.append("\n ").append("NOT_READABLE"); + } + return dump; } } From 8c350e361d12b6cfac07f6c5ace5dabbf198619c Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Sun, 28 Jan 2024 18:20:19 +0900 Subject: [PATCH 37/49] =?UTF-8?q?[OING-105]=20test:=20Widget=20API=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add WidgetControllertest * feat: Add the integration test, WidgetApiTest --- .../src/main/resources/application-test.yaml | 1 + .../oing/controller/WidgetControllerTest.java | 150 ++++++++++++ .../java/com/oing/restapi/WidgetApiTest.java | 222 ++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 gateway/src/test/java/com/oing/controller/WidgetControllerTest.java create mode 100644 gateway/src/test/java/com/oing/restapi/WidgetApiTest.java diff --git a/gateway/src/main/resources/application-test.yaml b/gateway/src/main/resources/application-test.yaml index f52781eb..95793fac 100644 --- a/gateway/src/main/resources/application-test.yaml +++ b/gateway/src/main/resources/application-test.yaml @@ -57,3 +57,4 @@ cloud: bucket: bucket image-optimizer-cdn: https://cdn.com thumbnail-optimizer-query: ?type=f&w=96&h=96&quality=70&align=4&faceopt=false&anilimit=1 + kb-optimizer-query: ?type=f&w=480&h=480&faceopt=false&quality=50&autorotate=false diff --git a/gateway/src/test/java/com/oing/controller/WidgetControllerTest.java b/gateway/src/test/java/com/oing/controller/WidgetControllerTest.java new file mode 100644 index 00000000..81a1d5c3 --- /dev/null +++ b/gateway/src/test/java/com/oing/controller/WidgetControllerTest.java @@ -0,0 +1,150 @@ +package com.oing.controller; + +import com.oing.component.TokenAuthenticationHolder; +import com.oing.domain.Member; +import com.oing.domain.MemberPost; +import com.oing.dto.response.SingleRecentPostWidgetResponse; +import com.oing.service.MemberPostService; +import com.oing.service.MemberService; +import com.oing.util.OptimizedImageUrlGenerator; +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.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class WidgetControllerTest { + + @InjectMocks + private WidgetController widgetController; + + @Mock + private MemberService memberService; + @Mock + private MemberPostService memberPostService; + @Mock + private TokenAuthenticationHolder tokenAuthenticationHolder; + @Mock + private OptimizedImageUrlGenerator optimizedImageUrlGenerator; + + + private final Member testMember1 = new Member( + "testMember1", + "testFamily", + LocalDate.of(1999, 10, 18), + "testMember1", + "profile.com/1", + "1" + ); + + private final Member testMember2 = new Member( + "testMember2", + "testFamily", + LocalDate.of(1999, 10, 18), + "testMember2", + "profile.com/2", + "2" + ); + + private final MemberPost testPost1 = new MemberPost( + "testPost1", + testMember1.getId(), + "post.com/1", + "1", + "testPost" + ); + + private final List familyIds = List.of(testMember1.getId(), testMember2.getId()); + + + @Test + void 최근_게시글_싱글_위젯_정상_조회_테스트() { + // given + String date = "2024-10-18"; + + when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); + when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); + when(memberPostService.findLatestPostOfEveryday(familyIds, LocalDate.parse(date), LocalDate.parse(date).plusDays(1))).thenReturn(List.of(testPost1)); + when(memberService.findMemberById(testPost1.getMemberId())).thenReturn(testMember1); + when(optimizedImageUrlGenerator.getKBImageUrlGenerator(testMember1.getProfileImgUrl())).thenReturn(testMember1.getProfileImgUrl()); + when(optimizedImageUrlGenerator.getKBImageUrlGenerator(testPost1.getPostImgUrl())).thenReturn(testPost1.getPostImgUrl()); + + // when + ResponseEntity response = widgetController.getSingleRecentFamilyPostWidget(date); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()) + .extracting( + SingleRecentPostWidgetResponse::authorName, + SingleRecentPostWidgetResponse::authorProfileImageUrl, + SingleRecentPostWidgetResponse::postImageUrl, + SingleRecentPostWidgetResponse::postContent + ).containsExactly( + testMember1.getName(), + testMember1.getProfileImgUrl(), + testPost1.getPostImgUrl(), + testPost1.getContent() + ); + } + + @Test + void 최근_게시글_싱글_위젯_null_파라미터_조회_테스트() { + // given + String date = null; + + when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); + when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); + when(memberPostService.findLatestPostOfEveryday(familyIds, LocalDate.now(), LocalDate.now().plusDays(1))).thenReturn(List.of(testPost1)); + when(memberService.findMemberById(testPost1.getMemberId())).thenReturn(testMember1); + when(optimizedImageUrlGenerator.getKBImageUrlGenerator(testMember1.getProfileImgUrl())).thenReturn(testMember1.getProfileImgUrl()); + when(optimizedImageUrlGenerator.getKBImageUrlGenerator(testPost1.getPostImgUrl())).thenReturn(testPost1.getPostImgUrl()); + + // when + ResponseEntity response = widgetController.getSingleRecentFamilyPostWidget(date); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()) + .extracting( + SingleRecentPostWidgetResponse::authorName, + SingleRecentPostWidgetResponse::authorProfileImageUrl, + SingleRecentPostWidgetResponse::postImageUrl, + SingleRecentPostWidgetResponse::postContent + ).containsExactly( + testMember1.getName(), + testMember1.getProfileImgUrl(), + testPost1.getPostImgUrl(), + testPost1.getContent() + ); + } + + @Test + void 최근_게시글_싱글_위젯_빈_게시글_조회_테스트() { + // given + String date = "2024-10-18"; + + when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); + when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); + when(memberPostService.findLatestPostOfEveryday(familyIds, LocalDate.parse(date), LocalDate.parse(date).plusDays(1))).thenReturn(List.of()); + + // when + ResponseEntity response = widgetController.getSingleRecentFamilyPostWidget(date); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } +} diff --git a/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java b/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java new file mode 100644 index 00000000..9bee2e65 --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java @@ -0,0 +1,222 @@ +package com.oing.restapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.domain.CreateNewUserDTO; +import com.oing.domain.Member; +import com.oing.domain.MemberPost; +import com.oing.domain.SocialLoginProvider; +import com.oing.dto.request.JoinFamilyRequest; +import com.oing.dto.response.DeepLinkResponse; +import com.oing.dto.response.FamilyResponse; +import com.oing.service.*; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@AutoConfigureMockMvc +class WidgetApiTest { + + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private MemberService memberService; + @Autowired + private MemberPostService memberPostService; + @Autowired + private FamilyService familyService; + @Autowired + private DeepLinkService deepLinkService; + @Autowired + private TokenGenerator tokenGenerator; + + + private Member TEST_MEMBER1; + private String TEST_MEMBER1_TOKEN; + private Member TEST_MEMBER2; + private String TEST_MEMBER2_TOKEN; + private Member TEST_MEMBER3; + private String TEST_MEMBER3_TOKEN; + private List TEST_FAMILIES_IDS; + + + @Value("${cloud.ncp.image-optimizer-cdn}") + private String imageOptimizerCdn; + @Value("${cloud.ncp.kb-optimizer-query}") + private String kbOptimizerQuery; + + + @BeforeEach + void setUp() throws Exception { + TEST_MEMBER1 = memberService.createNewMember( + new CreateNewUserDTO( + SocialLoginProvider.fromString("APPLE"), + "testUser1", + "testUser1", + LocalDate.of(1999, 10, 18), + "storage.com/images/1" + ) + ); + TEST_MEMBER1_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER1.getId()).accessToken(); + + TEST_MEMBER2 = memberService.createNewMember( + new CreateNewUserDTO( + SocialLoginProvider.fromString("APPLE"), + "testUser2", + "testUser2", + LocalDate.of(2000, 10, 18), + "storage.com/images/2" + ) + ); + TEST_MEMBER2_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER2.getId()).accessToken(); + + String familyId = objectMapper.readValue( + mockMvc.perform(post("/v1/me/create-family").header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), FamilyResponse.class + ).familyId(); + String inviteCode = objectMapper.readValue( + mockMvc.perform(post("/v1/links/family/{familyId}", familyId).header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), DeepLinkResponse.class + ).getLinkId(); + mockMvc.perform(post("/v1/me/join-family") + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) + ).andExpect(status().isOk()); + + TEST_MEMBER3 = memberService.createNewMember( + new CreateNewUserDTO( + SocialLoginProvider.fromString("APPLE"), + "testUser3", + "testUser3", + LocalDate.of(2001, 10, 18), + "storage.com/images/3" + ) + ); + TEST_MEMBER3_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER3.getId()).accessToken(); + + TEST_FAMILIES_IDS = List.of(TEST_MEMBER1.getId(), TEST_MEMBER2.getId()); + } + + + @Test + void 최근_게시글_싱글_위젯_정상_조회_테스트() throws Exception { + // given + MemberPost testPost1 = new MemberPost( + "testPost1", + TEST_MEMBER1.getId(), + "storage.com/images/1", + "1", + "testPos1" + ); + MemberPost testPost2 = new MemberPost( + "testPost2", + TEST_MEMBER2.getId(), + "storage.com/images/2", + "2", + "testPos2" + ); + MemberPost testPost3 = new MemberPost( + "testPost3", + TEST_MEMBER3.getId(), + "storage.com/images/3", + "3", + "testPos3" + ); + memberPostService.save(testPost1); + memberPostService.save(testPost2); + memberPostService.save(testPost3); + + + // when & then + mockMvc.perform(get("/v1/widgets/single-recent-family-post") + .param("date", LocalDate.now().format(DateTimeFormatter.ISO_DATE)) + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.authorName").value(TEST_MEMBER2.getName())) + .andExpect(jsonPath("$.authorProfileImageUrl").value(imageOptimizerCdn + "/images/2" + kbOptimizerQuery)) + .andExpect(jsonPath("$.postImageUrl").value(imageOptimizerCdn + "/images/2" + kbOptimizerQuery)) + .andExpect(jsonPath("$.postContent").value(testPost2.getContent())); + + } + + + @Test + void 최근_게시글_싱글_위젯_파라미터_없이_조회_테스트() throws Exception { + // given + MemberPost testPost1 = new MemberPost( + "testPost1", + TEST_MEMBER1.getId(), + "storage.com/images/1", + "1", + "testPos1" + ); + MemberPost testPost2 = new MemberPost( + "testPost2", + TEST_MEMBER2.getId(), + "storage.com/images/2", + "2", + "testPos2" + ); + MemberPost testPost3 = new MemberPost( + "testPost3", + TEST_MEMBER3.getId(), + "storage.com/images/3", + "3", + "testPos3" + ); + memberPostService.save(testPost1); + memberPostService.save(testPost2); + memberPostService.save(testPost3); + + + // when & then + mockMvc.perform(get("/v1/widgets/single-recent-family-post") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.authorName").value(TEST_MEMBER2.getName())) + .andExpect(jsonPath("$.authorProfileImageUrl").value(imageOptimizerCdn + "/images/2" + kbOptimizerQuery)) + .andExpect(jsonPath("$.postImageUrl").value(imageOptimizerCdn + "/images/2" + kbOptimizerQuery)) + .andExpect(jsonPath("$.postContent").value(testPost2.getContent())); + + } + + + @Test + void 최근_게시글_싱글_위젯_게시글_없이_조회_테스트() throws Exception { + // when & then + mockMvc.perform(get("/v1/widgets/single-recent-family-post") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + ) + .andExpect(status().isNoContent()); + + } +} From f881b1a0d257a254be73162f68ab1c2f5aca9cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC=20=28YeongMin=20Song=29?= Date: Sun, 28 Jan 2024 19:44:01 +0900 Subject: [PATCH 38/49] =?UTF-8?q?[OING-174]=20feat:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EA=B7=B8=EB=A3=B9=20=EA=B0=80=EC=9E=85=20=EC=9D=BC?= =?UTF-8?q?=EC=9E=90=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add familyJoinAt column * test: fix member related test issues * feat: add migration sql script --- ...V202401281811__add_FamilyJoinAt_column.sql | 2 ++ .../controller/CalendarControllerTest.java | 6 ++-- .../MemberPostRepositoryCustomTest.java | 9 ++++-- .../java/com/oing/restapi/MemberApiTest.java | 4 ++- .../com/oing/restapi/MemberPostApiTest.java | 4 ++- .../restapi/MemberPostCommentApiTest.java | 4 ++- .../restapi/MemberPostReactionApiTest.java | 4 ++- .../restapi/MemberPostRealEmojiApiTest.java | 4 ++- .../oing/restapi/MemberRealEmojiApiTest.java | 4 ++- .../src/main/java/com/oing/domain/Member.java | 11 +++++++ .../response/FamilyMemberProfileResponse.java | 11 +++++-- .../com/oing/dto/response/MemberResponse.java | 4 +++ .../oing/controller/MemberControllerTest.java | 32 ++++++++++++------- .../com/oing/domain/model/MemberTest.java | 10 ++++-- .../oing/domain/model/SocialMemberTest.java | 7 ++-- .../FamilyMemberProfileResponseTest.java | 4 ++- 16 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 gateway/src/main/resources/db/migration/V202401281811__add_FamilyJoinAt_column.sql diff --git a/gateway/src/main/resources/db/migration/V202401281811__add_FamilyJoinAt_column.sql b/gateway/src/main/resources/db/migration/V202401281811__add_FamilyJoinAt_column.sql new file mode 100644 index 00000000..cd8789d1 --- /dev/null +++ b/gateway/src/main/resources/db/migration/V202401281811__add_FamilyJoinAt_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE `member` ADD COLUMN `family_join_at` TIMESTAMP COMMENT '가족가입일시'; +UPDATE member SET family_join_at = created_at WHERE family_id IS NOT NULL; diff --git a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java index 2ce08729..010782c0 100644 --- a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java +++ b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java @@ -48,7 +48,8 @@ class CalendarControllerTest { LocalDate.of(1999, 10, 18), "testMember1", "profile.com/1", - "1" + "1", + LocalDateTime.now() ); private final Member testMember2 = new Member( @@ -57,7 +58,8 @@ class CalendarControllerTest { LocalDate.of(1999, 10, 18), "testMember2", "profile.com/2", - "2" + "2", + LocalDateTime.now() ); private final List familyIds = List.of(testMember1.getId(), testMember2.getId()); diff --git a/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java b/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java index 632b89f0..b743c072 100644 --- a/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java +++ b/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java @@ -45,7 +45,8 @@ class MemberPostRepositoryCustomTest { LocalDate.of(1999, 10, 18), "testMember1", "profile.com/1", - "1" + "1", + LocalDateTime.now() ); private final Member testMember2 = new Member( @@ -54,7 +55,8 @@ class MemberPostRepositoryCustomTest { LocalDate.of(1999, 10, 18), "testMember2", "profile.com/2", - "2" + "2", + LocalDateTime.now() ); private final Member testMember3 = new Member( @@ -63,7 +65,8 @@ class MemberPostRepositoryCustomTest { LocalDate.of(1999, 10, 18), "testMember3", "profile.com/3", - "2" + "2", + LocalDateTime.now() ); private final List familyIds = List.of(testMember1.getId(), testMember2.getId()); diff --git a/gateway/src/test/java/com/oing/restapi/MemberApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberApiTest.java index ab8d5bcb..5b3be1f5 100644 --- a/gateway/src/test/java/com/oing/restapi/MemberApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/MemberApiTest.java @@ -18,6 +18,7 @@ import org.springframework.test.web.servlet.ResultActions; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -51,7 +52,8 @@ void setUp() { TEST_MEMBER_ID, "testUser1", LocalDate.now(), - "", "", "" + "", "", "", + LocalDateTime.now() ) ); TEST_MEMBER_TOKEN = tokenGenerator diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostApiTest.java index a42fe7e4..6fbdfe60 100644 --- a/gateway/src/test/java/com/oing/restapi/MemberPostApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/MemberPostApiTest.java @@ -20,6 +20,7 @@ import org.springframework.test.web.servlet.ResultActions; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.ZonedDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -56,7 +57,8 @@ void setUp() { TEST_MEMBER_ID, "testUser1", LocalDate.now(), - "", "", "" + "", "", "", + LocalDateTime.now() ) ); TEST_MEMBER_TOKEN = tokenGenerator diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostCommentApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostCommentApiTest.java index f690db0c..0b549038 100644 --- a/gateway/src/test/java/com/oing/restapi/MemberPostCommentApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/MemberPostCommentApiTest.java @@ -21,6 +21,7 @@ import org.springframework.test.web.servlet.ResultActions; import java.time.LocalDate; +import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -58,7 +59,8 @@ void setUp() { TEST_MEMBER_ID, "testUser1", LocalDate.now(), - "", "", "" + "", "", "", + LocalDateTime.now() ) ); TEST_MEMBER_TOKEN = tokenGenerator diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostReactionApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostReactionApiTest.java index fdc96658..868adbd1 100644 --- a/gateway/src/test/java/com/oing/restapi/MemberPostReactionApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/MemberPostReactionApiTest.java @@ -22,6 +22,7 @@ import org.springframework.test.web.servlet.ResultActions; import java.time.LocalDate; +import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -58,7 +59,8 @@ void setUp() { TEST_MEMBER_ID, "testUser1", LocalDate.now(), - "", "", "" + "", "", "", + LocalDateTime.now() ) ); TEST_MEMBER_TOKEN = tokenGenerator diff --git a/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java index 9fa60d1a..5046552d 100644 --- a/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java @@ -20,6 +20,7 @@ import org.springframework.test.web.servlet.ResultActions; import java.time.LocalDate; +import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -55,7 +56,8 @@ public class MemberPostRealEmojiApiTest { @BeforeEach void setUp() { memberRepository.save(new Member(TEST_MEMBER_ID, "testUser1", LocalDate.now(), "", - "", "")); + "", "", + LocalDateTime.now())); TEST_MEMBER_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER_ID).accessToken(); memberPostRepository.save(new MemberPost(TEST_POST_ID, TEST_MEMBER_ID, "img", "img", diff --git a/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java b/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java index d5c97f3d..c0478b78 100644 --- a/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java @@ -22,6 +22,7 @@ import org.springframework.test.web.servlet.ResultActions; import java.time.LocalDate; +import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -56,7 +57,8 @@ void setUp() { TEST_MEMBER_ID, "testUser1", LocalDate.now(), - "", "", "" + "", "", "", + LocalDateTime.now() ) ); TEST_MEMBER_TOKEN = tokenGenerator diff --git a/member/src/main/java/com/oing/domain/Member.java b/member/src/main/java/com/oing/domain/Member.java index da21643f..61d0d193 100644 --- a/member/src/main/java/com/oing/domain/Member.java +++ b/member/src/main/java/com/oing/domain/Member.java @@ -6,6 +6,7 @@ import lombok.*; import java.time.LocalDate; +import java.time.LocalDateTime; /** * no5ing-server @@ -39,10 +40,15 @@ public class Member extends DeletableBaseAuditEntity { @Column(name = "profile_img_key") private String profileImgKey; + @Column(name = "family_join_at") + private LocalDateTime familyJoinAt; + + public void setProfileImgKey(String profileImgKey) { this.profileImgKey = profileImgKey; } + public void updateProfileImg(String profileImgUrl, String profileImgKey) { this.profileImgUrl = profileImgUrl; this.profileImgKey = profileImgKey; @@ -60,6 +66,11 @@ public void deleteMemberInfo() { public void setFamilyId(String familyId) { this.familyId = familyId; + if(familyId == null) { + this.familyJoinAt = null; + } else { + this.familyJoinAt = LocalDateTime.now(); + } } public boolean hasFamily() { diff --git a/member/src/main/java/com/oing/dto/response/FamilyMemberProfileResponse.java b/member/src/main/java/com/oing/dto/response/FamilyMemberProfileResponse.java index 5373d469..5c42bbc4 100644 --- a/member/src/main/java/com/oing/dto/response/FamilyMemberProfileResponse.java +++ b/member/src/main/java/com/oing/dto/response/FamilyMemberProfileResponse.java @@ -16,14 +16,19 @@ public record FamilyMemberProfileResponse( @Schema(description = "구성원 프로필 이미지 주소", example = "https://asset.no5ing.kr/post/01HGW2N7EHJVJ4CJ999RRS2E97") String imageUrl, + @Schema(description = "가족 가입 날짜", example = "2023-12-23") + LocalDate familyJoinAt, + @Schema(description = "구성원의 생일", example = "2021-12-05") LocalDate dayOfBirth ) { - public static FamilyMemberProfileResponse of(String memberId, String name, String imageUrl, LocalDate dayOfBirth) { - return new FamilyMemberProfileResponse(memberId, name, imageUrl, dayOfBirth); + public static FamilyMemberProfileResponse of + (String memberId, String name, String imageUrl, LocalDate familyJoinAt, LocalDate dayOfBirth) { + return new FamilyMemberProfileResponse(memberId, name, imageUrl, familyJoinAt, dayOfBirth); } public static FamilyMemberProfileResponse of(Member member) { - return of(member.getId(), member.getName(), member.getProfileImgUrl(), member.getDayOfBirth()); + return of(member.getId(), member.getName(), member.getProfileImgUrl(), + member.getFamilyJoinAt() == null ? null : member.getFamilyJoinAt().toLocalDate(), member.getDayOfBirth()); } } diff --git a/member/src/main/java/com/oing/dto/response/MemberResponse.java b/member/src/main/java/com/oing/dto/response/MemberResponse.java index 2148a70d..f61f6428 100644 --- a/member/src/main/java/com/oing/dto/response/MemberResponse.java +++ b/member/src/main/java/com/oing/dto/response/MemberResponse.java @@ -24,6 +24,9 @@ public record MemberResponse( @Schema(description = "구성원 가족 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E") String familyId, + @Schema(description = "가족 가입 날짜", example = "2023-12-23") + LocalDate familyJoinAt, + @Schema(description = "구성원 생일", example = "2023-12-23") LocalDate dayOfBirth ) { @@ -33,6 +36,7 @@ public static MemberResponse of(Member member) { member.getName(), member.getProfileImgUrl(), member.getFamilyId(), + member.getFamilyJoinAt() == null ? null : member.getFamilyJoinAt().toLocalDate(), member.getDayOfBirth() ); } diff --git a/member/src/test/java/com/oing/controller/MemberControllerTest.java b/member/src/test/java/com/oing/controller/MemberControllerTest.java index d8b01ce8..08d352a7 100644 --- a/member/src/test/java/com/oing/controller/MemberControllerTest.java +++ b/member/src/test/java/com/oing/controller/MemberControllerTest.java @@ -24,6 +24,7 @@ import java.security.InvalidParameterException; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.*; @@ -48,7 +49,8 @@ public class MemberControllerTest { void 멤버_프로필_조회_테스트() { // given Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), - "testMember1", "http://test.com/test-profile.jpg", null); + "testMember1", "http://test.com/test-profile.jpg", null, + LocalDateTime.now()); when(memberService.findMemberById(any())).thenReturn(member); // when @@ -66,15 +68,17 @@ public class MemberControllerTest { void 가족_멤버_프로필_조회_테스트() { // given Member member1 = new Member("1", "1", LocalDate.of(2000, 7, 8), - "testMember1", "http://test.com/test-profile.jpg", null); + "testMember1", "http://test.com/test-profile.jpg", null, + LocalDateTime.now()); Member member2 = new Member("2", "1", LocalDate.of(2003, 7, 26), - "testMember2", null, null); + "testMember2", null, null, + LocalDateTime.now()); String familyId = "1"; when(authenticationHolder.getUserId()).thenReturn("1"); when(memberService.findFamilyIdByMemberId(anyString())).thenReturn(familyId); Page profilePage = new PageImpl<>(Arrays.asList( - new FamilyMemberProfileResponse(member1.getId(), member1.getName(), member1.getProfileImgUrl(), member1.getDayOfBirth()), - new FamilyMemberProfileResponse(member2.getId(), member2.getName(), member2.getProfileImgUrl(), member2.getDayOfBirth()) + new FamilyMemberProfileResponse(member1.getId(), member1.getName(), member1.getProfileImgUrl(), member1.getFamilyJoinAt().toLocalDate(), member1.getDayOfBirth()), + new FamilyMemberProfileResponse(member2.getId(), member2.getName(), member2.getProfileImgUrl(),member2.getFamilyJoinAt().toLocalDate(), member2.getDayOfBirth()) )); when(memberService.findFamilyMembersProfilesByFamilyId(familyId, 1, 5)) .thenReturn(profilePage); @@ -93,7 +97,8 @@ public class MemberControllerTest { // given String newName = "newName"; Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), - "testMember1", "http://test.com/test-profile.jpg", null); + "testMember1", "http://test.com/test-profile.jpg", null, + LocalDateTime.now()); when(memberService.findMemberById(any())).thenReturn(member); when(authenticationHolder.getUserId()).thenReturn("1"); @@ -110,7 +115,8 @@ public class MemberControllerTest { // given String newName = "wrong-length-nam"; Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), - "testMember1", "http://test.com/test-profile.jpg", null); + "testMember1", "http://test.com/test-profile.jpg", null, + LocalDateTime.now()); when(memberService.findMemberById(any())).thenReturn(member); when(authenticationHolder.getUserId()).thenReturn("1"); @@ -126,7 +132,8 @@ public class MemberControllerTest { // given String newName = ""; Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), - "testMember1", "http://test.com/test-profile.jpg", null); + "testMember1", "http://test.com/test-profile.jpg", null, + LocalDateTime.now()); when(memberService.findMemberById(any())).thenReturn(member); when(authenticationHolder.getUserId()).thenReturn("1"); @@ -157,7 +164,8 @@ public class MemberControllerTest { // given String newProfileImageUrl = "http://test.com/profile.jpg"; Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), - "testMember1", "http://test.com/test-profile.jpg", null); + "testMember1", "http://test.com/test-profile.jpg", null, + LocalDateTime.now()); when(memberService.findMemberById(any())).thenReturn(member); when(authenticationHolder.getUserId()).thenReturn("1"); when(preSignedUrlGenerator.extractImageKey(any())).thenReturn("/profile.jpg"); @@ -174,7 +182,8 @@ public class MemberControllerTest { void 멤버_탈퇴_테스트() { // given Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), - "testMember1", "http://test.com/test-profile.jpg", null); + "testMember1", "http://test.com/test-profile.jpg", null, + LocalDateTime.now()); when(memberService.findMemberById(any())).thenReturn(member); when(authenticationHolder.getUserId()).thenReturn("1"); @@ -190,7 +199,8 @@ public class MemberControllerTest { void 잘못된_요청의_멤버_탈퇴_예외_테스트() { // given Member member = new Member("1", "1", LocalDate.of(2000, 7, 8), - "testMember1", "http://test.com/test-profile.jpg", null); + "testMember1", "http://test.com/test-profile.jpg", null, + LocalDateTime.now()); when(authenticationHolder.getUserId()).thenReturn("2"); // then diff --git a/member/src/test/java/com/oing/domain/model/MemberTest.java b/member/src/test/java/com/oing/domain/model/MemberTest.java index ebe960e9..4f6c2200 100644 --- a/member/src/test/java/com/oing/domain/model/MemberTest.java +++ b/member/src/test/java/com/oing/domain/model/MemberTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import java.time.LocalDate; +import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -27,7 +28,8 @@ void testMemberConstructorAndGetters() { String name = "sampleName"; // When - Member member = new Member(memberId, familyId, dayofBirth, name, null, null); + Member member = new Member(memberId, familyId, dayofBirth, name, null, null, + LocalDateTime.now()); // Then assertNotNull(member); @@ -44,8 +46,10 @@ void testMemberEqualsAndHashCode() { String name = "sampleName"; // When - Member member1 = new Member(memberId, familyId, dayofBirth, name, null, null); - Member member2 = new Member(memberId, familyId, dayofBirth, name, null, null); + Member member1 = new Member(memberId, familyId, dayofBirth, name, null, null, + LocalDateTime.now()); + Member member2 = new Member(memberId, familyId, dayofBirth, name, null, null, + LocalDateTime.now()); // Then assertEquals(member1, member2); diff --git a/member/src/test/java/com/oing/domain/model/SocialMemberTest.java b/member/src/test/java/com/oing/domain/model/SocialMemberTest.java index efd7cf7c..62e8674a 100644 --- a/member/src/test/java/com/oing/domain/model/SocialMemberTest.java +++ b/member/src/test/java/com/oing/domain/model/SocialMemberTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import java.time.LocalDate; +import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -28,7 +29,8 @@ void testSocialMemberConstructorAndGetters() { // When Member member = new Member("sampleId", "sampleFamilyId", - LocalDate.of(2023, 7, 8), "sampleName", null, null); + LocalDate.of(2023, 7, 8), "sampleName", null, null, + LocalDateTime.now()); // When SocialMember socialMember = new SocialMember(provider, identifier, member); @@ -47,7 +49,8 @@ void testSocialMemberEqualsAndHashCode() { SocialLoginProvider provider = SocialLoginProvider.APPLE; String identifier = "user123"; Member member = new Member("sampleId", "sampleFamilyId", - LocalDate.of(2023, 7, 8), "sampleName", null, null); + LocalDate.of(2023, 7, 8), "sampleName", null, null, + LocalDateTime.now()); // When SocialMember socialMember1 = new SocialMember(provider, identifier, member); diff --git a/member/src/test/java/com/oing/dto/response/FamilyMemberProfileResponseTest.java b/member/src/test/java/com/oing/dto/response/FamilyMemberProfileResponseTest.java index cbec8bbf..cea52360 100644 --- a/member/src/test/java/com/oing/dto/response/FamilyMemberProfileResponseTest.java +++ b/member/src/test/java/com/oing/dto/response/FamilyMemberProfileResponseTest.java @@ -16,14 +16,16 @@ void testFamilyMemberProfileResponse() { String name = "디프만"; String imageUrl = "https://asset.no5ing.kr/post/01HGW2N7EHJVJ4CJ999RRS2E97"; LocalDate dayOfBirth = LocalDate.of(2000, 7, 8); + LocalDate familyJoinAt = LocalDate.of(2000, 7, 8); // when - FamilyMemberProfileResponse response = new FamilyMemberProfileResponse(memberId, name, imageUrl, dayOfBirth); + FamilyMemberProfileResponse response = new FamilyMemberProfileResponse(memberId, name, imageUrl, dayOfBirth, familyJoinAt); // then assertEquals(response.memberId(), memberId); assertEquals(response.name(), name); assertEquals(response.imageUrl(), imageUrl); assertEquals(response.dayOfBirth(), dayOfBirth); + assertEquals(response.familyJoinAt(), familyJoinAt); } } From a84886c912ae6bd3758a4997f97c711666fe3600 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Tue, 30 Jan 2024 17:07:27 +0900 Subject: [PATCH 39/49] =?UTF-8?q?=20[OING-179]=20refactor:=20SingleRecentP?= =?UTF-8?q?ostWidget=EC=97=90=20PostId=20=EC=B6=94=EA=B0=80=20(#128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/oing/controller/WidgetController.java | 1 + .../com/oing/dto/response/SingleRecentPostWidgetResponse.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/gateway/src/main/java/com/oing/controller/WidgetController.java b/gateway/src/main/java/com/oing/controller/WidgetController.java index 03d89fe3..08d7face 100644 --- a/gateway/src/main/java/com/oing/controller/WidgetController.java +++ b/gateway/src/main/java/com/oing/controller/WidgetController.java @@ -46,6 +46,7 @@ public ResponseEntity getSingleRecentFamilyPostW Member author = memberService.findMemberById(latestPost.getMemberId()); return ResponseEntity.ok(new SingleRecentPostWidgetResponse( author.getName(), + latestPost.getId(), optimizedImageUrlGenerator.getKBImageUrlGenerator(author.getProfileImgUrl()), optimizedImageUrlGenerator.getKBImageUrlGenerator(latestPost.getPostImgUrl()), latestPost.getContent() diff --git a/gateway/src/main/java/com/oing/dto/response/SingleRecentPostWidgetResponse.java b/gateway/src/main/java/com/oing/dto/response/SingleRecentPostWidgetResponse.java index fc8e9c13..5c77b1e6 100644 --- a/gateway/src/main/java/com/oing/dto/response/SingleRecentPostWidgetResponse.java +++ b/gateway/src/main/java/com/oing/dto/response/SingleRecentPostWidgetResponse.java @@ -11,6 +11,9 @@ public record SingleRecentPostWidgetResponse( @Schema(description = "게시자 프로필 사진 주소", example = "https://asset.no5ing.kr/post/01HGW2N7EHJVJ4CJ999RRS2E97") String authorProfileImageUrl, + @Schema(description = "피드 게시물 아이디", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String postId, + @Schema(description = "피드 게시물 사진 주소", example = "https://asset.no5ing.kr/post/01HGW2N7EHJVJ4CJ999RRS2E97") String postImageUrl, From 6061ac9763b121b1df0733876163ec1dbef45c93 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Tue, 30 Jan 2024 18:06:26 +0900 Subject: [PATCH 40/49] =?UTF-8?q?=20[OING-179]=20fix:=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EA=B0=80=20=EC=9E=98=EB=AA=BB=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=9C=20SingleRecentPostWidgetResponse=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9E=90=EB=A5=BC=20=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/src/main/java/com/oing/controller/WidgetController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/src/main/java/com/oing/controller/WidgetController.java b/gateway/src/main/java/com/oing/controller/WidgetController.java index 08d7face..9f685dbe 100644 --- a/gateway/src/main/java/com/oing/controller/WidgetController.java +++ b/gateway/src/main/java/com/oing/controller/WidgetController.java @@ -46,8 +46,8 @@ public ResponseEntity getSingleRecentFamilyPostW Member author = memberService.findMemberById(latestPost.getMemberId()); return ResponseEntity.ok(new SingleRecentPostWidgetResponse( author.getName(), - latestPost.getId(), optimizedImageUrlGenerator.getKBImageUrlGenerator(author.getProfileImgUrl()), + latestPost.getId(), optimizedImageUrlGenerator.getKBImageUrlGenerator(latestPost.getPostImgUrl()), latestPost.getContent() )); From b11924ffd838847f5e029cb0a7fe960e2e173843 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 31 Jan 2024 02:28:16 +0900 Subject: [PATCH 41/49] feat: add parameter hidden option to familyId (#131) --- gateway/src/main/java/com/oing/restapi/CalendarApi.java | 1 + post/src/main/java/com/oing/restapi/MemberPostApi.java | 1 + 2 files changed, 2 insertions(+) diff --git a/gateway/src/main/java/com/oing/restapi/CalendarApi.java b/gateway/src/main/java/com/oing/restapi/CalendarApi.java index 8c076379..cedf5248 100644 --- a/gateway/src/main/java/com/oing/restapi/CalendarApi.java +++ b/gateway/src/main/java/com/oing/restapi/CalendarApi.java @@ -32,6 +32,7 @@ ArrayResponse getMonthlyCalendar( @Parameter(description = "조회할 년월", example = "2021-12") String yearMonth, + @Parameter(hidden = true) @FamilyId String familyId ); diff --git a/post/src/main/java/com/oing/restapi/MemberPostApi.java b/post/src/main/java/com/oing/restapi/MemberPostApi.java index 10983435..875689ed 100644 --- a/post/src/main/java/com/oing/restapi/MemberPostApi.java +++ b/post/src/main/java/com/oing/restapi/MemberPostApi.java @@ -70,6 +70,7 @@ PostResponse createPost( @RequestBody CreatePostRequest request, + @Parameter(hidden = true) @FamilyId String familyId ); From fa50af65660b32e43f9501bbd94fb39b3cec07a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC=20=28YeongMin=20Song=29?= Date: Wed, 31 Jan 2024 12:47:35 +0900 Subject: [PATCH 42/49] =?UTF-8?q?[OING-181]=20feat:=20=EC=BA=98=EB=A6=B0?= =?UTF-8?q?=EB=8D=94=20=ED=86=B5=EA=B3=84=20API=20=EC=B6=94=EA=B0=80=20(#1?= =?UTF-8?q?30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/oing/controller/FamilyController.java | 10 ---------- .../response/FamilyMonthlyStatisticsResponse.java | 8 +------- .../src/main/java/com/oing/restapi/FamilyApi.java | 12 ------------ .../com/oing/controller/CalendarController.java | 15 +++++++++++++++ .../MemberPostRepositoryCustomImpl.java | 12 ++++++++++++ .../main/java/com/oing/restapi/CalendarApi.java | 9 +++++++++ .../repository/MemberPostRepositoryCustom.java | 2 ++ .../java/com/oing/service/MemberPostService.java | 5 +++++ 8 files changed, 44 insertions(+), 29 deletions(-) diff --git a/family/src/main/java/com/oing/controller/FamilyController.java b/family/src/main/java/com/oing/controller/FamilyController.java index 8ec8c077..09cecdab 100644 --- a/family/src/main/java/com/oing/controller/FamilyController.java +++ b/family/src/main/java/com/oing/controller/FamilyController.java @@ -15,16 +15,6 @@ public class FamilyController implements FamilyApi { private final FamilyService familyService; - @Override - public FamilyMonthlyStatisticsResponse getMonthlyFamilyStatistics(String familyId, String yearMonth) { - Random random = new Random(); - return new FamilyMonthlyStatisticsResponse( - random.nextInt(3, 5), - random.nextInt(5, 10), - random.nextInt(0, 3) - ); - } - @Override public FamilyResponse createFamily() { Family family = familyService.createFamily(); diff --git a/family/src/main/java/com/oing/dto/response/FamilyMonthlyStatisticsResponse.java b/family/src/main/java/com/oing/dto/response/FamilyMonthlyStatisticsResponse.java index f49a0bcf..362d6924 100644 --- a/family/src/main/java/com/oing/dto/response/FamilyMonthlyStatisticsResponse.java +++ b/family/src/main/java/com/oing/dto/response/FamilyMonthlyStatisticsResponse.java @@ -4,13 +4,7 @@ @Schema(description = "월별 요약 정보") public record FamilyMonthlyStatisticsResponse( - @Schema(description = "모두 참여한 날", example = "12") - Integer totalParticipateCnt, - @Schema(description = "전체 사진 수", example = "124") - Integer totalImageCnt, - - @Schema(description = "나의 사진 수", example = "38") - Integer myImageCnt + Integer totalImageCnt ) { } diff --git a/family/src/main/java/com/oing/restapi/FamilyApi.java b/family/src/main/java/com/oing/restapi/FamilyApi.java index ea795156..374aeae8 100644 --- a/family/src/main/java/com/oing/restapi/FamilyApi.java +++ b/family/src/main/java/com/oing/restapi/FamilyApi.java @@ -13,18 +13,6 @@ @Valid @RequestMapping("/v1/families") public interface FamilyApi { - @Operation(summary = "가족 요약 정보 조회", description = "월별 가족 요약 정보를 조회합니다.") - @GetMapping("/{familyId}/summary") - FamilyMonthlyStatisticsResponse getMonthlyFamilyStatistics( - @Parameter(description = "조회할 가족 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") - @PathVariable - String familyId, - - @RequestParam(required = false) - @Parameter(description = "조회할 년월", example = "2021-12") - String yearMonth - ); - @Operation(summary = "가족 생성", description = "가족을 생성합니다.") @PostMapping FamilyResponse createFamily(); diff --git a/gateway/src/main/java/com/oing/controller/CalendarController.java b/gateway/src/main/java/com/oing/controller/CalendarController.java index f5c815e3..01775086 100644 --- a/gateway/src/main/java/com/oing/controller/CalendarController.java +++ b/gateway/src/main/java/com/oing/controller/CalendarController.java @@ -7,6 +7,7 @@ import com.oing.dto.response.ArrayResponse; import com.oing.dto.response.BannerResponse; import com.oing.dto.response.CalendarResponse; +import com.oing.dto.response.FamilyMonthlyStatisticsResponse; import com.oing.restapi.CalendarApi; import com.oing.service.MemberPostService; import com.oing.service.MemberService; @@ -91,4 +92,18 @@ public BannerResponse getBanner(String yearMonth) { BannerImageType.values()[new Random().nextInt(BannerImageType.values().length)] ); } + + @Override + public FamilyMonthlyStatisticsResponse getSummary(String yearMonth) { + String memberId = tokenAuthenticationHolder.getUserId(); + String[] yearMonthArray = yearMonth.split("-"); + int year = Integer.parseInt(yearMonthArray[0]); + int month = Integer.parseInt(yearMonthArray[1]); + + String familyId = memberService.findFamilyIdByMemberId(memberId); + long monthlyPostCount = memberPostService.countMonthlyPostByFamilyId(year, month, familyId); + return new FamilyMonthlyStatisticsResponse( + (int) monthlyPostCount + ); + } } diff --git a/gateway/src/main/java/com/oing/repository/MemberPostRepositoryCustomImpl.java b/gateway/src/main/java/com/oing/repository/MemberPostRepositoryCustomImpl.java index a89e4783..5fe04e0f 100644 --- a/gateway/src/main/java/com/oing/repository/MemberPostRepositoryCustomImpl.java +++ b/gateway/src/main/java/com/oing/repository/MemberPostRepositoryCustomImpl.java @@ -67,6 +67,18 @@ public QueryResults searchPosts(int page, int size, LocalDate date, .fetchResults(); } + @Override + public long countMonthlyPostByFamilyId(int year, int month, String familyId) { + return queryFactory + .select(memberPost.count()) + .from(memberPost) + .leftJoin(member).on(memberPost.memberId.eq(member.id)) + .where(member.familyId.eq(familyId), + memberPost.createdAt.year().eq(year), + memberPost.createdAt.month().eq(month)) + .fetchFirst(); + } + private BooleanExpression eqDate(LocalDate date) { DateTimeTemplate createdAtDate = Expressions.dateTimeTemplate(LocalDate.class, "DATE({0})", memberPost.createdAt); diff --git a/gateway/src/main/java/com/oing/restapi/CalendarApi.java b/gateway/src/main/java/com/oing/restapi/CalendarApi.java index cedf5248..b654c901 100644 --- a/gateway/src/main/java/com/oing/restapi/CalendarApi.java +++ b/gateway/src/main/java/com/oing/restapi/CalendarApi.java @@ -3,6 +3,7 @@ import com.oing.dto.response.ArrayResponse; import com.oing.dto.response.BannerResponse; import com.oing.dto.response.CalendarResponse; +import com.oing.dto.response.FamilyMonthlyStatisticsResponse; import com.oing.util.security.FamilyId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -44,4 +45,12 @@ BannerResponse getBanner( @Parameter(description = "조회할 년월", example = "2021-12") String yearMonth ); + + @Operation(summary = "캘린더 통계 조회", description = "캘린더의 통계를 조회합니다.") + @GetMapping("/summary") + FamilyMonthlyStatisticsResponse getSummary( + @RequestParam(required = false) + @Parameter(description = "조회할 년월", example = "2021-12") + String yearMonth + ); } diff --git a/post/src/main/java/com/oing/repository/MemberPostRepositoryCustom.java b/post/src/main/java/com/oing/repository/MemberPostRepositoryCustom.java index 0acada6e..bdcfb01d 100644 --- a/post/src/main/java/com/oing/repository/MemberPostRepositoryCustom.java +++ b/post/src/main/java/com/oing/repository/MemberPostRepositoryCustom.java @@ -16,5 +16,7 @@ public interface MemberPostRepositoryCustom { QueryResults searchPosts(int page, int size, LocalDate date, String memberId, String requesterMemberId, String familyId, boolean asc); + long countMonthlyPostByFamilyId(int year, int month, String familyId); + boolean existsByMemberIdAndCreatedAt(String memberId, LocalDate postDate); } diff --git a/post/src/main/java/com/oing/service/MemberPostService.java b/post/src/main/java/com/oing/service/MemberPostService.java index 3d46af10..ef81a161 100644 --- a/post/src/main/java/com/oing/service/MemberPostService.java +++ b/post/src/main/java/com/oing/service/MemberPostService.java @@ -97,4 +97,9 @@ public void deleteMemberPostById(String postId) { applicationEventPublisher.publishEvent(new DeleteMemberPostEvent(memberPost)); memberPostRepository.delete(memberPost); } + + @Transactional + public long countMonthlyPostByFamilyId(int year, int month, String familyId) { + return memberPostRepository.countMonthlyPostByFamilyId(year, month, familyId); + } } From b7e16b8425e32876dc6d828c05abe237e9ab358d Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:29:55 +0900 Subject: [PATCH 43/49] =?UTF-8?q?[OING-185]=20chore:=20Embedded=20Redis=20?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EC=97=B0=EA=B2=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-test.yaml | 2 +- .../com/oing/support/EmbeddedRedisConfig.java | 71 ++++++++++++++++--- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/gateway/src/main/resources/application-test.yaml b/gateway/src/main/resources/application-test.yaml index 95793fac..5849a0e9 100644 --- a/gateway/src/main/resources/application-test.yaml +++ b/gateway/src/main/resources/application-test.yaml @@ -19,7 +19,7 @@ spring: data: redis: host: localhost - port: 16379 + port: 6379 app: external-urls: slack-webhook: https://www.naver.com # Must Be Replaced diff --git a/gateway/src/test/java/com/oing/support/EmbeddedRedisConfig.java b/gateway/src/test/java/com/oing/support/EmbeddedRedisConfig.java index a6fbec60..34ee6b9a 100644 --- a/gateway/src/test/java/com/oing/support/EmbeddedRedisConfig.java +++ b/gateway/src/test/java/com/oing/support/EmbeddedRedisConfig.java @@ -5,32 +5,81 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.util.StringUtils; import redis.embedded.RedisServer; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + @Configuration @Profile("test") public class EmbeddedRedisConfig { @Value("${spring.data.redis.port}") - private int port; + private int redisPort; private RedisServer redisServer; @PostConstruct - public void startRedis() { - try { - redisServer = RedisServer.builder() - .port(port) - .setting("maxmemory 256M") - .build(); - redisServer.start(); - } catch (Exception e) { - e.printStackTrace(); - } + public void startRedis() throws IOException { + int port = isRedisRunning()? findAvailablePort() : redisPort; + redisServer = new RedisServer(port); + redisServer.start(); } @PreDestroy public void stopRedis() { redisServer.stop(); } + + /** + * Embedded Redis가 현재 실행중인지 확인 + */ + private boolean isRedisRunning() throws IOException { + return isRunning(executeGrepProcessCommand(redisPort)); + } + + /** + * 현재 PC/서버에서 사용가능한 포트 조회 + */ + public int findAvailablePort() throws IOException { + + for (int port = 10000; port <= 65535; port++) { + Process process = executeGrepProcessCommand(port); + if (!isRunning(process)) { + return port; + } + } + + throw new IllegalArgumentException("Not Found Available port: 10000 ~ 65535"); + } + + /** + * 해당 port를 사용중인 프로세스 확인하는 sh 실행 + */ + private Process executeGrepProcessCommand(int port) throws IOException { + String command = String.format("netstat -nat | grep LISTEN|grep %d", port); + String[] shell = {"/bin/sh", "-c", command}; + return Runtime.getRuntime().exec(shell); + } + + /** + * 해당 Process가 현재 실행중인지 확인 + */ + private boolean isRunning(Process process) { + String line; + StringBuilder pidInfo = new StringBuilder(); + + try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + + while ((line = input.readLine()) != null) { + pidInfo.append(line); + } + + } catch (Exception e) { + } + + return !StringUtils.isEmpty(pidInfo.toString()); + } } From 700c63ebcf66cfb583807df67322d9ca18bd47a7 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Wed, 31 Jan 2024 20:30:03 +0900 Subject: [PATCH 44/49] =?UTF-8?q?[OING-173]=20feat:=20MVP=202=EC=B0=A8=20?= =?UTF-8?q?=EC=B6=94=EC=96=B5=20=EC=BA=98=EB=A6=B0=EB=8D=94=20=EB=B0=B0?= =?UTF-8?q?=EB=84=88=20=EA=B5=AC=ED=98=84=20(#132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add familyScore definition * feat: Add JpaEntity and SpringEvent to add score per user activity * feat: Add the events to substract score per canceled user activity * fix: Change FamilyScoreEventListener annotation to fix not committed score event * feat: Impl the code to calculate the familyTopPercentage in getBanner API * feat: Impl the code to count family activity in getBanner API * feat: Add counting code to banners dynamic fields(allFamilyMembersUploadedDays, allFamilyMembersUploadedStreaks) and Impl getBanner API with a lot of comments * fix: Fix broken test code with changed Member entity * fix: Handle divide by zero error from calculateFamilyTopPercentile method * fix: Replace inclusiveToday as endDate from the code to calculate dynamic field of getBanner API to fix wrongly selected period * chore: Clear the commenting at redis cache annotation in MemberPostController --- .gitignore | 1 + .../event/MemberPostCommentCreatedEvent.java | 15 +++ .../event/MemberPostCommentDeletedEvent.java | 15 +++ .../oing/event/MemberPostCreatedEvent.java | 15 +++ .../oing/event/MemberPostDeletedEvent.java | 15 +++ .../event/MemberPostReactionCreatedEvent.java | 15 +++ .../event/MemberPostReactionDeletedEvent.java | 15 +++ .../MemberPostRealEmojiCreatedEvent.java | 15 +++ .../MemberPostRealEmojiDeletedEvent.java | 15 +++ .../src/main/java/com/oing/domain/Family.java | 62 ++++++++++++ .../oing/event/FamilyScoreEventListener.java | 79 +++++++++++++++ .../com/oing/repository/FamilyRepository.java | 1 + .../java/com/oing/service/FamilyService.java | 13 +++ .../oing/controller/CalendarController.java | 97 +++++++++++++++---- .../java/com/oing/domain/BannerImageType.java | 2 +- .../java/com/oing/restapi/CalendarApi.java | 6 +- .../V202401271853__add_familyScore_column.sql | 1 + .../controller/CalendarControllerTest.java | 3 +- .../oing/controller/WidgetControllerTest.java | 7 +- .../java/com/oing/restapi/WidgetApiTest.java | 18 ++-- .../src/main/java/com/oing/domain/Member.java | 1 + .../com/oing/repository/MemberRepository.java | 4 + .../java/com/oing/service/MemberService.java | 15 ++- .../main/java/com/oing/domain/MemberPost.java | 1 + .../com/oing/domain/MemberPostComment.java | 1 + .../MemberPostCommentEntityListener.java | 32 ++++++ .../oing/domain/MemberPostEntityListener.java | 32 ++++++ .../com/oing/domain/MemberPostReaction.java | 1 + .../MemberPostReactionEntityListener.java | 32 ++++++ .../com/oing/domain/MemberPostRealEmoji.java | 1 + .../MemberPostRealEmojiEntityListener.java | 32 ++++++ .../MemberPostCommentRepository.java | 5 + .../MemberPostReactionRepository.java | 3 + .../MemberPostRealEmojiRepository.java | 4 + .../oing/repository/MemberPostRepository.java | 6 ++ .../service/MemberPostCommentService.java | 15 +++ .../service/MemberPostReactionService.java | 21 +++- .../service/MemberPostRealEmojiService.java | 15 +++ .../com/oing/service/MemberPostService.java | 28 ++++-- 39 files changed, 609 insertions(+), 50 deletions(-) create mode 100644 common/src/main/java/com/oing/event/MemberPostCommentCreatedEvent.java create mode 100644 common/src/main/java/com/oing/event/MemberPostCommentDeletedEvent.java create mode 100644 common/src/main/java/com/oing/event/MemberPostCreatedEvent.java create mode 100644 common/src/main/java/com/oing/event/MemberPostDeletedEvent.java create mode 100644 common/src/main/java/com/oing/event/MemberPostReactionCreatedEvent.java create mode 100644 common/src/main/java/com/oing/event/MemberPostReactionDeletedEvent.java create mode 100644 common/src/main/java/com/oing/event/MemberPostRealEmojiCreatedEvent.java create mode 100644 common/src/main/java/com/oing/event/MemberPostRealEmojiDeletedEvent.java create mode 100644 family/src/main/java/com/oing/event/FamilyScoreEventListener.java create mode 100644 gateway/src/main/resources/db/migration/V202401271853__add_familyScore_column.sql create mode 100644 post/src/main/java/com/oing/domain/MemberPostCommentEntityListener.java create mode 100644 post/src/main/java/com/oing/domain/MemberPostEntityListener.java create mode 100644 post/src/main/java/com/oing/domain/MemberPostReactionEntityListener.java create mode 100644 post/src/main/java/com/oing/domain/MemberPostRealEmojiEntityListener.java diff --git a/.gitignore b/.gitignore index 4501d122..7e471013 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ out/ application-local.yaml .env +data.sql ### QueryDSL ### generated/ \ No newline at end of file diff --git a/common/src/main/java/com/oing/event/MemberPostCommentCreatedEvent.java b/common/src/main/java/com/oing/event/MemberPostCommentCreatedEvent.java new file mode 100644 index 00000000..fb0f3916 --- /dev/null +++ b/common/src/main/java/com/oing/event/MemberPostCommentCreatedEvent.java @@ -0,0 +1,15 @@ +package com.oing.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberPostCommentCreatedEvent extends ApplicationEvent { + + private final String memberId; + + public MemberPostCommentCreatedEvent(Object source, String memberId) { + super(source); + this.memberId = memberId; + } +} diff --git a/common/src/main/java/com/oing/event/MemberPostCommentDeletedEvent.java b/common/src/main/java/com/oing/event/MemberPostCommentDeletedEvent.java new file mode 100644 index 00000000..b07de9e7 --- /dev/null +++ b/common/src/main/java/com/oing/event/MemberPostCommentDeletedEvent.java @@ -0,0 +1,15 @@ +package com.oing.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberPostCommentDeletedEvent extends ApplicationEvent { + + private final String memberId; + + public MemberPostCommentDeletedEvent(Object source, String memberId) { + super(source); + this.memberId = memberId; + } +} diff --git a/common/src/main/java/com/oing/event/MemberPostCreatedEvent.java b/common/src/main/java/com/oing/event/MemberPostCreatedEvent.java new file mode 100644 index 00000000..4ceb617b --- /dev/null +++ b/common/src/main/java/com/oing/event/MemberPostCreatedEvent.java @@ -0,0 +1,15 @@ +package com.oing.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberPostCreatedEvent extends ApplicationEvent { + + private final String memberId; + + public MemberPostCreatedEvent(Object source, String memberId) { + super(source); + this.memberId = memberId; + } +} diff --git a/common/src/main/java/com/oing/event/MemberPostDeletedEvent.java b/common/src/main/java/com/oing/event/MemberPostDeletedEvent.java new file mode 100644 index 00000000..5b6b63e0 --- /dev/null +++ b/common/src/main/java/com/oing/event/MemberPostDeletedEvent.java @@ -0,0 +1,15 @@ +package com.oing.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberPostDeletedEvent extends ApplicationEvent { + + private final String memberId; + + public MemberPostDeletedEvent(Object source, String memberId) { + super(source); + this.memberId = memberId; + } +} diff --git a/common/src/main/java/com/oing/event/MemberPostReactionCreatedEvent.java b/common/src/main/java/com/oing/event/MemberPostReactionCreatedEvent.java new file mode 100644 index 00000000..e263c7ad --- /dev/null +++ b/common/src/main/java/com/oing/event/MemberPostReactionCreatedEvent.java @@ -0,0 +1,15 @@ +package com.oing.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberPostReactionCreatedEvent extends ApplicationEvent { + + private final String memberId; + + public MemberPostReactionCreatedEvent(Object source, String memberId) { + super(source); + this.memberId = memberId; + } +} diff --git a/common/src/main/java/com/oing/event/MemberPostReactionDeletedEvent.java b/common/src/main/java/com/oing/event/MemberPostReactionDeletedEvent.java new file mode 100644 index 00000000..2de7d4f2 --- /dev/null +++ b/common/src/main/java/com/oing/event/MemberPostReactionDeletedEvent.java @@ -0,0 +1,15 @@ +package com.oing.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberPostReactionDeletedEvent extends ApplicationEvent { + + private final String memberId; + + public MemberPostReactionDeletedEvent(Object source, String memberId) { + super(source); + this.memberId = memberId; + } +} diff --git a/common/src/main/java/com/oing/event/MemberPostRealEmojiCreatedEvent.java b/common/src/main/java/com/oing/event/MemberPostRealEmojiCreatedEvent.java new file mode 100644 index 00000000..c86b8416 --- /dev/null +++ b/common/src/main/java/com/oing/event/MemberPostRealEmojiCreatedEvent.java @@ -0,0 +1,15 @@ +package com.oing.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberPostRealEmojiCreatedEvent extends ApplicationEvent { + + private final String memberId; + + public MemberPostRealEmojiCreatedEvent(Object source, String memberId) { + super(source); + this.memberId = memberId; + } +} diff --git a/common/src/main/java/com/oing/event/MemberPostRealEmojiDeletedEvent.java b/common/src/main/java/com/oing/event/MemberPostRealEmojiDeletedEvent.java new file mode 100644 index 00000000..1902edc1 --- /dev/null +++ b/common/src/main/java/com/oing/event/MemberPostRealEmojiDeletedEvent.java @@ -0,0 +1,15 @@ +package com.oing.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberPostRealEmojiDeletedEvent extends ApplicationEvent { + + private final String memberId; + + public MemberPostRealEmojiDeletedEvent(Object source, String memberId) { + super(source); + this.memberId = memberId; + } +} diff --git a/family/src/main/java/com/oing/domain/Family.java b/family/src/main/java/com/oing/domain/Family.java index 2ee13c9e..e73e5fe1 100644 --- a/family/src/main/java/com/oing/domain/Family.java +++ b/family/src/main/java/com/oing/domain/Family.java @@ -16,4 +16,66 @@ public class Family extends BaseEntity { @Id @Column(name = "family_id", columnDefinition = "CHAR(26)", nullable = false) private String id; + + @Column(name = "score", nullable = false) + private Integer score = 0; + + + public Family(String id) { + this.id = id; + } + + private static final int NEW_POST_SCORE = 20; + private static final int ALL_FAMILY_MEMBERS_POSTS_UPLOADED_SCORE = 50; + private static final int NEW_COMMENT_SCORE = 5; + private static final int NEW_REACTION_SCORE = 1; + private static final int NEW_REAL_EMOJI_SCORE = 3; + + public void addNewPostScore() { + addScore(NEW_POST_SCORE); + } + + public void subtractNewPostScore() { + subtractScore(NEW_POST_SCORE); + } + + public void addAllFamilyMembersPostsUploadedScore() { + addScore(ALL_FAMILY_MEMBERS_POSTS_UPLOADED_SCORE); + } + + public void subtractAllFamilyMembersPostsUploadedScore() { + subtractScore(ALL_FAMILY_MEMBERS_POSTS_UPLOADED_SCORE); + } + + public void addNewCommentScore() { + addScore(NEW_COMMENT_SCORE); + } + + public void subtractNewCommentScore() { + subtractScore(NEW_COMMENT_SCORE); + } + + public void addNewReactionScore() { + addScore(NEW_REACTION_SCORE); + } + + public void subtractNewReactionScore() { + subtractScore(NEW_REACTION_SCORE); + } + + public void addNewRealEmojiScore() { + addScore(NEW_REAL_EMOJI_SCORE); + } + + public void subtractNewRealEmojiScore() { + subtractScore(NEW_REAL_EMOJI_SCORE); + } + + private void addScore(int score) { + this.score += score; + } + + private void subtractScore(int score) { + this.score -= score; + } } diff --git a/family/src/main/java/com/oing/event/FamilyScoreEventListener.java b/family/src/main/java/com/oing/event/FamilyScoreEventListener.java new file mode 100644 index 00000000..68125dab --- /dev/null +++ b/family/src/main/java/com/oing/event/FamilyScoreEventListener.java @@ -0,0 +1,79 @@ +package com.oing.event; + +import com.oing.service.FamilyService; +import com.oing.service.MemberBridge; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class FamilyScoreEventListener { + + public final MemberBridge memberBridge; + public final FamilyService familyService; + + // TODO: 1. BEFORE_COMMIT, 2. Async 모두 작동 안함. 원인 파악 후 리펙토링 필요. +// @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) +// @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onMemberPostCreatedEvent(MemberPostCreatedEvent memberPostCreatedEvent) { + String familyId = memberBridge.getFamilyIdByMemberId(memberPostCreatedEvent.getMemberId()); + familyService.getFamilyById(familyId).addNewPostScore(); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onMemberPostDeletedEvent(MemberPostDeletedEvent memberPostDeletedEvent) { + String familyId = memberBridge.getFamilyIdByMemberId(memberPostDeletedEvent.getMemberId()); + familyService.getFamilyById(familyId).subtractNewPostScore(); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onMemberPostCommentCreatedEvent(MemberPostCommentCreatedEvent memberPostCommentCreatedEvent) { + String familyId = memberBridge.getFamilyIdByMemberId(memberPostCommentCreatedEvent.getMemberId()); + familyService.getFamilyById(familyId).addNewCommentScore(); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onMemberPostCommentDeletedEvent(MemberPostCommentDeletedEvent memberPostCommentDeletedEvent) { + String familyId = memberBridge.getFamilyIdByMemberId(memberPostCommentDeletedEvent.getMemberId()); + familyService.getFamilyById(familyId).subtractNewCommentScore(); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onMemberPostReactionCreatedEvent(MemberPostReactionCreatedEvent memberPostReactionCreatedEvent) { + String familyId = memberBridge.getFamilyIdByMemberId(memberPostReactionCreatedEvent.getMemberId()); + familyService.getFamilyById(familyId).addNewReactionScore(); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onMemberPostReactionDeletedEvent(MemberPostReactionDeletedEvent memberPostReactionDeletedEvent) { + String familyId = memberBridge.getFamilyIdByMemberId(memberPostReactionDeletedEvent.getMemberId()); + familyService.getFamilyById(familyId).subtractNewReactionScore(); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onMemberPostRealEmojiCreatedEvent(MemberPostRealEmojiCreatedEvent memberPostRealEmojiCreatedEvent) { + String familyId = memberBridge.getFamilyIdByMemberId(memberPostRealEmojiCreatedEvent.getMemberId()); + familyService.getFamilyById(familyId).addNewRealEmojiScore(); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onMemberPostRealEmojiDeletedEvent(MemberPostRealEmojiDeletedEvent memberPostRealEmojiDeletedEvent) { + String familyId = memberBridge.getFamilyIdByMemberId(memberPostRealEmojiDeletedEvent.getMemberId()); + familyService.getFamilyById(familyId).subtractNewRealEmojiScore(); + } + + +} diff --git a/family/src/main/java/com/oing/repository/FamilyRepository.java b/family/src/main/java/com/oing/repository/FamilyRepository.java index 5fd46935..e85c3077 100644 --- a/family/src/main/java/com/oing/repository/FamilyRepository.java +++ b/family/src/main/java/com/oing/repository/FamilyRepository.java @@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface FamilyRepository extends JpaRepository { + long countByScoreGreaterThanEqual(int familyScore); } diff --git a/family/src/main/java/com/oing/service/FamilyService.java b/family/src/main/java/com/oing/service/FamilyService.java index 8973ea63..937e0b3d 100644 --- a/family/src/main/java/com/oing/service/FamilyService.java +++ b/family/src/main/java/com/oing/service/FamilyService.java @@ -49,4 +49,17 @@ public Family getFamilyById(String familyId) { .findById(familyId) .orElseThrow(FamilyNotFoundException::new); } + + public int calculateFamilyTopPercentile(String familyId) { + long allFamiliesCount = familyRepository.count(); + int familyScore = getFamilyById(familyId).getScore(); + long familyRank = familyRepository.countByScoreGreaterThanEqual(familyScore); + + // handle divide by zero error + if (allFamiliesCount == 0) { + return 0; + } + + return (100 - (int) ((familyRank / (double) allFamiliesCount) * 100)); + } } diff --git a/gateway/src/main/java/com/oing/controller/CalendarController.java b/gateway/src/main/java/com/oing/controller/CalendarController.java index 01775086..99cf70c6 100644 --- a/gateway/src/main/java/com/oing/controller/CalendarController.java +++ b/gateway/src/main/java/com/oing/controller/CalendarController.java @@ -9,8 +9,7 @@ import com.oing.dto.response.CalendarResponse; import com.oing.dto.response.FamilyMonthlyStatisticsResponse; import com.oing.restapi.CalendarApi; -import com.oing.service.MemberPostService; -import com.oing.service.MemberService; +import com.oing.service.*; import com.oing.util.OptimizedImageUrlGenerator; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; @@ -18,7 +17,6 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.stream.IntStream; @@ -29,14 +27,26 @@ public class CalendarController implements CalendarApi { private final MemberService memberService; private final MemberPostService memberPostService; + private final FamilyService familyService; + private final MemberPostCommentService memberPostCommentService; + private final MemberPostReactionService memberPostReactionService; + private final MemberPostRealEmojiService memberPostRealEmojiService; private final TokenAuthenticationHolder tokenAuthenticationHolder; private final OptimizedImageUrlGenerator optimizedImageUrlGenerator; - private List getFamilyIds() { - String myId = tokenAuthenticationHolder.getUserId(); - return memberService.findFamilyMembersIdByMemberId(myId); + @Override + @Cacheable(value = "calendarCache", key = "#familyId.concat(':').concat(#yearMonth)", cacheManager = "monthlyCalendarCacheManager") + public ArrayResponse getMonthlyCalendar(String yearMonth, String familyId) { + if (yearMonth == null) yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); + + LocalDate startDate = LocalDate.parse(yearMonth + "-01"); // yyyy-MM-dd 패턴으로 파싱 + LocalDate endDate = startDate.plusMonths(1); + List familyMembersIds = memberService.findFamilyMembersIdsByFamilyId(familyId); + + List calendarResponses = getCalendarResponses(familyMembersIds, startDate, endDate); + return new ArrayResponse<>(calendarResponses); } private List mapPostToCalendar( @@ -70,26 +80,75 @@ private List getCalendarResponses(List familyIds, Loca return mapPostToCalendar(representativePosts, calendarDTOs, familyIds.size()); } + @Override - @Cacheable(value = "calendarCache", key = "#familyId.concat(':').concat(#yearMonth)", cacheManager = "monthlyCalendarCacheManager") - public ArrayResponse getMonthlyCalendar(String yearMonth, String familyId) { + public BannerResponse getBanner(String yearMonth, String familyId) { + /* 파라미터 정리 */ if (yearMonth == null) yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); - LocalDate startDate = LocalDate.parse(yearMonth + "-01"); // yyyy-MM-dd 패턴으로 파싱 LocalDate endDate = startDate.plusMonths(1); - List familyIds = getFamilyIds(); + List familyMembersIds = memberService.findFamilyMembersIdsByFamilyId(familyId); + + + /* 배너를 위한 필드 조회 */ + // 정적 필드 조회 + int familyTopPercentage = familyService.calculateFamilyTopPercentile(familyId); + int familyPostsCount = (int) memberPostService.countMemberPostsByMemberIdsBetween(familyMembersIds, startDate, endDate); + int familyInteractionCount = (int) memberPostCommentService.countMemberPostCommentsByMemberIdsBetween(familyMembersIds, startDate, endDate) + + (int) memberPostReactionService.countMemberPostReactionsByMemberIdsBetween(familyMembersIds, startDate, endDate) + + (int) memberPostRealEmojiService.countMemberPostRealEmojisByMemberIdsBetween(familyMembersIds, startDate, endDate); + + // 다이나믹 필드 계산 + int allFamilyMembersUploadedDays = 0; + int allFamilyMembersUploadedStreaks = 0; + boolean allFamilyMembersUploadedStreaked = true; + // 한 달 동안 '가족이 전부 올린 날'과 '가족이 전부 올린 날의 연속'을 계산하기 위해, 1일부터 마지막 날까지 순회한다. + while (startDate.isBefore(endDate)) { + long postsCount = memberPostService.countMemberPostsByMemberIdsBetween(familyMembersIds, startDate, startDate.plusDays(1)); + long familyMembersCount = memberService.countFamilyMembersByFamilyIdBefore(familyId, startDate.plusDays(1)); + + // 가족이 존재한 날만 계산한다. + if (familyMembersCount != 0) { + if (postsCount == familyMembersCount) { // 가족 전체가 업로드했다면 + allFamilyMembersUploadedDays++; + + if (allFamilyMembersUploadedStreaked) allFamilyMembersUploadedStreaks++; // 가족 전체 업로드가 연속되면, Streak + 1 + } else { // 가족 전체 업로드가 연속되지 못하면, Streak false + allFamilyMembersUploadedStreaked = false; + } + } + + startDate = startDate.plusDays(1); + } + + + /* 가족의 활성화 레벨을 계산 */ + // [ Level 1 ] 디폴트 + int familyLevel = 1; + // [ Level 1 기저 조건 ] 업로드 된 글이 없으면, 무조건 Level 1 + if (familyPostsCount == 0) familyLevel = 1; + // [ Level 4 ] 모두 업로드 20일 이상 or (업로드 사진 60개 이상 and 리액션 120개 이상) + else if (allFamilyMembersUploadedDays >= 20 || (familyPostsCount >= 60 && familyInteractionCount >= 120)) familyLevel = 4; + // [ Level 3 ] 이때까지 모두 업로드가 연속되면 OR (업로드 사진 10개이상 and 리액션 10개 이상) + else if (allFamilyMembersUploadedStreaked || (familyPostsCount >= 10 && familyInteractionCount >= 10)) familyLevel = 3; + // [ Level 2 ] 모두 업로드 한 날이 1일 이상 OR 업로드된 사진 2개 이상 + else if (allFamilyMembersUploadedDays >= 1 || familyPostsCount >= 2) familyLevel = 2; + + + /* 배너 이미지 결정 */ + BannerImageType bannerImageType; + if (familyLevel == 1) bannerImageType = BannerImageType.SKULL_FLAG; + else if (familyLevel == 2) bannerImageType = BannerImageType.ALONE_WALKING; + else if (familyLevel == 3) bannerImageType = BannerImageType.WE_ARE_FRIENDS; + else if (familyLevel == 4) bannerImageType = BannerImageType.JEWELRY_TREASURE; + else bannerImageType = BannerImageType.SKULL_FLAG; // 예외 처리 - List calendarResponses = getCalendarResponses(familyIds, startDate, endDate); - return new ArrayResponse<>(calendarResponses); - } - @Override - public BannerResponse getBanner(String yearMonth) { return new BannerResponse( - new Random().nextInt(0, 101), - new Random().nextInt(0, 28), - new Random().nextInt(1, 5), - BannerImageType.values()[new Random().nextInt(BannerImageType.values().length)] + familyTopPercentage, + allFamilyMembersUploadedDays, + familyLevel, + bannerImageType ); } diff --git a/gateway/src/main/java/com/oing/domain/BannerImageType.java b/gateway/src/main/java/com/oing/domain/BannerImageType.java index 38787d89..02f01be3 100644 --- a/gateway/src/main/java/com/oing/domain/BannerImageType.java +++ b/gateway/src/main/java/com/oing/domain/BannerImageType.java @@ -5,7 +5,7 @@ @RequiredArgsConstructor public enum BannerImageType { SKULL_FLAG("SKULL_FLAG"), - ALONE_WALING("ALONE_WALING"), + ALONE_WALKING("ALONE_WALKING"), WE_ARE_FRIENDS("WE_ARE_FRIENDS"), JEWELRY_TREASURE("JEWELRY_TREASURE"), ; diff --git a/gateway/src/main/java/com/oing/restapi/CalendarApi.java b/gateway/src/main/java/com/oing/restapi/CalendarApi.java index b654c901..35029e04 100644 --- a/gateway/src/main/java/com/oing/restapi/CalendarApi.java +++ b/gateway/src/main/java/com/oing/restapi/CalendarApi.java @@ -43,7 +43,11 @@ ArrayResponse getMonthlyCalendar( BannerResponse getBanner( @RequestParam(required = false) @Parameter(description = "조회할 년월", example = "2021-12") - String yearMonth + String yearMonth, + + @FamilyId + @Parameter(hidden = true) + String familyId ); @Operation(summary = "캘린더 통계 조회", description = "캘린더의 통계를 조회합니다.") diff --git a/gateway/src/main/resources/db/migration/V202401271853__add_familyScore_column.sql b/gateway/src/main/resources/db/migration/V202401271853__add_familyScore_column.sql new file mode 100644 index 00000000..a97b92bd --- /dev/null +++ b/gateway/src/main/resources/db/migration/V202401271853__add_familyScore_column.sql @@ -0,0 +1 @@ +ALTER TABLE `family` ADD COLUMN (`score` INTEGER NOT NULL DEFAULT 0); diff --git a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java index 010782c0..d8a6915b 100644 --- a/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java +++ b/gateway/src/test/java/com/oing/controller/CalendarControllerTest.java @@ -111,8 +111,7 @@ class CalendarControllerTest { new MemberPostDailyCalendarDTO(2L), new MemberPostDailyCalendarDTO(1L) ); - when(tokenAuthenticationHolder.getUserId()).thenReturn(testMember1.getId()); - when(memberService.findFamilyMembersIdByMemberId(testMember1.getId())).thenReturn(familyIds); + when(memberService.findFamilyMembersIdsByFamilyId("testFamily")).thenReturn(familyIds); when(memberPostService.findLatestPostOfEveryday(familyIds, startDate, endDate)).thenReturn(representativePosts); when(memberPostService.findPostDailyCalendarDTOs(familyIds, startDate, endDate)).thenReturn(calendarDTOs); diff --git a/gateway/src/test/java/com/oing/controller/WidgetControllerTest.java b/gateway/src/test/java/com/oing/controller/WidgetControllerTest.java index 81a1d5c3..12077801 100644 --- a/gateway/src/test/java/com/oing/controller/WidgetControllerTest.java +++ b/gateway/src/test/java/com/oing/controller/WidgetControllerTest.java @@ -17,6 +17,7 @@ import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +46,8 @@ class WidgetControllerTest { LocalDate.of(1999, 10, 18), "testMember1", "profile.com/1", - "1" + "1", + LocalDateTime.now() ); private final Member testMember2 = new Member( @@ -54,7 +56,8 @@ class WidgetControllerTest { LocalDate.of(1999, 10, 18), "testMember2", "profile.com/2", - "2" + "2", + LocalDateTime.now() ); private final MemberPost testPost1 = new MemberPost( diff --git a/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java b/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java index 9bee2e65..c5cf8419 100644 --- a/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java @@ -78,7 +78,7 @@ void setUp() throws Exception { "testUser1", "testUser1", LocalDate.of(1999, 10, 18), - "storage.com/images/1" + "https://storage.com/bucket/images/1" ) ); TEST_MEMBER1_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER1.getId()).accessToken(); @@ -89,7 +89,7 @@ void setUp() throws Exception { "testUser2", "testUser2", LocalDate.of(2000, 10, 18), - "storage.com/images/2" + "https://storage.com/bucket/images/2" ) ); TEST_MEMBER2_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER2.getId()).accessToken(); @@ -116,7 +116,7 @@ void setUp() throws Exception { "testUser3", "testUser3", LocalDate.of(2001, 10, 18), - "storage.com/images/3" + "https://storage.com/bucket/images/3" ) ); TEST_MEMBER3_TOKEN = tokenGenerator.generateTokenPair(TEST_MEMBER3.getId()).accessToken(); @@ -131,21 +131,21 @@ void setUp() throws Exception { MemberPost testPost1 = new MemberPost( "testPost1", TEST_MEMBER1.getId(), - "storage.com/images/1", + "https://storage.com/bucket/images/1", "1", "testPos1" ); MemberPost testPost2 = new MemberPost( "testPost2", TEST_MEMBER2.getId(), - "storage.com/images/2", + "https://storage.com/bucket/images/2", "2", "testPos2" ); MemberPost testPost3 = new MemberPost( "testPost3", TEST_MEMBER3.getId(), - "storage.com/images/3", + "https://storage.com/bucket/images/3", "3", "testPos3" ); @@ -174,21 +174,21 @@ void setUp() throws Exception { MemberPost testPost1 = new MemberPost( "testPost1", TEST_MEMBER1.getId(), - "storage.com/images/1", + "https://storage.com/bucket/images/1", "1", "testPos1" ); MemberPost testPost2 = new MemberPost( "testPost2", TEST_MEMBER2.getId(), - "storage.com/images/2", + "https://storage.com/bucket/images/2", "2", "testPos2" ); MemberPost testPost3 = new MemberPost( "testPost3", TEST_MEMBER3.getId(), - "storage.com/images/3", + "https://storage.com/bucket/images/3", "3", "testPos3" ); diff --git a/member/src/main/java/com/oing/domain/Member.java b/member/src/main/java/com/oing/domain/Member.java index 61d0d193..6728dffd 100644 --- a/member/src/main/java/com/oing/domain/Member.java +++ b/member/src/main/java/com/oing/domain/Member.java @@ -66,6 +66,7 @@ public void deleteMemberInfo() { public void setFamilyId(String familyId) { this.familyId = familyId; + if(familyId == null) { this.familyJoinAt = null; } else { diff --git a/member/src/main/java/com/oing/repository/MemberRepository.java b/member/src/main/java/com/oing/repository/MemberRepository.java index 97cedcd7..6ea9a76a 100644 --- a/member/src/main/java/com/oing/repository/MemberRepository.java +++ b/member/src/main/java/com/oing/repository/MemberRepository.java @@ -5,6 +5,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; /** @@ -17,4 +19,6 @@ public interface MemberRepository extends JpaRepository { List findAllByFamilyId(String familyId); Page findAllByFamilyIdAndDeletedAtIsNull(String familyId, PageRequest pageRequest); + + long countByFamilyIdAndFamilyJoinAtBefore(String familyId, LocalDateTime dateTime); } diff --git a/member/src/main/java/com/oing/service/MemberService.java b/member/src/main/java/com/oing/service/MemberService.java index ba214d75..2925e879 100644 --- a/member/src/main/java/com/oing/service/MemberService.java +++ b/member/src/main/java/com/oing/service/MemberService.java @@ -19,9 +19,9 @@ import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import java.time.LocalDate; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @RequiredArgsConstructor @Service @@ -89,6 +89,13 @@ public List findFamilyMembersIdByMemberId(String memberId) { .toList(); } + public List findFamilyMembersIdsByFamilyId(String familyId) { + return memberRepository.findAllByFamilyId(familyId) + .stream() + .map(Member::getId) + .toList(); + } + @Transactional public Page findFamilyMembersProfilesByFamilyId( String familyId, int page, int size @@ -101,10 +108,14 @@ public Page findFamilyMembersProfilesByFamilyId( return new PageImpl<>(familyMemberProfiles, memberPage.getPageable(), memberPage.getTotalElements()); } + public long countFamilyMembersByFamilyIdBefore(String familyId, LocalDate date) { + return memberRepository.countByFamilyIdAndFamilyJoinAtBefore(familyId, date.atStartOfDay()); + } + private List createFamilyMemberProfiles(List members) { return members.stream() .map(FamilyMemberProfileResponse::of) - .collect(Collectors.toList()); + .toList(); } public void deleteAllSocialMembersByMember(String memberId) { diff --git a/post/src/main/java/com/oing/domain/MemberPost.java b/post/src/main/java/com/oing/domain/MemberPost.java index bccdac6d..d1ccda36 100644 --- a/post/src/main/java/com/oing/domain/MemberPost.java +++ b/post/src/main/java/com/oing/domain/MemberPost.java @@ -16,6 +16,7 @@ @Index(name = "member_post_idx1", columnList = "member_id") }) @Entity(name = "member_post") +@EntityListeners(MemberPostEntityListener.class) public class MemberPost extends BaseAuditEntity { @Id @Column(name = "post_id", columnDefinition = "CHAR(26)", nullable = false) diff --git a/post/src/main/java/com/oing/domain/MemberPostComment.java b/post/src/main/java/com/oing/domain/MemberPostComment.java index a5d998e7..6cd37e77 100644 --- a/post/src/main/java/com/oing/domain/MemberPostComment.java +++ b/post/src/main/java/com/oing/domain/MemberPostComment.java @@ -13,6 +13,7 @@ @Index(name = "member_post_comment_idx2", columnList = "member_id") }) @Entity(name = "member_post_comment") +@EntityListeners(MemberPostCommentEntityListener.class) public class MemberPostComment extends BaseAuditEntity { @Id @Column(name = "comment_id", columnDefinition = "CHAR(26)", nullable = false) diff --git a/post/src/main/java/com/oing/domain/MemberPostCommentEntityListener.java b/post/src/main/java/com/oing/domain/MemberPostCommentEntityListener.java new file mode 100644 index 00000000..a19e00ce --- /dev/null +++ b/post/src/main/java/com/oing/domain/MemberPostCommentEntityListener.java @@ -0,0 +1,32 @@ +package com.oing.domain; + +import com.oing.event.MemberPostCommentCreatedEvent; +import com.oing.event.MemberPostCommentDeletedEvent; +import jakarta.persistence.PostPersist; +import jakarta.persistence.PostRemove; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@NoArgsConstructor +public class MemberPostCommentEntityListener { + + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + public MemberPostCommentEntityListener(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @PostPersist + public void onPostPersist(MemberPostComment memberPostComment) { + applicationEventPublisher.publishEvent(new MemberPostCommentCreatedEvent(memberPostComment, memberPostComment.getMemberId())); + } + + @PostRemove + public void onPostRemove(MemberPostComment memberPostComment) { + applicationEventPublisher.publishEvent(new MemberPostCommentDeletedEvent(memberPostComment, memberPostComment.getMemberId())); + } +} diff --git a/post/src/main/java/com/oing/domain/MemberPostEntityListener.java b/post/src/main/java/com/oing/domain/MemberPostEntityListener.java new file mode 100644 index 00000000..78e76ae3 --- /dev/null +++ b/post/src/main/java/com/oing/domain/MemberPostEntityListener.java @@ -0,0 +1,32 @@ +package com.oing.domain; + +import com.oing.event.MemberPostCreatedEvent; +import com.oing.event.MemberPostDeletedEvent; +import jakarta.persistence.PostPersist; +import jakarta.persistence.PostRemove; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@NoArgsConstructor +public class MemberPostEntityListener { + + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + public MemberPostEntityListener(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @PostPersist + public void onPostPersist(MemberPost memberPost) { + applicationEventPublisher.publishEvent(new MemberPostCreatedEvent(memberPost, memberPost.getMemberId())); + } + + @PostRemove + public void onPostRemove(MemberPost memberPost) { + applicationEventPublisher.publishEvent(new MemberPostDeletedEvent(memberPost, memberPost.getMemberId())); + } +} diff --git a/post/src/main/java/com/oing/domain/MemberPostReaction.java b/post/src/main/java/com/oing/domain/MemberPostReaction.java index 7e1ee92b..e879b08d 100644 --- a/post/src/main/java/com/oing/domain/MemberPostReaction.java +++ b/post/src/main/java/com/oing/domain/MemberPostReaction.java @@ -13,6 +13,7 @@ @Index(name = "member_post_reaction_idx2", columnList = "member_id") }) @Entity(name = "member_post_reaction") +@EntityListeners(MemberPostReactionEntityListener.class) public class MemberPostReaction extends BaseEntity { @Id @Column(name = "reaction_id", columnDefinition = "CHAR(26)", nullable = false) diff --git a/post/src/main/java/com/oing/domain/MemberPostReactionEntityListener.java b/post/src/main/java/com/oing/domain/MemberPostReactionEntityListener.java new file mode 100644 index 00000000..9c468a2c --- /dev/null +++ b/post/src/main/java/com/oing/domain/MemberPostReactionEntityListener.java @@ -0,0 +1,32 @@ +package com.oing.domain; + +import com.oing.event.MemberPostReactionCreatedEvent; +import com.oing.event.MemberPostReactionDeletedEvent; +import jakarta.persistence.PostPersist; +import jakarta.persistence.PostRemove; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@NoArgsConstructor +public class MemberPostReactionEntityListener { + + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + public MemberPostReactionEntityListener(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @PostPersist + public void onPostPersist(MemberPostReaction memberPostReaction) { + applicationEventPublisher.publishEvent(new MemberPostReactionCreatedEvent(memberPostReaction, memberPostReaction.getMemberId())); + } + + @PostRemove + public void onPostRemove(MemberPostReaction memberPostReaction) { + applicationEventPublisher.publishEvent(new MemberPostReactionDeletedEvent(memberPostReaction, memberPostReaction.getMemberId())); + } +} diff --git a/post/src/main/java/com/oing/domain/MemberPostRealEmoji.java b/post/src/main/java/com/oing/domain/MemberPostRealEmoji.java index 1ac0068d..3c1a8b23 100644 --- a/post/src/main/java/com/oing/domain/MemberPostRealEmoji.java +++ b/post/src/main/java/com/oing/domain/MemberPostRealEmoji.java @@ -12,6 +12,7 @@ @Index(name = "member_post_real_emoji_idx2", columnList = "member_id") }) @Entity(name = "member_post_real_emoji") +@EntityListeners(MemberPostRealEmojiEntityListener.class) public class MemberPostRealEmoji extends BaseEntity { @Id diff --git a/post/src/main/java/com/oing/domain/MemberPostRealEmojiEntityListener.java b/post/src/main/java/com/oing/domain/MemberPostRealEmojiEntityListener.java new file mode 100644 index 00000000..6e13c7f9 --- /dev/null +++ b/post/src/main/java/com/oing/domain/MemberPostRealEmojiEntityListener.java @@ -0,0 +1,32 @@ +package com.oing.domain; + +import com.oing.event.MemberPostCreatedEvent; +import com.oing.event.MemberPostDeletedEvent; +import jakarta.persistence.PostPersist; +import jakarta.persistence.PostRemove; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@NoArgsConstructor +public class MemberPostRealEmojiEntityListener { + + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + public MemberPostRealEmojiEntityListener(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @PostPersist + public void onPostPersist(MemberPostRealEmoji memberPostRealEmoji) { + applicationEventPublisher.publishEvent(new MemberPostCreatedEvent(memberPostRealEmoji, memberPostRealEmoji.getPost().getMemberId())); + } + + @PostRemove + public void onPostRemove(MemberPostRealEmoji memberPostRealEmoji) { + applicationEventPublisher.publishEvent(new MemberPostDeletedEvent(memberPostRealEmoji, memberPostRealEmoji.getPost().getMemberId())); + } +} diff --git a/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java b/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java index dd40360e..99d7a33a 100644 --- a/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java @@ -3,6 +3,11 @@ import com.oing.domain.MemberPostComment; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDateTime; +import java.util.List; + public interface MemberPostCommentRepository extends JpaRepository, MemberPostCommentRepositoryCustom { void deleteAllByPostId(String memberPostId); + + long countByMemberIdInAndCreatedAtBetween(List memberIds, LocalDateTime startDateTime, LocalDateTime endDateTime); } diff --git a/post/src/main/java/com/oing/repository/MemberPostReactionRepository.java b/post/src/main/java/com/oing/repository/MemberPostReactionRepository.java index 8cab8833..422fc47c 100644 --- a/post/src/main/java/com/oing/repository/MemberPostReactionRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostReactionRepository.java @@ -5,6 +5,7 @@ import com.oing.domain.MemberPostReaction; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -16,4 +17,6 @@ public interface MemberPostReactionRepository extends JpaRepository findAllByPostId(String postId); void deleteAllByPostId(String memberPostId); + + long countByMemberIdInAndCreatedAtBetween(List memberIds, LocalDateTime startDateTime, LocalDateTime endDateTime); } diff --git a/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java index a9e0987f..cfefa6ae 100644 --- a/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java @@ -5,10 +5,14 @@ import com.oing.domain.MemberRealEmoji; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; public interface MemberPostRealEmojiRepository extends JpaRepository { boolean existsByPostAndMemberIdAndRealEmoji(MemberPost post, String memberId, MemberRealEmoji emoji); Optional findByRealEmojiIdAndMemberIdAndPostId(String realEmojiId, String memberId, String postId); + + long countByMemberIdInAndCreatedAtBetween(List memberIds, LocalDateTime startDateTime, LocalDateTime endDateTime); } diff --git a/post/src/main/java/com/oing/repository/MemberPostRepository.java b/post/src/main/java/com/oing/repository/MemberPostRepository.java index ff654085..d3552644 100644 --- a/post/src/main/java/com/oing/repository/MemberPostRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostRepository.java @@ -1,7 +1,13 @@ package com.oing.repository; import com.oing.domain.MemberPost; +import org.springframework.cglib.core.Local; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDateTime; +import java.util.List; + public interface MemberPostRepository extends JpaRepository, MemberPostRepositoryCustom { + + long countByMemberIdInAndCreatedAtBetween(List memberIds, LocalDateTime startDateTime, LocalDateTime endDateTime); } diff --git a/post/src/main/java/com/oing/service/MemberPostCommentService.java b/post/src/main/java/com/oing/service/MemberPostCommentService.java index 054713de..a3e2bfc3 100644 --- a/post/src/main/java/com/oing/service/MemberPostCommentService.java +++ b/post/src/main/java/com/oing/service/MemberPostCommentService.java @@ -12,6 +12,9 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import java.time.LocalDate; +import java.util.List; + @RequiredArgsConstructor @Service public class MemberPostCommentService { @@ -72,6 +75,18 @@ public PaginationDTO searchPostComments(int page, int size, S ); } + /** + * 특정 기간 동안 특정 멤버가 올린 댓글의 갯수를 반환한다. + * + * @param memberIds 조회 대상 멤버들의 ID + * @param inclusiveStartDate 조회 시작 날짜 + * @param exclusiveEndDate 조회 종료 날짜 + * @return 조회 대상인 댓글의 갯수 + */ + public long countMemberPostCommentsByMemberIdsBetween(List memberIds, LocalDate inclusiveStartDate, LocalDate exclusiveEndDate) { + return memberPostCommentRepository.countByMemberIdInAndCreatedAtBetween(memberIds, inclusiveStartDate.atStartOfDay(), exclusiveEndDate.atStartOfDay()); + } + @EventListener public void deleteAllWhenPostDelete(DeleteMemberPostEvent event) { MemberPost post = event.memberPost(); diff --git a/post/src/main/java/com/oing/service/MemberPostReactionService.java b/post/src/main/java/com/oing/service/MemberPostReactionService.java index 268f30b3..d139108e 100644 --- a/post/src/main/java/com/oing/service/MemberPostReactionService.java +++ b/post/src/main/java/com/oing/service/MemberPostReactionService.java @@ -10,6 +10,7 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import java.time.LocalDate; import java.util.List; @Service @@ -34,14 +35,26 @@ public MemberPostReaction findReaction(MemberPost post, String memberId, Emoji e .orElseThrow(EmojiNotFoundException::new); } - public void deletePostReaction(MemberPostReaction reaction) { - memberPostReactionRepository.deleteById(reaction.getId()); - } - public List getMemberPostReactionsByPostId(String postId) { return memberPostReactionRepository.findAllByPostId(postId); } + /** + * 특정 기간 동안 특정 멤버가 올린 반응의 갯수를 반환한다. + * + * @param memberIds 조회 대상 멤버들의 ID + * @param inclusiveStartDate 조회 시작 날짜 + * @param exclusiveEndDate 조회 종료 날짜 + * @return 조회 대상인 반응의 갯수 + */ + public long countMemberPostReactionsByMemberIdsBetween(List memberIds, LocalDate inclusiveStartDate, LocalDate exclusiveEndDate) { + return memberPostReactionRepository.countByMemberIdInAndCreatedAtBetween(memberIds, inclusiveStartDate.atStartOfDay(), exclusiveEndDate.atStartOfDay()); + } + + public void deletePostReaction(MemberPostReaction reaction) { + memberPostReactionRepository.deleteById(reaction.getId()); + } + @EventListener public void deleteAllWhenPostDelete(DeleteMemberPostEvent event) { MemberPost post = event.memberPost(); diff --git a/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java b/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java index 8163a817..7323452f 100644 --- a/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java +++ b/post/src/main/java/com/oing/service/MemberPostRealEmojiService.java @@ -8,6 +8,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDate; +import java.util.List; + @Service @RequiredArgsConstructor public class MemberPostRealEmojiService { @@ -46,6 +49,18 @@ public MemberPostRealEmoji getMemberPostRealEmojiByRealEmojiIdAndMemberIdAndPost .orElseThrow(RegisteredRealEmojiNotFoundException::new); } + /** + * 특정 기간 동안 특정 멤버가 올린 리얼 이모지의 갯수를 반환한다. + * + * @param memberIds 조회 대상 멤버들의 ID + * @param inclusiveStartDate 조회 시작 날짜 + * @param exclusiveEndDate 조회 종료 날짜 + * @return 조회 대상인 리얼 이모지의 갯수 + */ + public long countMemberPostRealEmojisByMemberIdsBetween(List memberIds, LocalDate inclusiveStartDate, LocalDate exclusiveEndDate) { + return memberPostRealEmojiRepository.countByMemberIdInAndCreatedAtBetween(memberIds, inclusiveStartDate.atStartOfDay(), exclusiveEndDate.atStartOfDay()); + } + /** * 게시물에 등록된 리얼 이모지를 삭제 * @param postRealEmoji 리얼 이모지 diff --git a/post/src/main/java/com/oing/service/MemberPostService.java b/post/src/main/java/com/oing/service/MemberPostService.java index ef81a161..917795e0 100644 --- a/post/src/main/java/com/oing/service/MemberPostService.java +++ b/post/src/main/java/com/oing/service/MemberPostService.java @@ -22,6 +22,12 @@ public class MemberPostService { private final MemberPostRepository memberPostRepository; private final ApplicationEventPublisher applicationEventPublisher; + + public MemberPost save(MemberPost post) { + return memberPostRepository.save(post); + } + + /** * 멤버들이 범위 날짜 안에 올린 대표 게시물들을 가져온다. * (대표 게시글의 기준은 당일 가장 늦게 올라온 게시글) @@ -48,7 +54,6 @@ public List findPostDailyCalendarDTOs(List m return memberPostRepository.findPostDailyCalendarDTOs(memberIds, inclusiveStartDate.atStartOfDay(), exclusiveEndDate.atStartOfDay()); } - public MemberPost findMemberPostById(String postId) { return memberPostRepository .findById(postId) @@ -67,15 +72,6 @@ public boolean hasUserCreatedPostToday(String memberId, LocalDate today) { return memberPostRepository.existsByMemberIdAndCreatedAt(memberId, today); } - /** - * 멤버가 오늘 작성한 게시물을 저장한다. - * - * @param post 저장할 MemberPost 객체 - */ - public MemberPost save(MemberPost post) { - return memberPostRepository.save(post); - } - @Transactional public MemberPost getMemberPostById(String postId) { return memberPostRepository.findById(postId).orElseThrow(PostNotFoundException::new); @@ -91,6 +87,18 @@ public PaginationDTO searchMemberPost(int page, int size, LocalDate ); } + /** + * 특정 기간 동안 특정 멤버가 올린 게시글의 갯수를 반환한다. + * + * @param memberIds 조회 대상 멤버들의 ID + * @param inclusiveStartDate 조회 시작 날짜 + * @param exclusiveEndDate 조회 종료 날짜 + * @return 조회 대상인 게시글의 갯수 + */ + public long countMemberPostsByMemberIdsBetween(List memberIds, LocalDate inclusiveStartDate, LocalDate exclusiveEndDate) { + return memberPostRepository.countByMemberIdInAndCreatedAtBetween(memberIds, inclusiveStartDate.atStartOfDay(), exclusiveEndDate.atStartOfDay()); + } + @Transactional public void deleteMemberPostById(String postId) { MemberPost memberPost = memberPostRepository.findById(postId).orElseThrow(PostNotFoundException::new); From 1d567bab39f4cd83df2120056d7254daaaf9c4db Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Wed, 31 Jan 2024 20:35:01 +0900 Subject: [PATCH 45/49] =?UTF-8?q?[OING-186]=20fix:=20=EC=8D=B8=EB=84=A4?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=95=95=EC=B6=95=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EC=97=90=EC=84=9C=20=EC=82=AC=EC=A7=84=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=ED=9A=8C=EC=A0=84=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=20(#133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Change thumbnail optimzing query as auto-rotate false * feat: delete data sql --------- Co-authored-by: 송영민 --- .../java/com/oing/config/support/OptimizedImageUrlProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java b/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java index 31881e59..d4a5eb35 100644 --- a/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java +++ b/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java @@ -17,7 +17,7 @@ @Component public class OptimizedImageUrlProvider implements OptimizedImageUrlGenerator { - private static final String THUMBNAIL_OPTIMIZER_QUERY_STRING = "?type=f&w=96&h=96&quality=70&align=4&faceopt=false&anilimit=1"; + private static final String THUMBNAIL_OPTIMIZER_QUERY_STRING = "type=f&w=96&h=96&quality=70&autorotate=false&faceopt=false"; private static final String KB_IMAGE_OPTIMIZER_QUERY_STRING = "?type=f&w=480&h=480&faceopt=false&quality=50&autorotate=false"; @Value("${cloud.ncp.image-optimizer-cdn}") From 4c79f902a4091e05baefaea41dec1040f92c279c Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Wed, 31 Jan 2024 22:16:42 +0900 Subject: [PATCH 46/49] =?UTF-8?q?[OING-184]=20chore:=20MicroMeter=20Promet?= =?UTF-8?q?heus=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=B6=94=EA=B0=80=20(#136?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Add micrometer prometheus and configs * chore: Remove metrics endpoints from application.yaml for actuaor * fix: Fix broken CalendarApiTest by making optimizing query public --- build.gradle | 1 + .../config/support/OptimizedImageUrlProvider.java | 4 ++-- gateway/src/main/resources/application-test.yaml | 2 -- gateway/src/main/resources/application.yaml | 6 ++++++ .../test/java/com/oing/restapi/CalendarApiTest.java | 11 +++++------ .../src/test/java/com/oing/restapi/WidgetApiTest.java | 11 +++++------ 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index ff83a63c..413ee57b 100644 --- a/build.gradle +++ b/build.gradle @@ -112,6 +112,7 @@ subprojects { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java b/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java index d4a5eb35..4b8fd813 100644 --- a/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java +++ b/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java @@ -17,8 +17,8 @@ @Component public class OptimizedImageUrlProvider implements OptimizedImageUrlGenerator { - private static final String THUMBNAIL_OPTIMIZER_QUERY_STRING = "type=f&w=96&h=96&quality=70&autorotate=false&faceopt=false"; - private static final String KB_IMAGE_OPTIMIZER_QUERY_STRING = "?type=f&w=480&h=480&faceopt=false&quality=50&autorotate=false"; + public static final String THUMBNAIL_OPTIMIZER_QUERY_STRING = "type=f&w=96&h=96&quality=70&autorotate=false&faceopt=false"; + public static final String KB_IMAGE_OPTIMIZER_QUERY_STRING = "?type=f&w=480&h=480&faceopt=false&quality=50&autorotate=false"; @Value("${cloud.ncp.image-optimizer-cdn}") private String imageOptimizerCdnUrl; diff --git a/gateway/src/main/resources/application-test.yaml b/gateway/src/main/resources/application-test.yaml index 5849a0e9..c92febf9 100644 --- a/gateway/src/main/resources/application-test.yaml +++ b/gateway/src/main/resources/application-test.yaml @@ -56,5 +56,3 @@ cloud: storage: bucket: bucket image-optimizer-cdn: https://cdn.com - thumbnail-optimizer-query: ?type=f&w=96&h=96&quality=70&align=4&faceopt=false&anilimit=1 - kb-optimizer-query: ?type=f&w=480&h=480&faceopt=false&quality=50&autorotate=false diff --git a/gateway/src/main/resources/application.yaml b/gateway/src/main/resources/application.yaml index 8a35d671..391a5f28 100644 --- a/gateway/src/main/resources/application.yaml +++ b/gateway/src/main/resources/application.yaml @@ -106,3 +106,9 @@ springdoc: display-request-duration: true operations-sorter: alpha tags-sorter: alpha + +management: + endpoints: + web: + exposure: + include: health,info,prometheus \ No newline at end of file diff --git a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java index 236f225e..493b5d76 100644 --- a/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java @@ -1,6 +1,7 @@ package com.oing.restapi; import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.config.support.OptimizedImageUrlProvider; import com.oing.domain.CreateNewUserDTO; import com.oing.domain.SocialLoginProvider; import com.oing.dto.request.JoinFamilyRequest; @@ -65,8 +66,6 @@ class CalendarApiTest { @Value("${cloud.ncp.image-optimizer-cdn}") private String imageOptimizerCdn; - @Value("${cloud.ncp.thumbnail-optimizer-query}") - private String thumbnailOptimizerQuery; @BeforeEach @@ -160,19 +159,19 @@ void setUp() { .andExpect(status().isOk()) .andExpect(jsonPath("$.results[0].date").value("2023-11-01")) .andExpect(jsonPath("$.results[0].representativePostId").value("2")) - .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/2" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[0].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/2" + OptimizedImageUrlProvider.THUMBNAIL_OPTIMIZER_QUERY_STRING)) .andExpect(jsonPath("$.results[0].allFamilyMembersUploaded").value(true)) .andExpect(jsonPath("$.results[1].date").value("2023-11-02")) .andExpect(jsonPath("$.results[1].representativePostId").value("4")) - .andExpect(jsonPath("$.results[1].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/4" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[1].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/4" + OptimizedImageUrlProvider.THUMBNAIL_OPTIMIZER_QUERY_STRING)) .andExpect(jsonPath("$.results[1].allFamilyMembersUploaded").value(false)) .andExpect(jsonPath("$.results[2].date").value("2023-11-29")) .andExpect(jsonPath("$.results[2].representativePostId").value("6")) - .andExpect(jsonPath("$.results[2].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/6" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[2].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/6" + OptimizedImageUrlProvider.THUMBNAIL_OPTIMIZER_QUERY_STRING)) .andExpect(jsonPath("$.results[2].allFamilyMembersUploaded").value(true)) .andExpect(jsonPath("$.results[3].date").value("2023-11-30")) .andExpect(jsonPath("$.results[3].representativePostId").value("8")) - .andExpect(jsonPath("$.results[3].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/8" + thumbnailOptimizerQuery)) + .andExpect(jsonPath("$.results[3].representativeThumbnailUrl").value(imageOptimizerCdn + "/images/8" + OptimizedImageUrlProvider.THUMBNAIL_OPTIMIZER_QUERY_STRING)) .andExpect(jsonPath("$.results[3].allFamilyMembersUploaded").value(false)); } diff --git a/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java b/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java index c5cf8419..e790467c 100644 --- a/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/WidgetApiTest.java @@ -1,6 +1,7 @@ package com.oing.restapi; import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.config.support.OptimizedImageUrlProvider; import com.oing.domain.CreateNewUserDTO; import com.oing.domain.Member; import com.oing.domain.MemberPost; @@ -66,8 +67,6 @@ class WidgetApiTest { @Value("${cloud.ncp.image-optimizer-cdn}") private String imageOptimizerCdn; - @Value("${cloud.ncp.kb-optimizer-query}") - private String kbOptimizerQuery; @BeforeEach @@ -161,8 +160,8 @@ void setUp() throws Exception { ) .andExpect(status().isOk()) .andExpect(jsonPath("$.authorName").value(TEST_MEMBER2.getName())) - .andExpect(jsonPath("$.authorProfileImageUrl").value(imageOptimizerCdn + "/images/2" + kbOptimizerQuery)) - .andExpect(jsonPath("$.postImageUrl").value(imageOptimizerCdn + "/images/2" + kbOptimizerQuery)) + .andExpect(jsonPath("$.authorProfileImageUrl").value(imageOptimizerCdn + "/images/2" + OptimizedImageUrlProvider.KB_IMAGE_OPTIMIZER_QUERY_STRING)) + .andExpect(jsonPath("$.postImageUrl").value(imageOptimizerCdn + "/images/2" + OptimizedImageUrlProvider.KB_IMAGE_OPTIMIZER_QUERY_STRING)) .andExpect(jsonPath("$.postContent").value(testPost2.getContent())); } @@ -203,8 +202,8 @@ void setUp() throws Exception { ) .andExpect(status().isOk()) .andExpect(jsonPath("$.authorName").value(TEST_MEMBER2.getName())) - .andExpect(jsonPath("$.authorProfileImageUrl").value(imageOptimizerCdn + "/images/2" + kbOptimizerQuery)) - .andExpect(jsonPath("$.postImageUrl").value(imageOptimizerCdn + "/images/2" + kbOptimizerQuery)) + .andExpect(jsonPath("$.authorProfileImageUrl").value(imageOptimizerCdn + "/images/2" + OptimizedImageUrlProvider.KB_IMAGE_OPTIMIZER_QUERY_STRING)) + .andExpect(jsonPath("$.postImageUrl").value(imageOptimizerCdn + "/images/2" + OptimizedImageUrlProvider.KB_IMAGE_OPTIMIZER_QUERY_STRING)) .andExpect(jsonPath("$.postContent").value(testPost2.getContent())); } From 7a18285d9570eb937a7e7da08b4257e4a621dcd0 Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Wed, 31 Jan 2024 22:56:52 +0900 Subject: [PATCH 47/49] chore: Append actuator endpoints configs of application.yml into dev and prod profiles (#137) --- gateway/src/main/resources/application-dev.yaml | 6 ++++++ gateway/src/main/resources/application-prod.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/gateway/src/main/resources/application-dev.yaml b/gateway/src/main/resources/application-dev.yaml index dfa4535b..4a6b7f1c 100644 --- a/gateway/src/main/resources/application-dev.yaml +++ b/gateway/src/main/resources/application-dev.yaml @@ -33,3 +33,9 @@ spring: redis: host: ${REDIS_HOST} port: ${REDIS_PORT} + +management: + endpoints: + web: + exposure: + include: health,info,prometheus \ No newline at end of file diff --git a/gateway/src/main/resources/application-prod.yaml b/gateway/src/main/resources/application-prod.yaml index 009cb6c2..0025404a 100644 --- a/gateway/src/main/resources/application-prod.yaml +++ b/gateway/src/main/resources/application-prod.yaml @@ -41,3 +41,9 @@ logging: springdoc: api-docs: enabled: false + +management: + endpoints: + web: + exposure: + include: health,info,prometheus \ No newline at end of file From 249dfd4430e8c1e531d2f7da23f39d3517d75b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Wed, 31 Jan 2024 23:39:16 +0900 Subject: [PATCH 48/49] hotfix: ignore flyway out of order --- gateway/src/main/resources/application-dev.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gateway/src/main/resources/application-dev.yaml b/gateway/src/main/resources/application-dev.yaml index 4a6b7f1c..58158761 100644 --- a/gateway/src/main/resources/application-dev.yaml +++ b/gateway/src/main/resources/application-dev.yaml @@ -26,6 +26,7 @@ spring: flyway: enabled: true baseline-on-migrate: true + out-of-order: true h2: console: enabled: false @@ -38,4 +39,4 @@ management: endpoints: web: exposure: - include: health,info,prometheus \ No newline at end of file + include: health,info,prometheus From f2671d0e95eab14a4f25d4ef688f0fb83d200431 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:53:18 +0900 Subject: [PATCH 49/49] hotfix: fix THUMBNAIL_OPTIMIZER_QUERY_STRING (#139) --- .../java/com/oing/config/support/OptimizedImageUrlProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java b/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java index 4b8fd813..1625dac0 100644 --- a/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java +++ b/gateway/src/main/java/com/oing/config/support/OptimizedImageUrlProvider.java @@ -17,7 +17,7 @@ @Component public class OptimizedImageUrlProvider implements OptimizedImageUrlGenerator { - public static final String THUMBNAIL_OPTIMIZER_QUERY_STRING = "type=f&w=96&h=96&quality=70&autorotate=false&faceopt=false"; + public static final String THUMBNAIL_OPTIMIZER_QUERY_STRING = "?type=f&w=96&h=96&quality=70&autorotate=false&faceopt=false"; public static final String KB_IMAGE_OPTIMIZER_QUERY_STRING = "?type=f&w=480&h=480&faceopt=false&quality=50&autorotate=false"; @Value("${cloud.ncp.image-optimizer-cdn}")