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 +

+ +### 🛠 환경변수 | 이름 | 설명 | |----------------------------|-----------------------------| 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..823e5587 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 */ @@ -50,6 +54,13 @@ 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"), + 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/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/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/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/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/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/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(); + } } } 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/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/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/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/controller/MeController.java b/gateway/src/main/java/com/oing/controller/MeController.java index 59775c88..dd2f7079 100644 --- a/gateway/src/main/java/com/oing/controller/MeController.java +++ b/gateway/src/main/java/com/oing/controller/MeController.java @@ -1,16 +1,20 @@ 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; 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; @@ -21,6 +25,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; +import java.util.UUID; + @RequiredArgsConstructor @Controller public class MeController implements MeApi { @@ -29,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() { @@ -83,4 +90,22 @@ public FamilyResponse createFamilyAndJoin() { member.setFamilyId(family.getId()); return FamilyResponse.of(family); } + + + @Override + public AppVersionResponse getCurrentAppVersion(UUID appKey) { + AppVersion appVersion = appVersionCache.getAppVersion(appKey); + return AppVersionResponse.from(appVersion); + } + + @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/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/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 7e5a537c..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; @@ -55,7 +54,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/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/java/com/oing/restapi/MeApi.java b/gateway/src/main/java/com/oing/restapi/MeApi.java index 1d8e0652..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 @@ -54,4 +58,15 @@ FamilyResponse joinFamily( @Operation(summary = "가족 생성 및 가입", description = "가족을 생성하고 가입합니다.") @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/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..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 @@ -38,6 +39,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/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/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/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/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); diff --git a/gateway/src/main/resources/template-application-local.yaml b/gateway/src/main/resources/template-application-local.yaml index 0c3e1337..b2e98324 100644 --- a/gateway/src/main/resources/template-application-local.yaml +++ b/gateway/src/main/resources/template-application-local.yaml @@ -13,6 +13,12 @@ spring: show_sql: true format_sql: true app: +<<<<<<< HEAD +======= + oauth: + google-client-id: ${GOOGLE_CLIENT_ID} + web: + versionFilterEnabled: false external-urls: slack-webhook: ${SLACK_WEBHOOK_URL} # Must Be Replaced token: @@ -24,5 +30,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} 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..632b89f0 --- /dev/null +++ b/gateway/src/test/java/com/oing/repository/MemberPostRepositoryCustomTest.java @@ -0,0 +1,137 @@ +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); + } + + @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/CalendarApiTest.java b/gateway/src/test/java/com/oing/restapi/CalendarApiTest.java index 3c772a05..fac636ca 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; @@ -18,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.*; @@ -48,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}") @@ -64,7 +67,7 @@ class CalendarApiTest { @BeforeEach void setUp() { - TEST_USER1_ID = memberService.createNewMember( + TEST_MEMBER1_ID = memberService.createNewMember( new CreateNewUserDTO( SocialLoginProvider.fromString("APPLE"), "testUser1", @@ -73,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", @@ -84,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", @@ -95,41 +98,142 @@ 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 { + 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_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 ('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 ('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 ('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 ('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 ('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 ('1', '" + TEST_MEMBER1_ID + "', 'https://storage.com/images/1', 0, 0, '" + now + "', '" + now + "', 'post1111', '1');"); // 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); + 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_USER1_TOKEN) + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(joinFamilyRequest)) + .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 + // 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_USER2_TOKEN) + .header("X-AUTH-TOKEN", TEST_MEMBER2_TOKEN) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(joinFamilyRequest)) + .content(objectMapper.writeValueAsString(new JoinFamilyRequest(inviteCode))) ).andExpect(status().isOk()); @@ -137,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")) @@ -147,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 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/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/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/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/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..9fa60d1a --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MemberPostRealEmojiApiTest.java @@ -0,0 +1,188 @@ +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.*; +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)); + } + + @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/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..d5c97f3d --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MemberRealEmojiApiTest.java @@ -0,0 +1,165 @@ +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.*; +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)); + } + + @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/gateway/src/test/resources/application.yaml b/gateway/src/test/resources/application.yaml index e74595d1..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: test + end-point: https://test.com/ access-key: access-key secret-key: secret-key storage: 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/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/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/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/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/controller/MemberControllerTest.java b/member/src/test/java/com/oing/controller/MemberControllerTest.java new file mode 100644 index 00000000..d8b01ce8 --- /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(), member1.getDayOfBirth()), + new FamilyMemberProfileResponse(member2.getId(), member2.getName(), member2.getProfileImgUrl(), member2.getDayOfBirth()) + )); + 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(), null); + + // 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(), null)); + } +} 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); } } 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 + } + +} 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..b9f9af27 --- /dev/null +++ b/post/src/main/java/com/oing/controller/MemberPostCommentController.java @@ -0,0 +1,129 @@ +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 savedComment = memberPostCommentService.savePostComment(memberPostComment); + MemberPostComment addedComment = memberPost.addComment(savedComment); + 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(); + } + + memberPostCommentService.deletePostComment(memberPostComment); + 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/controller/MemberPostController.java b/post/src/main/java/com/oing/controller/MemberPostController.java index cf1735ea..29a8d617 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; @@ -40,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(); @@ -107,4 +109,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/controller/MemberPostReactionController.java b/post/src/main/java/com/oing/controller/MemberPostReactionController.java index 2223d492..94961aeb 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) { @@ -100,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(); @@ -111,12 +117,6 @@ public PostReactionsResponse getPostReactionMembers(String postId) { )); emojiList.forEach(emoji -> emojiMemberIdsMap.putIfAbsent(emoji.getTypeKey(), Collections.emptyList())); - return new PostReactionsResponse(emojiMemberIdsMap); - } - - private void validatePostReactionForDeletion(MemberPost post, String memberId, Emoji emoji) { - if (!memberPostReactionService.isMemberPostReactionExists(post, memberId, emoji)) { - throw new EmojiNotFoundException(); - } + 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 new file mode 100644 index 00000000..a5d3626c --- /dev/null +++ b/post/src/main/java/com/oing/controller/MemberPostRealEmojiController.java @@ -0,0 +1,162 @@ +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.*; +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.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@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 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 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(); + } + + /** + * 게시물에 등록된 리얼 이모지 요약을 조회합니다 + * @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) { + 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); + + 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/controller/MemberRealEmojiController.java b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java new file mode 100644 index 00000000..97e8d909 --- /dev/null +++ b/post/src/main/java/com/oing/controller/MemberRealEmojiController.java @@ -0,0 +1,91 @@ +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.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; + +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@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) { + validateMemberId(memberId); + String imageName = request.imageName(); + return preSignedUrlGenerator.getRealEmojiPreSignedUrl(imageName); + } + + @Transactional + @Override + public RealEmojiResponse createMemberRealEmoji(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 RealEmojiResponse changeMemberRealEmoji(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 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) { + 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 73b8cada..bccdac6d 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) { @@ -64,11 +71,32 @@ 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 = this.realEmojis.size(); + } + + public void removeRealEmoji(MemberPostRealEmoji realEmoji) { + this.realEmojis.remove(realEmoji); + this.realEmojiCnt = this.realEmojis.size(); + } + + 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/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..75812233 --- /dev/null +++ b/post/src/main/java/com/oing/domain/MemberRealEmoji.java @@ -0,0 +1,37 @@ +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; + + public void updateRealEmoji(String realEmojiImageUrl, String realEmojiImageKey) { + this.realEmojiImageUrl = realEmojiImageUrl; + this.realEmojiImageKey = 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/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/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/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/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/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/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/PostRealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java new file mode 100644 index 00000000..4078b388 --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/PostRealEmojiResponse.java @@ -0,0 +1,28 @@ +package com.oing.dto.response; + + +import com.oing.domain.MemberPostRealEmoji; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "피드 게시물 리얼 이모지 응답") +public record PostRealEmojiResponse( + @Schema(description = "피드 게시물 리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String postRealEmojiId, + + @Schema(description = "피드 게시물 ID", example = "01HGW2N7EHJUUDIF99RRS2E97") + String postId, + + @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/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/dto/response/RealEmojiResponse.java b/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java new file mode 100644 index 00000000..9ab83b29 --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/RealEmojiResponse.java @@ -0,0 +1,21 @@ +package com.oing.dto.response; + +import com.oing.domain.MemberRealEmoji; +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 +){ + 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/dto/response/RealEmojisResponse.java b/post/src/main/java/com/oing/dto/response/RealEmojisResponse.java new file mode 100644 index 00000000..5535be3c --- /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/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/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/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/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/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/MemberPostCommentRepository.java b/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java index e99c0dbf..dd40360e 100644 --- a/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java +++ b/post/src/main/java/com/oing/repository/MemberPostCommentRepository.java @@ -3,5 +3,6 @@ import com.oing.domain.MemberPostComment; import org.springframework.data.jpa.repository.JpaRepository; -public interface MemberPostCommentRepository extends JpaRepository { +public interface MemberPostCommentRepository extends JpaRepository, MemberPostCommentRepositoryCustom { + void deleteAllByPostId(String memberPostId); } 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/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/repository/MemberPostRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java new file mode 100644 index 00000000..10661f45 --- /dev/null +++ b/post/src/main/java/com/oing/repository/MemberPostRealEmojiRepository.java @@ -0,0 +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/repository/MemberRealEmojiRepository.java b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java new file mode 100644 index 00000000..5eace7ee --- /dev/null +++ b/post/src/main/java/com/oing/repository/MemberRealEmojiRepository.java @@ -0,0 +1,15 @@ +package com.oing.repository; + +import com.oing.domain.Emoji; +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/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/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/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 new file mode 100644 index 00000000..640da1be --- /dev/null +++ b/post/src/main/java/com/oing/restapi/MemberPostRealEmojiApi.java @@ -0,0 +1,64 @@ +package com.oing.restapi; + +import com.oing.dto.request.PostRealEmojiRequest; +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; +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 + PostRealEmojiResponse createPostRealEmoji( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId, + + @Valid + @RequestBody + PostRealEmojiRequest request + ); + + @Operation(summary = "게시물에서 리얼 이모지 삭제", description = "게시물에서 리얼 이모지를 삭제합니다.") + @DeleteMapping("/{realEmojiId}") + DefaultResponse deletePostRealEmoji( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId, + + @Parameter(description = "리얼 이모지 ID", example = "01HEFDFADFDFDAFDFDARS2E97") + @PathVariable + String realEmojiId + ); + + @Operation(summary = "게시물의 리얼 이모지 요약 조회", description = "게시물에 달린 리얼 이모지 요약을 조회합니다.") + @GetMapping("/summary") + PostRealEmojiSummaryResponse getPostRealEmojiSummary( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId + ); + + @Operation(summary = "게시물의 리얼 이모지 전체 조회", description = "게시물에 달린 모든 리얼 이모지 목록을 조회합니다.") + @GetMapping + ArrayResponse getPostRealEmojis( + @Parameter(description = "게시물 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String postId + ); + + @Operation(summary = "게시물의 리얼 이모지를 남긴 전체 멤버 조회", description = "게시물에 리얼 이모지를 남긴 모든 멤버 목록을 조회합니다.") + @GetMapping("/member") + PostRealEmojiMemberResponse getPostRealEmojiMembers( + @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..0279d30d --- /dev/null +++ b/post/src/main/java/com/oing/restapi/MemberRealEmojiApi.java @@ -0,0 +1,68 @@ +package com.oing.restapi; + +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.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; +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 + RealEmojiResponse createMemberRealEmoji( + @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String memberId, + + @Valid + @RequestBody + CreateMyRealEmojiRequest request + ); + + @Operation(summary = "회원의 리얼 이모지 변경", description = "회원의 리얼 이모지 사진을 변경합니다.") + @PutMapping("/{realEmojiId}") + RealEmojiResponse changeMemberRealEmoji( + @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String memberId, + + @Parameter(description = "리얼 이모지 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String realEmojiId, + + @Valid + @RequestBody + UpdateMyRealEmojiRequest request + ); + + @Operation(summary = "회원의 리얼 이모지 조회", description = "회원의 리얼 이모지를 조회합니다.") + @GetMapping + RealEmojisResponse getMemberRealEmojis( + @Parameter(description = "회원 ID", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + @PathVariable + String memberId + ); +} 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..054713de --- /dev/null +++ b/post/src/main/java/com/oing/service/MemberPostCommentService.java @@ -0,0 +1,80 @@ +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 +@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() + ); + } + + @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/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/MemberPostService.java b/post/src/main/java/com/oing/service/MemberPostService.java index ee8dce5e..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; /** * 멤버들이 범위 날짜 안에 올린 대표 게시물들을 가져온다. @@ -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 @@ -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/MemberRealEmojiService.java b/post/src/main/java/com/oing/service/MemberRealEmojiService.java new file mode 100644 index 00000000..14597d6b --- /dev/null +++ b/post/src/main/java/com/oing/service/MemberRealEmojiService.java @@ -0,0 +1,44 @@ +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; + +import java.util.List; + +@Service +@RequiredArgsConstructor +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); + } + + public MemberRealEmoji findRealEmojiById(String realEmojiId) { + return memberRealEmojiRepository + .findById(realEmojiId) + .orElseThrow(RealEmojiNotFoundException::new); + } + + public boolean findRealEmojiByEmojiType(Emoji emoji) { + return memberRealEmojiRepository + .findByType(emoji) + .isPresent(); + } + + public List findRealEmojisByMemberId(String memberId) { + return memberRealEmojiRepository.findAllByMemberId(memberId); + } + +} 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) { +} 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..1ff42774 --- /dev/null +++ b/post/src/test/java/com/oing/controller/MemberPostCommentControllerTest.java @@ -0,0 +1,277 @@ +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(memberPostCommentService.savePostComment(any())).thenReturn(memberPostComment); + + //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/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 + } +} 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..0b3c7398 --- /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.PostReactionMemberResponse; +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 + PostReactionMemberResponse 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)); + } +} 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..c0313fab --- /dev/null +++ b/post/src/test/java/com/oing/controller/MemberPostRealEmojiControllerTest.java @@ -0,0 +1,206 @@ +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.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; +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 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 + 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())); + } + + @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()); + } +} 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..e0515e62 --- /dev/null +++ b/post/src/test/java/com/oing/controller/MemberRealEmojiControllerTest.java @@ -0,0 +1,168 @@ +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.dto.response.RealEmojisResponse; +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 java.util.List; + +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.createMemberRealEmoji(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.createMemberRealEmoji(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.createMemberRealEmoji(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.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()); + } +} 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); 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 + } +}