Skip to content

Commit

Permalink
feat: 팝업스토어 및 태그 구현 (#13)
Browse files Browse the repository at this point in the history
* feat: 여러 권한이 필요한 API에 대해 인증 커스텀 어노테이션 생성

* feat: 팝업스토어 관련 뼈대 CRUD 기능 생성

* refactor: 테스트 클래스 네이밍 변경

* test: 테스트 추가

* refactor: 테스트 격리가 안되는 문제 해결

* test: 인수테스트 추가

* refactor: 잘못된 yml 설정파일 수정

* refactor: 태그 간접참조 방식으로 분리 (활용 가능성으로)

* refactor: 테스트 패키지 변경

* refactor: 불필요한 주석 제거

* refactor: 줄바꿈 변경

* refactor: 정적 팩토리 메서드 추가

* refactor: 개행 변경

* refactor: 개행 변경
  • Loading branch information
sosow0212 authored Aug 14, 2024
1 parent d7f52dc commit a02910e
Show file tree
Hide file tree
Showing 50 changed files with 1,753 additions and 56 deletions.
10 changes: 5 additions & 5 deletions backend/pcloud-api/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
78 changes: 78 additions & 0 deletions backend/pcloud-api/docs/asciidoc/popups.adoc
Original file line number Diff line number Diff line change
@@ -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[]
Original file line number Diff line number Diff line change
@@ -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<CustomTag> findByTypeAndTargetId(final CustomTagType type, final Long targetId) {
return customTagJpaRepository.findByTypeAndTargetId(type, targetId);
}

@Override
public List<CustomTag> saveAll(final List<CustomTag> customTags) {
return customTagJpaRepository.saveAll(customTags);
}

@Override
public void deleteAllByTypeAndTargetId(final CustomTagType type, final Long targetId) {
customTagJpaRepository.deleteAllByTypeAndTargetId(type, targetId);
}
}
Original file line number Diff line number Diff line change
@@ -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<CustomTag> customTags = getPopupsCustomTag(event.tags(), event.type(), event.popupsId());
customTagRepository.saveAll(customTags);
}

private List<CustomTag> getPopupsCustomTag(
final List<String> 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<CustomTag> customTags = getPopupsCustomTag(event.tags(), event.type(), event.popupsId());
customTagRepository.deleteAllByTypeAndTargetId(event.type(), event.popupsId());
customTagRepository.saveAll(customTags);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authArgumentResolver);
resolvers.add(authMemberArgumentResolver);
resolvers.add(authMembersArgumentResolver);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,7 +14,7 @@

@RequiredArgsConstructor
@Component
public class AuthArgumentResolver implements HandlerMethodArgumentResolver {
public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver {

private static final int ANONYMOUS = -1;

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MemberRole> 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<MemberRole> getPermittedRoles(final MethodParameter parameter) {
AuthMembers auths = parameter.getParameterAnnotation(AuthMembers.class);
requireNonNull(auths);
return Arrays.asList(auths.permit());
}
}
Original file line number Diff line number Diff line change
@@ -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<PopupsSimpleResponse> findAll(final Long popupsId, final Integer pageSize) {
return popupsRepository.findAllWithPaging(popupsId, pageSize);
}
}
Loading

0 comments on commit a02910e

Please sign in to comment.