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
+
+
+
+
+
+### 🖥️ 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
+ }
+}