diff --git a/backend/pcloud-api/docs/asciidoc/auth.adoc b/backend/pcloud-api/docs/asciidoc/auth.adoc index 9dc38b54..5be6386f 100644 --- a/backend/pcloud-api/docs/asciidoc/auth.adoc +++ b/backend/pcloud-api/docs/asciidoc/auth.adoc @@ -11,11 +11,11 @@ === Request -include::{snippets}/auth-controller-test/oauth_login/request-headers.adoc[] -include::{snippets}/auth-controller-test/oauth_login/path-parameters.adoc[] -include::{snippets}/auth-controller-test/oauth_login/http-request.adoc[] +include::{snippets}/auth-controller-web-mvc-test/oauth_login/request-headers.adoc[] +include::{snippets}/auth-controller-web-mvc-test/oauth_login/path-parameters.adoc[] +include::{snippets}/auth-controller-web-mvc-test/oauth_login/http-request.adoc[] === Response -include::{snippets}/auth-controller-test/oauth_login/response-fields.adoc[] -include::{snippets}/auth-controller-test/oauth_login/http-response.adoc[] +include::{snippets}/auth-controller-web-mvc-test/oauth_login/response-fields.adoc[] +include::{snippets}/auth-controller-web-mvc-test/oauth_login/http-response.adoc[] diff --git a/backend/pcloud-api/docs/asciidoc/popups.adoc b/backend/pcloud-api/docs/asciidoc/popups.adoc new file mode 100644 index 00000000..9f0b10aa --- /dev/null +++ b/backend/pcloud-api/docs/asciidoc/popups.adoc @@ -0,0 +1,78 @@ += Auth API 문서 +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 3 + +== Public Tag 정보 (24.08.12 업데이트) + +++ 서버측에서 조회 가능하나 캐싱이 성능적으로 나아서 정합성과 트레이드 오프했다고 봐주시면 됩니다. +만약 캐싱이 불필요하다 생각하면 회의 요청주세요 :) + +- 브랜드 +- 패션 +- 뷰티 +- 음식 +- 홈 +- 완구류 +- 레저 +- 서적 +- 음악 +- 펫 +- 운동 +- 디지털 +- 예술 +- 캐릭터 +- 굿즈 +- 전시 +- 기타 + +== 팝업스토어를 생성한다 (POST /api/popups) + +=== Request + +include::{snippets}/popups-controller-web-mvc-test/create_popups/request-headers.adoc[] +include::{snippets}/popups-controller-web-mvc-test/create_popups/request-fields.adoc[] +include::{snippets}/popups-controller-web-mvc-test/create_popups/http-request.adoc[] + +=== Response + +include::{snippets}/popups-controller-web-mvc-test/create_popups/response-headers.adoc[] +include::{snippets}/popups-controller-web-mvc-test/create_popups/http-response.adoc[] + +== 팝업스토어를 페이징 조회한다 (GET /api/popups?popupsId=${value}&pageSize=${value}) + +=== Request + +include::{snippets}/popups-controller-web-mvc-test/find_all_popups_with_paging/query-parameters.adoc[] +include::{snippets}/popups-controller-web-mvc-test/find_all_popups_with_paging/http-request.adoc[] + +=== Response + +include::{snippets}/popups-controller-web-mvc-test/find_all_popups_with_paging/response-fields.adoc[] +include::{snippets}/popups-controller-web-mvc-test/find_all_popups_with_paging/http-response.adoc[] + +== 팝업스토어를 상세 조회한다 (GET /api/popups/{popupsId}) + +=== Request + +include::{snippets}/popups-controller-web-mvc-test/find_popups/path-parameters.adoc[] +include::{snippets}/popups-controller-web-mvc-test/find_popups/http-request.adoc[] + +=== Response + +include::{snippets}/popups-controller-web-mvc-test/find_popups/response-fields.adoc[] +include::{snippets}/popups-controller-web-mvc-test/find_popups/http-response.adoc[] + +== 팝업스토어를 업데이트한다 (PATCH /api/popups/{popupsId}) + +=== Request + +include::{snippets}/popups-controller-web-mvc-test/patch_popups/request-headers.adoc[] +include::{snippets}/popups-controller-web-mvc-test/patch_popups/request-fields.adoc[] +include::{snippets}/popups-controller-web-mvc-test/patch_popups/http-request.adoc[] + +=== Response + +include::{snippets}/popups-controller-web-mvc-test/patch_popups/http-response.adoc[] diff --git a/backend/pcloud-api/src/main/java/com/api/customtag/infrastructure/CustomTagRepositoryImpl.java b/backend/pcloud-api/src/main/java/com/api/customtag/infrastructure/CustomTagRepositoryImpl.java new file mode 100644 index 00000000..8e2647fb --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/customtag/infrastructure/CustomTagRepositoryImpl.java @@ -0,0 +1,33 @@ +package com.api.customtag.infrastructure; + +import com.domain.domains.common.CustomTagType; +import com.domain.domains.customtag.domain.CustomTag; +import com.domain.domains.customtag.domain.CustomTagRepository; +import com.domain.domains.customtag.infrastructure.CustomTagJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Repository +public class CustomTagRepositoryImpl implements CustomTagRepository { + + private final CustomTagJpaRepository customTagJpaRepository; + + @Override + public Optional findByTypeAndTargetId(final CustomTagType type, final Long targetId) { + return customTagJpaRepository.findByTypeAndTargetId(type, targetId); + } + + @Override + public List saveAll(final List customTags) { + return customTagJpaRepository.saveAll(customTags); + } + + @Override + public void deleteAllByTypeAndTargetId(final CustomTagType type, final Long targetId) { + customTagJpaRepository.deleteAllByTypeAndTargetId(type, targetId); + } +} diff --git a/backend/pcloud-api/src/main/java/com/api/customtag/service/TagEventHandler.java b/backend/pcloud-api/src/main/java/com/api/customtag/service/TagEventHandler.java new file mode 100644 index 00000000..8937f939 --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/customtag/service/TagEventHandler.java @@ -0,0 +1,44 @@ +package com.api.customtag.service; + +import com.domain.domains.common.CustomTagType; +import com.domain.domains.customtag.domain.CustomTag; +import com.domain.domains.customtag.domain.CustomTagRepository; +import com.domain.domains.popups.event.PopupsTagsCreatedEvents; +import com.domain.domains.popups.event.PopupsTagsUpdatedEvents; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional +@Service +public class TagEventHandler { + + private final CustomTagRepository customTagRepository; + + @EventListener(PopupsTagsCreatedEvents.class) + public void savePopupsTags(final PopupsTagsCreatedEvents event) { + List customTags = getPopupsCustomTag(event.tags(), event.type(), event.popupsId()); + customTagRepository.saveAll(customTags); + } + + private List getPopupsCustomTag( + final List tagNames, + final CustomTagType type, + final Long targetId + ) { + return tagNames.stream() + .map(tagName -> CustomTag.of(tagName, type, targetId)) + .toList(); + } + + @EventListener(PopupsTagsUpdatedEvents.class) + public void updatePopupsTags(final PopupsTagsUpdatedEvents event) { + List customTags = getPopupsCustomTag(event.tags(), event.type(), event.popupsId()); + customTagRepository.deleteAllByTypeAndTargetId(event.type(), event.popupsId()); + customTagRepository.saveAll(customTags); + } +} diff --git a/backend/pcloud-api/src/main/java/com/api/global/config/AuthConfig.java b/backend/pcloud-api/src/main/java/com/api/global/config/AuthConfig.java index a8716195..7ec8e884 100644 --- a/backend/pcloud-api/src/main/java/com/api/global/config/AuthConfig.java +++ b/backend/pcloud-api/src/main/java/com/api/global/config/AuthConfig.java @@ -3,7 +3,8 @@ import com.api.global.config.interceptor.auth.LoginValidCheckerInterceptor; import com.api.global.config.interceptor.auth.ParseMemberIdFromTokenInterceptor; import com.api.global.config.interceptor.auth.PathMatcherInterceptor; -import com.api.global.config.resolver.AuthArgumentResolver; +import com.api.global.config.resolver.AuthMemberArgumentResolver; +import com.api.global.config.resolver.AuthMembersArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -14,7 +15,6 @@ import java.util.List; import static com.api.global.config.interceptor.auth.support.HttpMethod.DELETE; -import static com.api.global.config.interceptor.auth.support.HttpMethod.GET; import static com.api.global.config.interceptor.auth.support.HttpMethod.OPTIONS; import static com.api.global.config.interceptor.auth.support.HttpMethod.PATCH; import static com.api.global.config.interceptor.auth.support.HttpMethod.POST; @@ -23,7 +23,8 @@ @Configuration public class AuthConfig implements WebMvcConfigurer { - private final AuthArgumentResolver authArgumentResolver; + private final AuthMemberArgumentResolver authMemberArgumentResolver; + private final AuthMembersArgumentResolver authMembersArgumentResolver; private final ParseMemberIdFromTokenInterceptor parseMemberIdFromTokenInterceptor; private final LoginValidCheckerInterceptor loginValidCheckerInterceptor; @@ -41,11 +42,12 @@ private HandlerInterceptor parseMemberIdFromTokenInterceptor() { private HandlerInterceptor loginValidCheckerInterceptor() { return new PathMatcherInterceptor(loginValidCheckerInterceptor) .excludePathPattern("/**", OPTIONS) - .addPathPatterns("/members/test", GET, POST, PATCH, DELETE); + .addPathPatterns("/popups/**", POST, PATCH, DELETE); } @Override public void addArgumentResolvers(final List resolvers) { - resolvers.add(authArgumentResolver); + resolvers.add(authMemberArgumentResolver); + resolvers.add(authMembersArgumentResolver); } } diff --git a/backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthArgumentResolver.java b/backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthMemberArgumentResolver.java similarity index 74% rename from backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthArgumentResolver.java rename to backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthMemberArgumentResolver.java index 56801876..036264dc 100644 --- a/backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthArgumentResolver.java +++ b/backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthMemberArgumentResolver.java @@ -1,9 +1,9 @@ package com.api.global.config.resolver; import com.api.global.config.interceptor.auth.support.AuthenticationContext; -import com.common.annotation.AuthMember; import com.common.exception.AuthException; import com.common.exception.AuthExceptionType; +import com.domain.annotation.AuthMember; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; @@ -14,7 +14,7 @@ @RequiredArgsConstructor @Component -public class AuthArgumentResolver implements HandlerMethodArgumentResolver { +public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver { private static final int ANONYMOUS = -1; @@ -27,10 +27,12 @@ public boolean supportsParameter(final MethodParameter parameter) { } @Override - public Object resolveArgument(final MethodParameter parameter, - final ModelAndViewContainer mavContainer, - final NativeWebRequest webRequest, - final WebDataBinderFactory binderFactory) throws Exception { + public Object resolveArgument( + final MethodParameter parameter, + final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory binderFactory + ) { Long memberId = authenticationContext.getPrincipal(); if (memberId == ANONYMOUS) { diff --git a/backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthMembersArgumentResolver.java b/backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthMembersArgumentResolver.java new file mode 100644 index 00000000..61025a96 --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/global/config/resolver/AuthMembersArgumentResolver.java @@ -0,0 +1,72 @@ +package com.api.global.config.resolver; + +import com.api.global.config.interceptor.auth.support.AuthenticationContext; +import com.common.exception.AuthException; +import com.common.exception.AuthExceptionType; +import com.domain.annotation.AuthMembers; +import com.domain.domains.member.domain.Member; +import com.domain.domains.member.domain.MemberRepository; +import com.domain.domains.member.domain.vo.MemberRole; +import com.domain.domains.member.exception.MemberException; +import com.domain.domains.member.exception.MemberExceptionType; +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.Arrays; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +@RequiredArgsConstructor +@Component +public class AuthMembersArgumentResolver implements HandlerMethodArgumentResolver { + + private static final int ANONYMOUS = -1; + + private final AuthenticationContext authenticationContext; + private final MemberRepository memberRepository; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthMembers.class) && + parameter.getParameterType().equals(Long.class); + } + + @Override + public Object resolveArgument( + final MethodParameter parameter, + final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory binderFactory + ) { + Long memberId = authenticationContext.getPrincipal(); + + if (memberId == ANONYMOUS) { + throw new AuthException(AuthExceptionType.LOGIN_INVALID_EXCEPTION); + } + + Member member = findMember(memberId); + List permittedRoles = getPermittedRoles(parameter); + if (!permittedRoles.contains(member.getMemberRole())) { + throw new AuthException(AuthExceptionType.FORBIDDEN_AUTH_LEVEL); + } + + return memberId; + } + + private Member findMember(final Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberExceptionType.MEMBER_NOT_FOUND_EXCEPTION)); + } + + private List getPermittedRoles(final MethodParameter parameter) { + AuthMembers auths = parameter.getParameterAnnotation(AuthMembers.class); + requireNonNull(auths); + return Arrays.asList(auths.permit()); + } +} diff --git a/backend/pcloud-api/src/main/java/com/api/popups/application/PopupsQueryService.java b/backend/pcloud-api/src/main/java/com/api/popups/application/PopupsQueryService.java new file mode 100644 index 00000000..4542194d --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/popups/application/PopupsQueryService.java @@ -0,0 +1,30 @@ +package com.api.popups.application; + +import com.domain.domains.popups.domain.PopupsRepository; +import com.domain.domains.popups.domain.response.PopupsSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; +import com.domain.domains.popups.exception.PopupsException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.domain.domains.popups.exception.PopupsExceptionType.POPUPS_NOT_FOUND_EXCEPTION; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class PopupsQueryService { + + private final PopupsRepository popupsRepository; + + public PopupsSpecificResponse findById(final Long popupsId) { + return popupsRepository.findSpecificById(popupsId) + .orElseThrow(() -> new PopupsException(POPUPS_NOT_FOUND_EXCEPTION)); + } + + public List findAll(final Long popupsId, final Integer pageSize) { + return popupsRepository.findAllWithPaging(popupsId, pageSize); + } +} diff --git a/backend/pcloud-api/src/main/java/com/api/popups/application/PopupsService.java b/backend/pcloud-api/src/main/java/com/api/popups/application/PopupsService.java new file mode 100644 index 00000000..b6953b0d --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/popups/application/PopupsService.java @@ -0,0 +1,46 @@ +package com.api.popups.application; + +import com.api.popups.application.request.PopupsCreateRequest; +import com.api.popups.application.request.PopupsUpdateRequest; +import com.common.config.event.Events; +import com.domain.domains.common.CustomTagType; +import com.domain.domains.popups.domain.Popups; +import com.domain.domains.popups.domain.PopupsRepository; +import com.domain.domains.popups.event.PopupsTagsCreatedEvents; +import com.domain.domains.popups.event.PopupsTagsUpdatedEvents; +import com.domain.domains.popups.exception.PopupsException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.domain.domains.popups.exception.PopupsExceptionType.POPUPS_NOT_FOUND_EXCEPTION; + +@RequiredArgsConstructor +@Transactional +@Service +public class PopupsService { + + private final PopupsRepository popupsRepository; + + public Long create(final Long memberId, final PopupsCreateRequest request) { + Popups popups = popupsRepository.save(request.toDomain(memberId)); + Events.raise(new PopupsTagsCreatedEvents(popups.getId(), request.tags(), CustomTagType.POPUPS)); + return popups.getId(); + } + + public void patchById( + final Long memberId, + final Long popupsId, + final PopupsUpdateRequest request + ) { + Popups popups = findPopups(popupsId); + Popups updatedPopups = request.toDomain(memberId); + popups.update(updatedPopups); + Events.raise(new PopupsTagsUpdatedEvents(popups.getId(), request.tags(), CustomTagType.POPUPS)); + } + + private Popups findPopups(final Long popupsId) { + return popupsRepository.findById(popupsId) + .orElseThrow(() -> new PopupsException(POPUPS_NOT_FOUND_EXCEPTION)); + } +} diff --git a/backend/pcloud-api/src/main/java/com/api/popups/application/request/PopupsCreateRequest.java b/backend/pcloud-api/src/main/java/com/api/popups/application/request/PopupsCreateRequest.java new file mode 100644 index 00000000..8865d58f --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/popups/application/request/PopupsCreateRequest.java @@ -0,0 +1,39 @@ +package com.api.popups.application.request; + +import com.domain.domains.popups.domain.Popups; + +import java.time.LocalDateTime; +import java.util.List; + +public record PopupsCreateRequest( + String title, + String description, + String location, + Boolean isParkingAvailable, + Integer fee, + LocalDateTime startDate, + LocalDateTime endDate, + String openTimes, + String latitude, + String longitude, + String publicTag, + List tags +) { + + public Popups toDomain(final Long memberId) { + return Popups.of( + memberId, + title, + description, + location, + isParkingAvailable, + fee, + startDate, + endDate, + openTimes, + latitude, + longitude, + publicTag + ); + } +} diff --git a/backend/pcloud-api/src/main/java/com/api/popups/application/request/PopupsUpdateRequest.java b/backend/pcloud-api/src/main/java/com/api/popups/application/request/PopupsUpdateRequest.java new file mode 100644 index 00000000..1dea3eb9 --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/popups/application/request/PopupsUpdateRequest.java @@ -0,0 +1,39 @@ +package com.api.popups.application.request; + +import com.domain.domains.popups.domain.Popups; + +import java.time.LocalDateTime; +import java.util.List; + +public record PopupsUpdateRequest( + String title, + String description, + String location, + Boolean isParkingAvailable, + int fee, + LocalDateTime startDate, + LocalDateTime endDate, + String openTimes, + String latitude, + String longitude, + String publicTag, + List tags +) { + + public Popups toDomain(final Long memberId) { + return Popups.of( + memberId, + title, + description, + location, + isParkingAvailable, + fee, + startDate, + endDate, + openTimes, + latitude, + longitude, + publicTag + ); + } +} diff --git a/backend/pcloud-api/src/main/java/com/api/popups/infrastructure/PopupsRepositoryImpl.java b/backend/pcloud-api/src/main/java/com/api/popups/infrastructure/PopupsRepositoryImpl.java new file mode 100644 index 00000000..61004525 --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/popups/infrastructure/PopupsRepositoryImpl.java @@ -0,0 +1,41 @@ +package com.api.popups.infrastructure; + +import com.domain.domains.popups.domain.Popups; +import com.domain.domains.popups.domain.PopupsRepository; +import com.domain.domains.popups.domain.response.PopupsSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; +import com.domain.domains.popups.infrastructure.PopupsJpaRepository; +import com.domain.domains.popups.infrastructure.PopupsQueryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Repository +public class PopupsRepositoryImpl implements PopupsRepository { + + private final PopupsJpaRepository popupsJpaRepository; + private final PopupsQueryRepository popupsQueryRepository; + + @Override + public Optional findById(final Long id) { + return popupsJpaRepository.findById(id); + } + + @Override + public Popups save(final Popups popups) { + return popupsJpaRepository.save(popups); + } + + @Override + public Optional findSpecificById(final Long id) { + return popupsQueryRepository.findSpecificById(id); + } + + @Override + public List findAllWithPaging(final Long popupsId, final Integer pageSize) { + return popupsQueryRepository.findAllWithPaging(popupsId, pageSize); + } +} diff --git a/backend/pcloud-api/src/main/java/com/api/popups/presentation/PopupsController.java b/backend/pcloud-api/src/main/java/com/api/popups/presentation/PopupsController.java new file mode 100644 index 00000000..6188c83e --- /dev/null +++ b/backend/pcloud-api/src/main/java/com/api/popups/presentation/PopupsController.java @@ -0,0 +1,73 @@ +package com.api.popups.presentation; + +import com.api.popups.application.PopupsQueryService; +import com.api.popups.application.PopupsService; +import com.api.popups.application.request.PopupsCreateRequest; +import com.api.popups.application.request.PopupsUpdateRequest; +import com.domain.annotation.AuthMember; +import com.domain.annotation.AuthMembers; +import com.domain.domains.popups.domain.response.PopupsSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; +import java.util.List; + +import static com.domain.domains.member.domain.vo.MemberRole.ADMIN; +import static com.domain.domains.member.domain.vo.MemberRole.MANAGER; + +@RequiredArgsConstructor +@RequestMapping("/popups") +@RestController +public class PopupsController { + + private final PopupsService popupsService; + private final PopupsQueryService popupsQueryService; + + /** + * TODO : 이미지 처리 방식 회의 필요 + */ + + @PostMapping + public ResponseEntity create( + @AuthMembers(permit = {MANAGER, ADMIN}) final Long memberId, + @RequestBody final PopupsCreateRequest request + ) { + Long createdPopupsId = popupsService.create(memberId, request); + return ResponseEntity.created(URI.create("/popups/" + createdPopupsId)) + .build(); + } + + @GetMapping + public ResponseEntity> findAll( + @RequestParam(name = "popupsId", required = false) final Long popupsId, + @RequestParam(name = "pageSize") final Integer pageSize + ) { + return ResponseEntity.ok(popupsQueryService.findAll(popupsId, pageSize)); + } + + @GetMapping("/{popupsId}") + public ResponseEntity findById(@PathVariable final Long popupsId) { + return ResponseEntity.ok(popupsQueryService.findById(popupsId)); + } + + @PatchMapping("/{popupsId}") + public ResponseEntity patchById( + @AuthMember final Long memberId, + @PathVariable final Long popupsId, + @RequestBody final PopupsUpdateRequest request + ) { + popupsService.patchById(memberId, popupsId, request); + return ResponseEntity.noContent() + .build(); + } +} diff --git a/backend/pcloud-api/src/test/java/com/api/auth/application/AuthServiceTest.java b/backend/pcloud-api/src/test/java/com/api/auth/application/AuthServiceTest.java index 0c4f0889..7aa7764e 100644 --- a/backend/pcloud-api/src/test/java/com/api/auth/application/AuthServiceTest.java +++ b/backend/pcloud-api/src/test/java/com/api/auth/application/AuthServiceTest.java @@ -7,7 +7,7 @@ import com.domain.domains.member.domain.MemberRepository; import com.domain.domains.member.domain.vo.OAuthPlatform; import com.domain.domains.member.domain.vo.OauthId; -import member.fixture.FakeMemberRepository; +import member.FakeMemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; diff --git a/backend/pcloud-api/src/test/java/com/api/auth/presentation/AuthControllerTest.java b/backend/pcloud-api/src/test/java/com/api/auth/presentation/AuthControllerWebMvcTest.java similarity index 97% rename from backend/pcloud-api/src/test/java/com/api/auth/presentation/AuthControllerTest.java rename to backend/pcloud-api/src/test/java/com/api/auth/presentation/AuthControllerWebMvcTest.java index 2bfd8352..5a8810f6 100644 --- a/backend/pcloud-api/src/test/java/com/api/auth/presentation/AuthControllerTest.java +++ b/backend/pcloud-api/src/test/java/com/api/auth/presentation/AuthControllerWebMvcTest.java @@ -25,7 +25,7 @@ @SuppressWarnings("NonAsciiCharacters") @AutoConfigureRestDocs @WebMvcTest(AuthController.class) -class AuthControllerTest extends MockBeanInjection { +class AuthControllerWebMvcTest extends MockBeanInjection { @Autowired private MockMvc mockMvc; diff --git a/backend/pcloud-api/src/test/java/com/api/helper/AcceptanceBaseFixture.java b/backend/pcloud-api/src/test/java/com/api/helper/AcceptanceBaseFixture.java new file mode 100644 index 00000000..20ec2294 --- /dev/null +++ b/backend/pcloud-api/src/test/java/com/api/helper/AcceptanceBaseFixture.java @@ -0,0 +1,34 @@ +package com.api.helper; + +import com.common.auth.TokenProvider; +import com.domain.domains.member.domain.Member; +import com.domain.domains.member.domain.MemberRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.springframework.beans.factory.annotation.Autowired; + +import static member.fixture.MemberFixture.어드민_멤버_생성_id_없음_kakao_oauth_가입; +import static member.fixture.MemberFixture.일반_멤버_생성_id_없음; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class AcceptanceBaseFixture extends IntegrationHelper { + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + protected MemberRepository memberRepository; + + protected String 일반_유저_토큰; + protected String 관리자_토큰; + + @BeforeEach + void initMembers() { + Member normalMember = memberRepository.save(일반_멤버_생성_id_없음()); + Member adminMember = memberRepository.save(어드민_멤버_생성_id_없음_kakao_oauth_가입()); + 일반_유저_토큰 = tokenProvider.create(normalMember.getId()); + 관리자_토큰 = tokenProvider.create(adminMember.getId()); + } +} diff --git a/backend/pcloud-api/src/test/java/com/api/helper/MockBeanInjection.java b/backend/pcloud-api/src/test/java/com/api/helper/MockBeanInjection.java index b046437b..d53fac02 100644 --- a/backend/pcloud-api/src/test/java/com/api/helper/MockBeanInjection.java +++ b/backend/pcloud-api/src/test/java/com/api/helper/MockBeanInjection.java @@ -2,7 +2,10 @@ import com.api.auth.application.AuthService; import com.api.global.config.interceptor.auth.support.AuthenticationContext; +import com.api.popups.application.PopupsQueryService; +import com.api.popups.application.PopupsService; import com.common.auth.TokenProvider; +import com.domain.domains.member.domain.MemberRepository; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; @@ -17,4 +20,13 @@ public class MockBeanInjection { @MockBean protected AuthService authService; + + @MockBean + protected MemberRepository memberRepository; + + @MockBean + protected PopupsService popupsService; + + @MockBean + protected PopupsQueryService popupsQueryService; } diff --git a/backend/pcloud-api/src/test/java/com/api/popups/application/PopupsServiceTest.java b/backend/pcloud-api/src/test/java/com/api/popups/application/PopupsServiceTest.java new file mode 100644 index 00000000..a4251fce --- /dev/null +++ b/backend/pcloud-api/src/test/java/com/api/popups/application/PopupsServiceTest.java @@ -0,0 +1,86 @@ +package com.api.popups.application; + +import com.api.popups.application.request.PopupsCreateRequest; +import com.common.exception.AuthException; +import com.common.exception.AuthExceptionType; +import com.domain.domains.popups.domain.Popups; +import com.domain.domains.popups.domain.PopupsRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import popups.FakePopupsRepository; + +import java.util.Optional; + +import static com.api.popups.fixture.request.PopupsRequestFixtures.팝업스토어_생성_요청; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static popups.fixture.PopupsFixture.일반_팝업_스토어_생성_뷰티; +import static popups.fixture.PopupsFixture.일반_팝업_스토어_생성_뷰티_유효하지_않은_주인; +import static popups.fixture.PopupsFixture.일반_팝업_스토어_생성_펫샵; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class PopupsServiceTest { + + private PopupsService popupsService; + private PopupsRepository popupsRepository; + + @BeforeEach + void setup() { + popupsRepository = new FakePopupsRepository(); + popupsService = new PopupsService(popupsRepository); + } + + @Test + void 팝업_스토어를_생성한다() { + // given + Long memberId = 1L; + PopupsCreateRequest request = 팝업스토어_생성_요청(); + + // when + Long response = popupsService.create(memberId, request); + + // then + assertThat(response).isEqualTo(1L); + } + + @Nested + class 팝업스토어_업데이트 { + + @Test + void 정상적으로_업데이트한다() { + // given + Popups savedPopups = popupsRepository.save(일반_팝업_스토어_생성_뷰티()); + Popups updatedPopups = 일반_팝업_스토어_생성_펫샵(); + + // when + savedPopups.update(updatedPopups); + + // then + Optional found = popupsRepository.findById(savedPopups.getId()); + assertSoftly(softly -> { + softly.assertThat(found).isPresent(); + softly.assertThat(found.get()) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(updatedPopups); + }); + } + + @Test + void 유저가_다르면_업데이트하지_못한다() { + // given + Popups savedPopups = popupsRepository.save(일반_팝업_스토어_생성_뷰티()); + Popups updatedPopups = 일반_팝업_스토어_생성_뷰티_유효하지_않은_주인(); + + // when & then + assertThatThrownBy(() -> savedPopups.update(updatedPopups)) + .isInstanceOf(AuthException.class) + .hasMessageContaining(AuthExceptionType.AUTH_NOT_EQUALS_EXCEPTION.message()); + } + } +} diff --git a/backend/pcloud-api/src/test/java/com/api/popups/fixture/request/PopupsRequestFixtures.java b/backend/pcloud-api/src/test/java/com/api/popups/fixture/request/PopupsRequestFixtures.java new file mode 100644 index 00000000..80e0316c --- /dev/null +++ b/backend/pcloud-api/src/test/java/com/api/popups/fixture/request/PopupsRequestFixtures.java @@ -0,0 +1,51 @@ +package com.api.popups.fixture.request; + +import com.api.popups.application.request.PopupsCreateRequest; +import com.api.popups.application.request.PopupsUpdateRequest; +import com.domain.domains.common.PublicTag; + +import java.time.LocalDateTime; +import java.util.List; + +public class PopupsRequestFixtures { + + public static PopupsCreateRequest 팝업스토어_생성_요청() { + return new PopupsCreateRequest( + "빵빵이 팝업스토어", + "빵빵이와 함께하는 체험형 팝업스토어입니다.", + "서울 마포구 동교동 155-55", + true, + 10000, + LocalDateTime.now().minusDays(10), + LocalDateTime.now(), + """ + 평일 09:00 ~ 18:00, + 주말 12:00 ~ 21:00 + """, + "37.556725", + "126.9234952", + PublicTag.CHARACTER.getName(), + List.of("빵빵이", "만원", "가족", "데이트") + ); + } + + public static PopupsUpdateRequest 팝업스토어_업데이트_요청() { + return new PopupsUpdateRequest( + "빵빵이 팝업스토어", + "빵빵이와 함께하는 체험형 팝업스토어입니다.", + "서울 마포구 동교동 155-55", + true, + 10000, + LocalDateTime.now().minusDays(10), + LocalDateTime.now(), + """ + 평일 09:00 ~ 18:00, + 주말 12:00 ~ 21:00 + """, + "37.556725", + "126.9234952", + PublicTag.CHARACTER.getName(), + List.of("빵빵이", "만원", "가족", "데이트") + ); + } +} diff --git a/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerAcceptanceFixture.java b/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerAcceptanceFixture.java new file mode 100644 index 00000000..bd7a7fa6 --- /dev/null +++ b/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerAcceptanceFixture.java @@ -0,0 +1,94 @@ +package com.api.popups.presentation; + +import com.api.helper.AcceptanceBaseFixture; +import com.api.popups.application.request.PopupsCreateRequest; +import com.api.popups.application.request.PopupsUpdateRequest; +import com.api.popups.fixture.request.PopupsRequestFixtures; +import com.domain.domains.popups.domain.Popups; +import com.domain.domains.popups.domain.PopupsRepository; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.apache.http.HttpHeaders; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.api.popups.fixture.request.PopupsRequestFixtures.팝업스토어_업데이트_요청; +import static org.assertj.core.api.Assertions.assertThat; +import static popups.fixture.PopupsFixture.일반_팝업_스토어_생성_뷰티; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class PopupsControllerAcceptanceFixture extends AcceptanceBaseFixture { + + @Autowired + protected PopupsRepository popupsRepository; + + protected PopupsCreateRequest 팝업스토어_생성_요청서() { + return PopupsRequestFixtures.팝업스토어_생성_요청(); + } + + protected ExtractableResponse 팝업스토어_생성_요청(final PopupsCreateRequest request) { + return RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + 관리자_토큰) + .contentType(ContentType.JSON) + .body(request) + .post("/popups") + .then().log().all() + .extract(); + } + + protected void 생성_요청_결과_검증(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + protected Popups 팝업_스토어_생성() { + return popupsRepository.save(일반_팝업_스토어_생성_뷰티()); + } + + protected ExtractableResponse 팝업스토어_페이징_조회_요청() { + return RestAssured.given().log().all() + .when() + .get("/popups?pageSize=1") + .then().log().all() + .extract(); + } + + protected void 페이징_조회_결과_검증(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + protected ExtractableResponse 팝업스토어_상세_조회_요청() { + return RestAssured.given().log().all() + .when() + .get("/popups/1") + .then().log().all() + .extract(); + } + + protected void 팝업스토어_상세_조회_결과_검증(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + protected PopupsUpdateRequest 팝업스토어_업데이트_요청서() { + return 팝업스토어_업데이트_요청(); + } + + protected ExtractableResponse 팝업스토어_상세_조회_요청(final PopupsUpdateRequest updateRequest) { + return RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + 일반_유저_토큰) + .contentType(ContentType.JSON) + .body(updateRequest) + .when() + .patch("/popups/1") + .then().log().all() + .extract(); + } + + protected void 팝업스토어_업데이트_결과_검증(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } +} diff --git a/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerAcceptanceTest.java b/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerAcceptanceTest.java new file mode 100644 index 00000000..a39cfa02 --- /dev/null +++ b/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerAcceptanceTest.java @@ -0,0 +1,59 @@ +package com.api.popups.presentation; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class PopupsControllerAcceptanceTest extends PopupsControllerAcceptanceFixture { + + @Test + void 팝업스토어_생성() { + // given + var 요청서 = 팝업스토어_생성_요청서(); + + // when + var 생성_요청_결과 = 팝업스토어_생성_요청(요청서); + + // then + 생성_요청_결과_검증(생성_요청_결과); + } + + @Test + void 팝업스토어_페이징_조회() { + // given + 팝업_스토어_생성(); + + // when + var 요청_결과 = 팝업스토어_페이징_조회_요청(); + + // then + 페이징_조회_결과_검증(요청_결과); + } + + @Test + void 팝업스토어_상세_조회() { + // given + 팝업_스토어_생성(); + + // when + var 요청_결과 = 팝업스토어_상세_조회_요청(); + + // then + 팝업스토어_상세_조회_결과_검증(요청_결과); + } + + @Test + void 팝업스토어_업데이트() { + // given + 팝업_스토어_생성(); + var 업데이트_요청서 = 팝업스토어_업데이트_요청서(); + + // when + var 요청_결과 = 팝업스토어_상세_조회_요청(업데이트_요청서); + + // then + 팝업스토어_업데이트_결과_검증(요청_결과); + } +} diff --git a/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerWebMvcTest.java b/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerWebMvcTest.java new file mode 100644 index 00000000..c8dac91c --- /dev/null +++ b/backend/pcloud-api/src/test/java/com/api/popups/presentation/PopupsControllerWebMvcTest.java @@ -0,0 +1,186 @@ +package com.api.popups.presentation; + +import com.api.helper.MockBeanInjection; +import com.api.popups.application.request.PopupsCreateRequest; +import com.api.popups.application.request.PopupsUpdateRequest; +import com.domain.domains.popups.domain.response.PopupsSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static com.api.helper.RestDocsHelper.customDocument; +import static com.api.popups.fixture.request.PopupsRequestFixtures.팝업스토어_생성_요청; +import static com.api.popups.fixture.request.PopupsRequestFixtures.팝업스토어_업데이트_요청; +import static member.fixture.MemberFixture.어드민_멤버_생성_id_없음_kakao_oauth_가입; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static popups.fixture.PopupsSpecificResponseFixture.팝업_스토어_상세조회_결과; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@AutoConfigureRestDocs +@WebMvcTest(PopupsController.class) +class PopupsControllerWebMvcTest extends MockBeanInjection { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void 팝업스토어를_생성한다() throws Exception { + // given + PopupsCreateRequest request = 팝업스토어_생성_요청(); + when(memberRepository.findById(any())).thenReturn(Optional.ofNullable(어드민_멤버_생성_id_없음_kakao_oauth_가입())); + when(popupsService.create(any(), eq(request))).thenReturn(1L); + + // when & then + mockMvc.perform(post("/popups") + .header(AUTHORIZATION, "Bearer tokenInfo ~~") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ).andExpect(status().isCreated()) + .andDo(customDocument("create_popups", + requestHeaders( + headerWithName(AUTHORIZATION).description("유저 토큰 정보") + ), + requestFields( + fieldWithPath("title").description("팝업스토어 이름"), + fieldWithPath("description").description("팝업스토어 내용"), + fieldWithPath("location").description("팝업스토어 열리는 장소"), + fieldWithPath("isParkingAvailable").description("주차 가능 여부"), + fieldWithPath("fee").description("입장 요금 (없다면 0)"), + fieldWithPath("startDate").description("팝업스토어 시작 날짜"), + fieldWithPath("endDate").description("팝업스토어 종료 날짜"), + fieldWithPath("openTimes").description("팝업스토어 운영 시간"), + fieldWithPath("latitude").description("latitude, 위도 정보 (String)"), + fieldWithPath("longitude").description("longitude, 경도 정보 (String)"), + fieldWithPath("publicTag").description("큰 범주 안에서 퍼블릭 태그"), + fieldWithPath("tags").description("업로더가 설정하는 커스텀 태그") + ), + responseHeaders( + headerWithName("location").description("생성된 팝업스토어 redirection URL") + ) + )); + } + + @Test + void 페이징_조회를_한다() throws Exception { + // given + when(popupsQueryService.findAll(any(), any())).thenReturn(List.of(new PopupsSimpleResponse(1L, "빵빵이 전시회", "서울특별시 마포구", LocalDateTime.now().minusDays(30), LocalDateTime.now()))); + + // when & then + mockMvc.perform(get("/popups") + .param("popupsId", "11") + .param("pageSize", "10") + ).andExpect(status().isOk()) + .andDo(customDocument("find_all_popups_with_paging", + queryParameters( + parameterWithName("popupsId").description("마지막으로 받은 popupsId, 맨 처음 조회라면 null 허용"), + parameterWithName("pageSize").description("한 페이지에 조회되는 사이즈") + ), + responseFields( + fieldWithPath("[].id").description("팝업스토어 id"), + fieldWithPath("[].title").description("팝업스토어 이름"), + fieldWithPath("[].location").description("팝업스토어 장소명"), + fieldWithPath("[].startDate").description("팝업스토어 시작일"), + fieldWithPath("[].endDate").description("팝업스토어 종료일") + + ) + )); + } + + @Test + void 팝업스토어_상세조회를_한다() throws Exception { + // given + PopupsSpecificResponse response = 팝업_스토어_상세조회_결과(); + when(popupsQueryService.findById(anyLong())).thenReturn(response); + + // when + mockMvc.perform(get("/popups/{popupsId}", 1) + ).andExpect(status().isOk()) + .andDo(customDocument("find_popups", + pathParameters( + parameterWithName("popupsId").description("팝업스토어 id") + ), + responseFields( + fieldWithPath("id").description("팝업스토어 id"), + fieldWithPath("ownerId").description("팝업스토어 게시글 작성자 id"), + fieldWithPath("title").description("팝업스토어 이름"), + fieldWithPath("description").description("팝업스토어 설명"), + fieldWithPath("location").description("팝업스토어 장소명"), + fieldWithPath("isParkingAvailable").description("주차 가능 여부"), + fieldWithPath("fee").description("팝업스토어 입장요금"), + fieldWithPath("startDate").description("팝업스토어 시작일"), + fieldWithPath("endDate").description("팝업스토어 종료일"), + fieldWithPath("openTimes").description("팝업스토어 운영 시간"), + fieldWithPath("latitude").description("위도"), + fieldWithPath("longitude").description("경도"), + fieldWithPath("publicTag").description("공용 퍼블릭 태그"), + fieldWithPath("tags[]").description("커스텀 태그") + ) + )); + } + + @Test + void 팝업스토어를_업데이트한다() throws Exception { + // given + PopupsUpdateRequest request = 팝업스토어_업데이트_요청(); + doNothing().when(popupsService).patchById(any(), any(), eq(request)); + + // when & then + mockMvc.perform(patch("/popups/{popupsId}", 1) + .header(AUTHORIZATION, "Bearer tokenInfo ~~") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ).andExpect(status().isNoContent()) + .andDo(customDocument("patch_popups", + requestHeaders( + headerWithName(AUTHORIZATION).description("유저 토큰 정보") + ), + requestFields( + fieldWithPath("title").description("팝업스토어 이름"), + fieldWithPath("description").description("팝업스토어 내용"), + fieldWithPath("location").description("팝업스토어 열리는 장소"), + fieldWithPath("isParkingAvailable").description("주차 가능 여부"), + fieldWithPath("fee").description("입장 요금 (없다면 0)"), + fieldWithPath("startDate").description("팝업스토어 시작 날짜"), + fieldWithPath("endDate").description("팝업스토어 종료 날짜"), + fieldWithPath("openTimes").description("팝업스토어 운영 시간"), + fieldWithPath("latitude").description("latitude, 위도 정보 (String)"), + fieldWithPath("longitude").description("longitude, 경도 정보 (String)"), + fieldWithPath("publicTag").description("큰 범주 안에서 퍼블릭 태그"), + fieldWithPath("tags").description("업로더가 설정하는 커스텀 태그") + ) + )); + } +} diff --git a/backend/pcloud-common/src/main/java/com/common/exception/AuthExceptionType.java b/backend/pcloud-common/src/main/java/com/common/exception/AuthExceptionType.java index 6529bc8a..72019cc3 100644 --- a/backend/pcloud-common/src/main/java/com/common/exception/AuthExceptionType.java +++ b/backend/pcloud-common/src/main/java/com/common/exception/AuthExceptionType.java @@ -9,7 +9,9 @@ public enum AuthExceptionType implements CustomExceptionType { TOKEN_INVALID_EXCEPTION(400, "PC0105", "토큰의 값이 유효하지 않습니다."), UNSUPPORTED_TOKEN_EXCEPTION(400, "PC0106", "지원하지 않는 토큰 형식입니다."), REQUEST_FAIL_OF_OAUTH_ACCESS_TOKEN(400, "PC0107", "OAuth 인증 Access-token 발급에 실패하였습니다."), - REQUEST_FAIL_OF_OAUTH_MEMBER_INFO(400, "PC0108", "OAuth 유저 정보 조회에 실패하였습니다."); + REQUEST_FAIL_OF_OAUTH_MEMBER_INFO(400, "PC0108", "OAuth 유저 정보 조회에 실패하였습니다."), + FORBIDDEN_AUTH_LEVEL(403, "PC0109", "접근 권한이 존재하지 않습니다."), + AUTH_NOT_EQUALS_EXCEPTION(401, "PC0110", "본인의 계정으로 진행하는 요청이 아닙니다."); private final int httpStatusCode; private final String customCode; diff --git a/backend/pcloud-common/src/main/java/com/common/annotation/AuthMember.java b/backend/pcloud-domain/src/main/java/com/domain/annotation/AuthMember.java similarity index 89% rename from backend/pcloud-common/src/main/java/com/common/annotation/AuthMember.java rename to backend/pcloud-domain/src/main/java/com/domain/annotation/AuthMember.java index e6fa6bf6..b6bb49f6 100644 --- a/backend/pcloud-common/src/main/java/com/common/annotation/AuthMember.java +++ b/backend/pcloud-domain/src/main/java/com/domain/annotation/AuthMember.java @@ -1,4 +1,4 @@ -package com.common.annotation; +package com.domain.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/backend/pcloud-domain/src/main/java/com/domain/annotation/AuthMembers.java b/backend/pcloud-domain/src/main/java/com/domain/annotation/AuthMembers.java new file mode 100644 index 00000000..b70bcc47 --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/annotation/AuthMembers.java @@ -0,0 +1,15 @@ +package com.domain.annotation; + +import com.domain.domains.member.domain.vo.MemberRole; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthMembers { + + MemberRole[] permit(); +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/config/QuerydslConfig.java b/backend/pcloud-domain/src/main/java/com/domain/config/QuerydslConfig.java index 5d6c666b..8c2c75b5 100644 --- a/backend/pcloud-domain/src/main/java/com/domain/config/QuerydslConfig.java +++ b/backend/pcloud-domain/src/main/java/com/domain/config/QuerydslConfig.java @@ -1,5 +1,6 @@ package com.domain.config; +import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -14,6 +15,6 @@ public class QuerydslConfig { @Bean public JPAQueryFactory jpaQueryFactory() { - return new JPAQueryFactory(entityManager); + return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager); } } diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/common/CustomTagType.java b/backend/pcloud-domain/src/main/java/com/domain/domains/common/CustomTagType.java new file mode 100644 index 00000000..f326a81a --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/common/CustomTagType.java @@ -0,0 +1,7 @@ +package com.domain.domains.common; + +public enum CustomTagType { + + POPUPS, + PERSONAL_EXHIBITION; +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/Tag.java b/backend/pcloud-domain/src/main/java/com/domain/domains/customtag/domain/CustomTag.java similarity index 50% rename from backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/Tag.java rename to backend/pcloud-domain/src/main/java/com/domain/domains/customtag/domain/CustomTag.java index 3ee90133..6d11acfe 100644 --- a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/Tag.java +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/customtag/domain/CustomTag.java @@ -1,7 +1,11 @@ -package com.domain.domains.popups.domain; +package com.domain.domains.customtag.domain; +import com.domain.domains.common.BaseEntity; +import com.domain.domains.common.CustomTagType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -18,7 +22,7 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class Tag { +public class CustomTag extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -26,4 +30,19 @@ public class Tag { @Column(nullable = false) private String name; + + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + private CustomTagType type; + + @Column(nullable = false) + private Long targetId; + + public static CustomTag of(final String name, final CustomTagType type, final Long targetId) { + return CustomTag.builder() + .name(name) + .type(type) + .targetId(targetId) + .build(); + } } diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/customtag/domain/CustomTagRepository.java b/backend/pcloud-domain/src/main/java/com/domain/domains/customtag/domain/CustomTagRepository.java new file mode 100644 index 00000000..877291f5 --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/customtag/domain/CustomTagRepository.java @@ -0,0 +1,15 @@ +package com.domain.domains.customtag.domain; + +import com.domain.domains.common.CustomTagType; + +import java.util.List; +import java.util.Optional; + +public interface CustomTagRepository { + + Optional findByTypeAndTargetId(CustomTagType type, Long targetId); + + List saveAll(List customTags); + + void deleteAllByTypeAndTargetId(CustomTagType type, Long targetId); +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/customtag/infrastructure/CustomTagJpaRepository.java b/backend/pcloud-domain/src/main/java/com/domain/domains/customtag/infrastructure/CustomTagJpaRepository.java new file mode 100644 index 00000000..61bc73fd --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/customtag/infrastructure/CustomTagJpaRepository.java @@ -0,0 +1,14 @@ +package com.domain.domains.customtag.infrastructure; + +import com.domain.domains.common.CustomTagType; +import com.domain.domains.customtag.domain.CustomTag; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CustomTagJpaRepository extends JpaRepository { + + Optional findByTypeAndTargetId(CustomTagType type, Long targetId); + + void deleteAllByTypeAndTargetId(CustomTagType type, Long targetId); +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/Popups.java b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/Popups.java index 22ec064a..abaf14fb 100644 --- a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/Popups.java +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/Popups.java @@ -1,6 +1,8 @@ package com.domain.domains.popups.domain; +import com.common.exception.AuthException; import com.domain.domains.common.BaseEntity; +import com.domain.domains.common.Price; import com.domain.domains.common.PublicTag; import com.domain.domains.popups.domain.vo.AvailableTime; import com.domain.domains.popups.domain.vo.Latitude; @@ -11,12 +13,9 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToMany; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -24,10 +23,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.ArrayList; -import java.util.List; +import java.time.LocalDateTime; -import static jakarta.persistence.CascadeType.ALL; +import static com.common.exception.AuthExceptionType.AUTH_NOT_EQUALS_EXCEPTION; @Getter @Builder @@ -41,6 +39,9 @@ public class Popups extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) + private Long ownerId; + @Embedded private StoreDetails storeDetails; @@ -57,16 +58,52 @@ public class Popups extends BaseEntity { @Column(nullable = false) private PublicTag publicTag; - @JoinColumn(name = "tag_id") - @OneToMany(fetch = FetchType.LAZY, cascade = ALL, orphanRemoval = true) - private List tags = new ArrayList<>(); + public static Popups of( + final Long memberId, + final String title, + final String description, + final String location, + final Boolean isParkingAvailable, + final Integer fee, + final LocalDateTime startDate, + final LocalDateTime endDate, + final String openTimes, + final String latitude, + final String longitude, + final String publicTag + ) { + return Popups.builder() + .ownerId(memberId) + .storeDetails(StoreDetails.builder() + .title(title) + .description(description) + .location(location) + .isParkingAvailable(isParkingAvailable) + .fee(Price.from(fee)) + .build()) + .availableTime(AvailableTime.builder() + .startDate(startDate) + .endDate(endDate) + .openTimes(openTimes) + .build()) + .latitude(Latitude.from(latitude)) + .longitude(Longitude.from(longitude)) + .publicTag(PublicTag.from(publicTag)) + .build(); + } + + public void update(final Popups updatedPopups) { + validateOwnerEquals(updatedPopups.getOwnerId()); + this.storeDetails = updatedPopups.storeDetails; + this.availableTime = updatedPopups.availableTime; + this.latitude = updatedPopups.latitude; + this.longitude = updatedPopups.longitude; + this.publicTag = updatedPopups.publicTag; + } - public void update(final Popups popups) { - this.storeDetails = popups.storeDetails; - this.availableTime = popups.availableTime; - this.latitude = popups.latitude; - this.longitude = popups.longitude; - this.publicTag = popups.publicTag; - this.tags = popups.tags; + private void validateOwnerEquals(final Long ownerId) { + if (!this.getOwnerId().equals(ownerId)) { + throw new AuthException(AUTH_NOT_EQUALS_EXCEPTION); + } } } diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/PopupsRepository.java b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/PopupsRepository.java index 4bd35636..b01f95bd 100644 --- a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/PopupsRepository.java +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/PopupsRepository.java @@ -1,5 +1,9 @@ package com.domain.domains.popups.domain; +import com.domain.domains.popups.domain.response.PopupsSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; + +import java.util.List; import java.util.Optional; public interface PopupsRepository { @@ -7,4 +11,8 @@ public interface PopupsRepository { Optional findById(Long id); Popups save(Popups popups); + + Optional findSpecificById(Long id); + + List findAllWithPaging(Long popupsId, Integer pageSize); } diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/CustomTagSimpleResponse.java b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/CustomTagSimpleResponse.java new file mode 100644 index 00000000..c14d7e75 --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/CustomTagSimpleResponse.java @@ -0,0 +1,6 @@ +package com.domain.domains.popups.domain.response; + +public record CustomTagSimpleResponse( + String name +) { +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/PopupsSimpleResponse.java b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/PopupsSimpleResponse.java new file mode 100644 index 00000000..0f67a3f6 --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/PopupsSimpleResponse.java @@ -0,0 +1,12 @@ +package com.domain.domains.popups.domain.response; + +import java.time.LocalDateTime; + +public record PopupsSimpleResponse( + Long id, + String title, + String location, + LocalDateTime startDate, + LocalDateTime endDate +) { +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/PopupsSpecificResponse.java b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/PopupsSpecificResponse.java new file mode 100644 index 00000000..7ebb81de --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/domain/response/PopupsSpecificResponse.java @@ -0,0 +1,61 @@ +package com.domain.domains.popups.domain.response; + +import com.domain.domains.common.PublicTag; +import lombok.Getter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +public class PopupsSpecificResponse { + + private final Long id; + private final Long ownerId; + private final String title; + private final String description; + private final String location; + private final Boolean isParkingAvailable; + private final BigDecimal fee; + private final LocalDateTime startDate; + private final LocalDateTime endDate; + private final String openTimes; + private final BigDecimal latitude; + private final BigDecimal longitude; + private final String publicTag; + private final List tags; + + public PopupsSpecificResponse( + final Long id, + final Long ownerId, + final String title, + final String description, + final String location, + final Boolean isParkingAvailable, + final BigDecimal fee, + final LocalDateTime startDate, + final LocalDateTime endDate, + final String openTimes, + final BigDecimal latitude, + final BigDecimal longitude, + final PublicTag publicTag, + final List tags + ) { + this.id = id; + this.ownerId = ownerId; + this.title = title; + this.description = description; + this.location = location; + this.isParkingAvailable = isParkingAvailable; + this.fee = fee; + this.startDate = startDate; + this.endDate = endDate; + this.openTimes = openTimes; + this.latitude = latitude; + this.longitude = longitude; + this.publicTag = publicTag.getName(); + this.tags = tags.stream() + .map(CustomTagSimpleResponse::name) + .toList(); + } +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/event/PopupsTagsCreatedEvents.java b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/event/PopupsTagsCreatedEvents.java new file mode 100644 index 00000000..a8e25670 --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/event/PopupsTagsCreatedEvents.java @@ -0,0 +1,12 @@ +package com.domain.domains.popups.event; + +import com.domain.domains.common.CustomTagType; + +import java.util.List; + +public record PopupsTagsCreatedEvents( + Long popupsId, + List tags, + CustomTagType type +) { +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/event/PopupsTagsUpdatedEvents.java b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/event/PopupsTagsUpdatedEvents.java new file mode 100644 index 00000000..04727953 --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/event/PopupsTagsUpdatedEvents.java @@ -0,0 +1,12 @@ +package com.domain.domains.popups.event; + +import com.domain.domains.common.CustomTagType; + +import java.util.List; + +public record PopupsTagsUpdatedEvents( + Long popupsId, + List tags, + CustomTagType type +) { +} diff --git a/backend/pcloud-domain/src/main/java/com/domain/domains/popups/infrastructure/PopupsQueryRepository.java b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/infrastructure/PopupsQueryRepository.java new file mode 100644 index 00000000..8a3e945a --- /dev/null +++ b/backend/pcloud-domain/src/main/java/com/domain/domains/popups/infrastructure/PopupsQueryRepository.java @@ -0,0 +1,82 @@ +package com.domain.domains.popups.infrastructure; + +import com.domain.domains.common.CustomTagType; +import com.domain.domains.popups.domain.response.CustomTagSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +import static com.domain.domains.customtag.domain.QCustomTag.customTag; +import static com.domain.domains.popups.domain.QPopups.popups; +import static com.querydsl.core.group.GroupBy.groupBy; +import static com.querydsl.core.group.GroupBy.list; +import static com.querydsl.core.types.Projections.constructor; + +@RequiredArgsConstructor +@Repository +public class PopupsQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Optional findSpecificById(final Long popupsId) { + List result = jpaQueryFactory.selectFrom(popups) + .where(popups.id.eq(popupsId)) + .leftJoin(customTag).on( + customTag.targetId.eq(popups.id) + .and(customTag.type.eq(CustomTagType.POPUPS)) + ).transform(groupBy(popups.id) + .list(constructor(PopupsSpecificResponse.class, + popups.id, + popups.ownerId, + popups.storeDetails.title, + popups.storeDetails.description, + popups.storeDetails.location, + popups.storeDetails.isParkingAvailable, + popups.storeDetails.fee.value, + popups.availableTime.startDate, + popups.availableTime.endDate, + popups.availableTime.openTimes, + popups.latitude.value, + popups.longitude.value, + popups.publicTag, + list(constructor(CustomTagSimpleResponse.class, + customTag.name + ))) + ) + ); + + if (result.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(result.get(0)); + } + + public List findAllWithPaging(final Long popupsId, final Integer pageSize) { + return jpaQueryFactory.select(constructor(PopupsSimpleResponse.class, + popups.id, + popups.storeDetails.title, + popups.storeDetails.location, + popups.availableTime.startDate, + popups.availableTime.endDate + )).from(popups) + .where(ltPopupsId(popupsId)) + .orderBy(popups.id.desc()) + .limit(pageSize) + .fetch(); + } + + private BooleanExpression ltPopupsId(final Long popupsId) { + if (popupsId == null) { + return null; + } + + return popups.id.lt(popupsId); + } +} diff --git a/backend/pcloud-domain/src/main/resources/application-domain-test.yml b/backend/pcloud-domain/src/main/resources/application-domain-test.yml index 37d866da..51ee3e1f 100644 --- a/backend/pcloud-domain/src/main/resources/application-domain-test.yml +++ b/backend/pcloud-domain/src/main/resources/application-domain-test.yml @@ -1,8 +1,9 @@ spring: datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_LOWER=TRUE + url: jdbc:h2:mem:api;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL username: sa + password: sa + driver-class-name: org.h2.Driver flyway: enabled: false diff --git a/backend/pcloud-domain/src/test/java/com/domain/domains/common/PublicTagTest.java b/backend/pcloud-domain/src/test/java/com/domain/domains/common/PublicCustomTagTest.java similarity index 97% rename from backend/pcloud-domain/src/test/java/com/domain/domains/common/PublicTagTest.java rename to backend/pcloud-domain/src/test/java/com/domain/domains/common/PublicCustomTagTest.java index 2da3654c..608a9d6b 100644 --- a/backend/pcloud-domain/src/test/java/com/domain/domains/common/PublicTagTest.java +++ b/backend/pcloud-domain/src/test/java/com/domain/domains/common/PublicCustomTagTest.java @@ -11,7 +11,7 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class PublicTagTest { +class PublicCustomTagTest { @Test void 관심사를_이름으로_가져온다() { diff --git a/backend/pcloud-domain/src/test/java/com/domain/domains/customtag/infrastructure/CustomTagJpaRepositoryTest.java b/backend/pcloud-domain/src/test/java/com/domain/domains/customtag/infrastructure/CustomTagJpaRepositoryTest.java new file mode 100644 index 00000000..96a1493e --- /dev/null +++ b/backend/pcloud-domain/src/test/java/com/domain/domains/customtag/infrastructure/CustomTagJpaRepositoryTest.java @@ -0,0 +1,55 @@ +package com.domain.domains.customtag.infrastructure; + +import com.domain.domains.customtag.domain.CustomTag; +import com.domain.helper.IntegrationHelper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Optional; + +import static customtag.fixture.CustomTagFixture.팝업_태그_생성_타겟_아이디_1L; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class CustomTagJpaRepositoryTest extends IntegrationHelper { + + @Autowired + private CustomTagJpaRepository customTagJpaRepository; + + @Test + void 타입과_타겟_아이디로_찾는다() { + // given + CustomTag customTag = 팝업_태그_생성_타겟_아이디_1L(); + customTagJpaRepository.save(customTag); + + // when + Optional result = customTagJpaRepository.findByTypeAndTargetId(customTag.getType(), customTag.getTargetId()); + + // then + assertSoftly(softly -> { + softly.assertThat(result).isPresent(); + softly.assertThat(result.get()) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(customTag); + }); + } + + @Test + void 타입과_타겟_아이디로_지운다() { + // given + CustomTag customTag = 팝업_태그_생성_타겟_아이디_1L(); + customTagJpaRepository.save(customTag); + + // when + customTagJpaRepository.deleteAllByTypeAndTargetId(customTag.getType(), customTag.getTargetId()); + + // then + Optional result = customTagJpaRepository.findByTypeAndTargetId(customTag.getType(), customTag.getTargetId()); + assertThat(result).isEmpty(); + } +} diff --git a/backend/pcloud-domain/src/test/java/com/domain/domains/popups/domain/PopupsTest.java b/backend/pcloud-domain/src/test/java/com/domain/domains/popups/domain/PopupsTest.java index e5c5d167..d7e922c6 100644 --- a/backend/pcloud-domain/src/test/java/com/domain/domains/popups/domain/PopupsTest.java +++ b/backend/pcloud-domain/src/test/java/com/domain/domains/popups/domain/PopupsTest.java @@ -1,29 +1,50 @@ package com.domain.domains.popups.domain; +import com.common.exception.AuthException; +import com.common.exception.AuthExceptionType; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static popups.fixture.PopupsFixture.일반_팝업_스토어_생성_뷰티; +import static popups.fixture.PopupsFixture.일반_팝업_스토어_생성_뷰티_유효하지_않은_주인; import static popups.fixture.PopupsFixture.일반_팝업_스토어_생성_펫샵; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class PopupsTest { - @Test - void 팝업_스토어_정보를_업데이트한다() { - // given - Popups popups = 일반_팝업_스토어_생성_펫샵(); - Popups updatePopups = 일반_팝업_스토어_생성_뷰티(); + @Nested + class 팝업스토어_업데이트 { - // when - popups.update(updatePopups); + @Test + void 팝업_스토어_정보를_업데이트한다() { + // given + Popups popups = 일반_팝업_스토어_생성_펫샵(); + Popups updatePopups = 일반_팝업_스토어_생성_뷰티(); - // then - assertThat(popups) - .usingRecursiveComparison() - .isEqualTo(updatePopups); + // when + popups.update(updatePopups); + + // then + assertThat(popups) + .usingRecursiveComparison() + .isEqualTo(updatePopups); + } + + @Test + void 자신의_계정이_아니면_업데이트_못한다() { + // given + Popups popups = 일반_팝업_스토어_생성_뷰티(); + Popups updatedPopups = 일반_팝업_스토어_생성_뷰티_유효하지_않은_주인(); + + // when & then + assertThatThrownBy(() -> popups.update(updatedPopups)) + .isInstanceOf(AuthException.class) + .hasMessageContaining(AuthExceptionType.AUTH_NOT_EQUALS_EXCEPTION.message()); + } } } diff --git a/backend/pcloud-domain/src/test/java/com/domain/domains/popups/infrastructure/PopupsQueryRepositoryTest.java b/backend/pcloud-domain/src/test/java/com/domain/domains/popups/infrastructure/PopupsQueryRepositoryTest.java new file mode 100644 index 00000000..d6a5134e --- /dev/null +++ b/backend/pcloud-domain/src/test/java/com/domain/domains/popups/infrastructure/PopupsQueryRepositoryTest.java @@ -0,0 +1,129 @@ +package com.domain.domains.popups.infrastructure; + +import com.domain.domains.common.Price; +import com.domain.domains.common.PublicTag; +import com.domain.domains.popups.domain.Popups; +import com.domain.domains.popups.domain.response.PopupsSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; +import com.domain.domains.popups.domain.vo.AvailableTime; +import com.domain.domains.popups.domain.vo.Latitude; +import com.domain.domains.popups.domain.vo.Longitude; +import com.domain.domains.popups.domain.vo.StoreDetails; +import com.domain.helper.IntegrationHelper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static popups.fixture.PopupsFixture.일반_팝업_스토어_생성_뷰티; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class PopupsQueryRepositoryTest extends IntegrationHelper { + + @Autowired + private PopupsJpaRepository popupsJpaRepository; + + @Autowired + private PopupsQueryRepository popupsQueryRepository; + + @Test + void 팝업스토어_상세조회를_한다() { + // given + Popups savedPopups = popupsJpaRepository.save(일반_팝업_스토어_생성_뷰티()); + + // when + Optional response = popupsQueryRepository.findSpecificById(savedPopups.getId()); + + // then + assertSoftly(softly -> { + softly.assertThat(response).isPresent(); + softly.assertThat(response.get().getId()).isEqualTo(savedPopups.getId()); + }); + } + + @Test + void no_offset_페이징_첫_조회() { + // given + for (long i = 1L; i <= 20L; i++) { + popupsJpaRepository.save( + Popups.builder() + .id(i) + .ownerId(1L) + .availableTime( + AvailableTime.builder() + .startDate(LocalDateTime.of(2024, 1, 1, 0, 0)) + .endDate(LocalDateTime.of(2024, 12, 31, 0, 0)) + .openTimes("평일 12시 ~ 18시") + .build() + ).storeDetails( + StoreDetails.builder() + .title("펫 케어 팝업스토어") + .description("펫을 케어하는 팝업스토어입니다.") + .location("마포구") + .isParkingAvailable(Boolean.TRUE) + .fee(Price.from(10000)) + .build() + ).latitude(Latitude.from("34")) + .longitude(Longitude.from("128")) + .publicTag(PublicTag.PET) + .build() + ); + } + + // when + List result = popupsQueryRepository.findAllWithPaging(null, 10); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(10); + softly.assertThat(result.get(0).id()).isEqualTo(20L); + softly.assertThat(result.get(9).id()).isEqualTo(11L); + }); + } + + @Test + void no_offset_페이징_두번째_조회() { + // given + for (long i = 1L; i <= 20L; i++) { + popupsJpaRepository.save( + Popups.builder() + .id(i) + .ownerId(1L) + .availableTime( + AvailableTime.builder() + .startDate(LocalDateTime.of(2024, 1, 1, 0, 0)) + .endDate(LocalDateTime.of(2024, 12, 31, 0, 0)) + .openTimes("평일 12시 ~ 18시") + .build() + ).storeDetails( + StoreDetails.builder() + .title("펫 케어 팝업스토어") + .description("펫을 케어하는 팝업스토어입니다.") + .location("마포구") + .isParkingAvailable(Boolean.TRUE) + .fee(Price.from(10000)) + .build() + ).latitude(Latitude.from("34")) + .longitude(Longitude.from("128")) + .publicTag(PublicTag.PET) + .build() + ); + } + + // when + List result = popupsQueryRepository.findAllWithPaging(11L, 10); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(10); + softly.assertThat(result.get(0).id()).isEqualTo(10L); + softly.assertThat(result.get(9).id()).isEqualTo(1L); + }); + } +} diff --git a/backend/pcloud-domain/src/test/resources/application.yml b/backend/pcloud-domain/src/test/resources/application.yml index 37d866da..51ee3e1f 100644 --- a/backend/pcloud-domain/src/test/resources/application.yml +++ b/backend/pcloud-domain/src/test/resources/application.yml @@ -1,8 +1,9 @@ spring: datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_LOWER=TRUE + url: jdbc:h2:mem:api;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL username: sa + password: sa + driver-class-name: org.h2.Driver flyway: enabled: false diff --git a/backend/pcloud-domain/src/testFixtures/java/customtag/fixture/CustomTagFixture.java b/backend/pcloud-domain/src/testFixtures/java/customtag/fixture/CustomTagFixture.java new file mode 100644 index 00000000..a1e9157b --- /dev/null +++ b/backend/pcloud-domain/src/testFixtures/java/customtag/fixture/CustomTagFixture.java @@ -0,0 +1,15 @@ +package customtag.fixture; + +import com.domain.domains.common.CustomTagType; +import com.domain.domains.customtag.domain.CustomTag; + +public class CustomTagFixture { + + public static CustomTag 팝업_태그_생성_타겟_아이디_1L() { + return CustomTag.of("빵빵이", CustomTagType.POPUPS, 1L); + } + + public static CustomTag 팝업_태그_생성_타겟_아이디_수동(final Long targetId) { + return CustomTag.of("빵빵이", CustomTagType.POPUPS, targetId); + } +} diff --git a/backend/pcloud-domain/src/testFixtures/java/member/fixture/FakeMemberRepository.java b/backend/pcloud-domain/src/testFixtures/java/member/FakeMemberRepository.java similarity index 98% rename from backend/pcloud-domain/src/testFixtures/java/member/fixture/FakeMemberRepository.java rename to backend/pcloud-domain/src/testFixtures/java/member/FakeMemberRepository.java index d762022a..a93c6bf3 100644 --- a/backend/pcloud-domain/src/testFixtures/java/member/fixture/FakeMemberRepository.java +++ b/backend/pcloud-domain/src/testFixtures/java/member/FakeMemberRepository.java @@ -1,4 +1,4 @@ -package member.fixture; +package member; import com.domain.domains.member.domain.Member; import com.domain.domains.member.domain.MemberRepository; diff --git a/backend/pcloud-domain/src/testFixtures/java/member/fixture/MemberFixture.java b/backend/pcloud-domain/src/testFixtures/java/member/fixture/MemberFixture.java index 2726f6d1..50d4b4b0 100644 --- a/backend/pcloud-domain/src/testFixtures/java/member/fixture/MemberFixture.java +++ b/backend/pcloud-domain/src/testFixtures/java/member/fixture/MemberFixture.java @@ -20,4 +20,12 @@ public class MemberFixture { .oauthId(new OauthId("1", "KAKAO")) .build(); } + + public static Member 어드민_멤버_생성_id_없음_kakao_oauth_가입() { + return Member.builder() + .email("email@email.com") + .memberRole(MemberRole.ADMIN) + .oauthId(new OauthId("1", "KAKAO")) + .build(); + } } diff --git a/backend/pcloud-domain/src/testFixtures/java/popups/FakePopupsRepository.java b/backend/pcloud-domain/src/testFixtures/java/popups/FakePopupsRepository.java new file mode 100644 index 00000000..a50582ae --- /dev/null +++ b/backend/pcloud-domain/src/testFixtures/java/popups/FakePopupsRepository.java @@ -0,0 +1,82 @@ +package popups; + +import com.domain.domains.popups.domain.Popups; +import com.domain.domains.popups.domain.PopupsRepository; +import com.domain.domains.popups.domain.response.CustomTagSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class FakePopupsRepository implements PopupsRepository { + + private final Map map = new HashMap<>(); + private Long id = 0L; + + @Override + public Optional findById(final Long id) { + return Optional.ofNullable(map.get(id)); + } + + @Override + public Popups save(final Popups popups) { + id++; + + Popups savedPopups = Popups.builder() + .id(id) + .ownerId(popups.getOwnerId()) + .storeDetails(popups.getStoreDetails()) + .availableTime(popups.getAvailableTime()) + .latitude(popups.getLatitude()) + .longitude(popups.getLongitude()) + .publicTag(popups.getPublicTag()) + .build(); + + map.put(id, savedPopups); + return savedPopups; + } + + @Override + public Optional findSpecificById(final Long id) { + if (!map.containsKey(id)) { + return Optional.empty(); + } + + Popups popups = map.get(id); + PopupsSpecificResponse response = new PopupsSpecificResponse( + popups.getId(), + popups.getOwnerId(), + popups.getStoreDetails().getTitle(), + popups.getStoreDetails().getDescription(), + popups.getStoreDetails().getLocation(), + popups.getStoreDetails().getIsParkingAvailable(), + popups.getStoreDetails().getFee().getValue(), + popups.getAvailableTime().getStartDate(), + popups.getAvailableTime().getEndDate(), + popups.getAvailableTime().getOpenTimes(), + popups.getLatitude().getValue(), + popups.getLongitude().getValue(), + popups.getPublicTag(), + List.of(new CustomTagSimpleResponse("빵빵이")) + ); + + return Optional.of(response); + } + + @Override + public List findAllWithPaging(final Long popupsId, final Integer pageSize) { + return List.of( + new PopupsSimpleResponse( + 1L, + "빵빵이 전시회", + "서울시 마포구", + LocalDateTime.now().minusDays(100), + LocalDateTime.now() + ) + ); + } +} diff --git a/backend/pcloud-domain/src/testFixtures/java/popups/fixture/PopupsFixture.java b/backend/pcloud-domain/src/testFixtures/java/popups/fixture/PopupsFixture.java index 657116ec..c4831aae 100644 --- a/backend/pcloud-domain/src/testFixtures/java/popups/fixture/PopupsFixture.java +++ b/backend/pcloud-domain/src/testFixtures/java/popups/fixture/PopupsFixture.java @@ -14,6 +14,7 @@ public class PopupsFixture { public static Popups 일반_팝업_스토어_생성_펫샵() { return Popups.builder() + .ownerId(1L) .availableTime( AvailableTime.builder() .startDate(LocalDateTime.of(2024, 1, 1, 0, 0)) @@ -36,6 +37,30 @@ public class PopupsFixture { public static Popups 일반_팝업_스토어_생성_뷰티() { return Popups.builder() + .ownerId(1L) + .availableTime( + AvailableTime.builder() + .startDate(LocalDateTime.of(2024, 1, 1, 0, 0)) + .endDate(LocalDateTime.of(2024, 12, 31, 0, 0)) + .openTimes("평일 12시 ~ 18시") + .build() + ).storeDetails( + StoreDetails.builder() + .title("뷰티 케어 팝업스토어") + .description("뷰티 케어하는 팝업스토어입니다.") + .location("마포구") + .isParkingAvailable(Boolean.TRUE) + .fee(Price.from(10000)) + .build() + ).latitude(Latitude.from("34")) + .longitude(Longitude.from("128")) + .publicTag(PublicTag.BEAUTY) + .build(); + } + + public static Popups 일반_팝업_스토어_생성_뷰티_유효하지_않은_주인() { + return Popups.builder() + .ownerId(-1L) .availableTime( AvailableTime.builder() .startDate(LocalDateTime.of(2024, 1, 1, 0, 0)) diff --git a/backend/pcloud-domain/src/testFixtures/java/popups/fixture/PopupsSpecificResponseFixture.java b/backend/pcloud-domain/src/testFixtures/java/popups/fixture/PopupsSpecificResponseFixture.java new file mode 100644 index 00000000..f85a684e --- /dev/null +++ b/backend/pcloud-domain/src/testFixtures/java/popups/fixture/PopupsSpecificResponseFixture.java @@ -0,0 +1,36 @@ +package popups.fixture; + +import com.domain.domains.common.PublicTag; +import com.domain.domains.popups.domain.response.CustomTagSimpleResponse; +import com.domain.domains.popups.domain.response.PopupsSpecificResponse; +import com.domain.domains.popups.domain.vo.Latitude; +import com.domain.domains.popups.domain.vo.Longitude; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +public class PopupsSpecificResponseFixture { + + public static PopupsSpecificResponse 팝업_스토어_상세조회_결과() { + return new PopupsSpecificResponse( + 1L, + 1L, + "빵빵이 전시회", + "빵빵이와 함꼐 놀아요", + "서울시 마포구", + true, + BigDecimal.valueOf(0), + LocalDateTime.now().minusMinutes(30), + LocalDateTime.now(), + """ + 평일 09:00 ~ 18:00, + 주말 12:00 ~ 21:00 + """, + Latitude.from("37.556725").getValue(), + Longitude.from("126.9234952").getValue(), + PublicTag.CHARACTER, + List.of(new CustomTagSimpleResponse("빵빵이")) + ); + } +}