diff --git a/src/main/java/treehouse/server/api/invitation/business/InvitationMapper.java b/src/main/java/treehouse/server/api/invitation/business/InvitationMapper.java new file mode 100644 index 0000000..27c7b8c --- /dev/null +++ b/src/main/java/treehouse/server/api/invitation/business/InvitationMapper.java @@ -0,0 +1,34 @@ +package treehouse.server.api.invitation.business; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; +import treehouse.server.api.invitation.presentation.dto.InvitationResponseDTO; +import treehouse.server.global.entity.Invitation.Invitation; +import treehouse.server.global.entity.User.User; +import treehouse.server.global.entity.User.UserRole; +import treehouse.server.global.entity.User.UserStatus; + +import java.util.List; + +@Component +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class InvitationMapper { + + public InvitationResponseDTO.getInvitation toGetInvitation (Invitation invitation, List treeMemberProfileImages) { + return InvitationResponseDTO.getInvitation.builder() + .invitationId(invitation.getId()) + .treehouseName(invitation.getTreeHouse().getName()) + .senderName(invitation.getSender().getName()) + .senderProfileImageUrl(invitation.getSender().getProfileImageUrl()) + .treehouseSize(invitation.getTreeHouse().getMemberList().size()) + .treehouseMemberProfileImages(treeMemberProfileImages) + .build(); + } + public InvitationResponseDTO.getInvitations toGetInvitations(List invitationDtos) { + return InvitationResponseDTO.getInvitations.builder() + .invitations(invitationDtos) + .build(); + } +} + diff --git a/src/main/java/treehouse/server/api/invitation/business/InvitationService.java b/src/main/java/treehouse/server/api/invitation/business/InvitationService.java new file mode 100644 index 0000000..5e2aeaf --- /dev/null +++ b/src/main/java/treehouse/server/api/invitation/business/InvitationService.java @@ -0,0 +1,60 @@ +package treehouse.server.api.invitation.business; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import treehouse.server.api.invitation.implement.InvitationCommandAdapter; +import treehouse.server.api.invitation.implement.InvitationQueryAdapter; +import treehouse.server.api.invitation.presentation.dto.InvitationRequestDTO; +import treehouse.server.api.invitation.presentation.dto.InvitationResponseDTO; +import treehouse.server.global.entity.Invitation.Invitation; +import treehouse.server.global.entity.User.User; +import treehouse.server.global.entity.member.Member; +import treehouse.server.global.entity.redis.RefreshToken; +import treehouse.server.global.entity.treeHouse.TreeHouse; +import treehouse.server.global.exception.GlobalErrorCode; +import treehouse.server.global.exception.ThrowClass.AuthException; +import treehouse.server.global.exception.ThrowClass.GeneralException; +import treehouse.server.global.redis.service.RedisService; +import treehouse.server.global.security.jwt.dto.TokenDTO; +import treehouse.server.global.security.provider.TokenProvider; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +@Slf4j +public class InvitationService { + + + private final InvitationQueryAdapter invitationQueryAdapter; + + private final InvitationCommandAdapter invitationCommandAdapter; + private final InvitationMapper invitationMapper; + private static final Integer treeMemberRandomProfileSize = 3; + + + @Transactional + public InvitationResponseDTO.getInvitations getInvitations(User user) { + + log.error("User name : ", user.getName()); + log.error("User id : ", user.getId()); + List invitations = invitationQueryAdapter.findAllByPhone(user.getPhone()); + log.error("size : ", invitations.size()); + + List invitationDtos = invitations.stream() + .map(invitation -> { + TreeHouse treeHouse = invitation.getTreeHouse(); + List treeMembers = treeHouse.getMemberList(); + List randomProfileImages = treeMembers.stream() + .map(Member::getProfileImageUrl) + .limit(treeMemberRandomProfileSize) + .toList(); + return invitationMapper.toGetInvitation(invitation, randomProfileImages); + }) + .collect(Collectors.toList()); + return invitationMapper.toGetInvitations(invitationDtos); + } +} diff --git a/src/main/java/treehouse/server/api/invitation/implement/InvitationCommandAdapter.java b/src/main/java/treehouse/server/api/invitation/implement/InvitationCommandAdapter.java new file mode 100644 index 0000000..24f3c30 --- /dev/null +++ b/src/main/java/treehouse/server/api/invitation/implement/InvitationCommandAdapter.java @@ -0,0 +1,25 @@ +package treehouse.server.api.invitation.implement; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import treehouse.server.api.invitation.persistence.InvitationRepository; +import treehouse.server.global.annotations.Adapter; +import treehouse.server.global.entity.User.User; +import treehouse.server.global.entity.User.UserRole; +import treehouse.server.global.entity.redis.RefreshToken; +import treehouse.server.global.redis.service.RedisService; +import treehouse.server.global.security.jwt.dto.TokenDTO; +import treehouse.server.global.security.provider.TokenProvider; + +import java.util.List; + +@Adapter +@Slf4j +@RequiredArgsConstructor +public class InvitationCommandAdapter { + + private final InvitationRepository invitationRepository; + + +} diff --git a/src/main/java/treehouse/server/api/invitation/implement/InvitationQueryAdapter.java b/src/main/java/treehouse/server/api/invitation/implement/InvitationQueryAdapter.java new file mode 100644 index 0000000..cdfd2e8 --- /dev/null +++ b/src/main/java/treehouse/server/api/invitation/implement/InvitationQueryAdapter.java @@ -0,0 +1,26 @@ +package treehouse.server.api.invitation.implement; + +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; +import treehouse.server.api.invitation.persistence.InvitationRepository; +import treehouse.server.api.invitation.presentation.dto.InvitationRequestDTO; +import treehouse.server.global.annotations.Adapter; +import treehouse.server.global.entity.Invitation.Invitation; +import treehouse.server.global.entity.User.User; +import treehouse.server.global.exception.GlobalErrorCode; +import treehouse.server.global.exception.ThrowClass.UserException; + +import java.util.List; +import java.util.Optional; + +@Adapter +@RequiredArgsConstructor +public class InvitationQueryAdapter { + + private final InvitationRepository invitationRepository; + + public List findAllByPhone(String phone) { + return invitationRepository.findAllByPhone(phone); + } + +} diff --git a/src/main/java/treehouse/server/api/invitation/persistence/InvitationRepository.java b/src/main/java/treehouse/server/api/invitation/persistence/InvitationRepository.java new file mode 100644 index 0000000..24b4265 --- /dev/null +++ b/src/main/java/treehouse/server/api/invitation/persistence/InvitationRepository.java @@ -0,0 +1,16 @@ +package treehouse.server.api.invitation.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import treehouse.server.global.entity.Invitation.Invitation; +import treehouse.server.global.entity.User.User; +import treehouse.server.global.entity.treeHouse.TreeHouse; + +import java.util.List; +import java.util.Optional; + +public interface InvitationRepository extends JpaRepository { + + List findAllByPhone(String phone); + + Invitation findByPhoneAndTreeHouse(String phone, TreeHouse treeHouse); +} diff --git a/src/main/java/treehouse/server/api/invitation/presentation/InvitationApi.java b/src/main/java/treehouse/server/api/invitation/presentation/InvitationApi.java new file mode 100644 index 0000000..556580c --- /dev/null +++ b/src/main/java/treehouse/server/api/invitation/presentation/InvitationApi.java @@ -0,0 +1,36 @@ +package treehouse.server.api.invitation.presentation; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import treehouse.server.api.invitation.business.InvitationService; +import treehouse.server.api.invitation.presentation.dto.InvitationRequestDTO; +import treehouse.server.api.invitation.presentation.dto.InvitationResponseDTO; +import treehouse.server.global.common.CommonResponse; +import treehouse.server.global.entity.User.User; +import treehouse.server.global.security.handler.annotation.AuthMember; + +@RestController +@RequiredArgsConstructor +@Slf4j +@Validated +@Tag(name = "๐Ÿ˜Ž Invitation API", description = "์ดˆ๋Œ€์žฅ ๊ด€๋ จ API ์ž…๋‹ˆ๋‹ค. ์ดˆ๋Œ€์žฅ ์กฐํšŒ, ์ „์†ก ๋“ฑ์˜ API๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.") +@RequestMapping("/invi") +public class InvitationApi { + + private final InvitationService invitationService; + + @GetMapping("/invitation") + @Operation(summary = "์ดˆ๋Œ€์žฅ ์กฐํšŒ", description = "๋‚ด๊ฐ€ ๋ฐ›์€ ์ดˆ๋Œ€์žฅ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + public CommonResponse getInvitations( + @AuthMember @Parameter(hidden = true) User user + ) { + return CommonResponse.onSuccess(invitationService.getInvitations(user)); + } + + +} diff --git a/src/main/java/treehouse/server/api/invitation/presentation/dto/InvitationRequestDTO.java b/src/main/java/treehouse/server/api/invitation/presentation/dto/InvitationRequestDTO.java new file mode 100644 index 0000000..aeb894a --- /dev/null +++ b/src/main/java/treehouse/server/api/invitation/presentation/dto/InvitationRequestDTO.java @@ -0,0 +1,18 @@ +package treehouse.server.api.invitation.presentation.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + + +public class InvitationRequestDTO { + + + + +} diff --git a/src/main/java/treehouse/server/api/invitation/presentation/dto/InvitationResponseDTO.java b/src/main/java/treehouse/server/api/invitation/presentation/dto/InvitationResponseDTO.java new file mode 100644 index 0000000..685489d --- /dev/null +++ b/src/main/java/treehouse/server/api/invitation/presentation/dto/InvitationResponseDTO.java @@ -0,0 +1,31 @@ +package treehouse.server.api.invitation.presentation.dto; + +import lombok.*; + +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class InvitationResponseDTO { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class getInvitation { + private Long invitationId; + private String treehouseName; + private String senderName; + private String senderProfileImageUrl; + private Integer treehouseSize; + private List treehouseMemberProfileImages; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class getInvitations { + List invitations; + } + +} diff --git a/src/main/java/treehouse/server/api/user/business/UserMapper.java b/src/main/java/treehouse/server/api/user/business/UserMapper.java index edb110c..951058d 100644 --- a/src/main/java/treehouse/server/api/user/business/UserMapper.java +++ b/src/main/java/treehouse/server/api/user/business/UserMapper.java @@ -1,14 +1,28 @@ package treehouse.server.api.user.business; +import jakarta.annotation.PostConstruct; import lombok.*; +import org.springframework.stereotype.Component; import treehouse.server.api.user.presentation.dto.UserResponseDTO; import treehouse.server.global.entity.User.User; import treehouse.server.global.entity.User.UserRole; import treehouse.server.global.entity.User.UserStatus; -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Component +//@NoArgsConstructor(access = AccessLevel.PRIVATE) +@RequiredArgsConstructor public class UserMapper { + private final UserService userService; + private static UserService staticUserService; + @PostConstruct + public void init(){ + this.staticUserService = this.userService; + } + + public static User toUserSecurity(String id){ + return staticUserService.findById(Long.valueOf(id)); + } public static UserResponseDTO.checkName toCheckNameDTO(boolean isDuplicated){ return UserResponseDTO.checkName.builder() diff --git a/src/main/java/treehouse/server/api/user/business/UserService.java b/src/main/java/treehouse/server/api/user/business/UserService.java index bc2af6b..930bec7 100644 --- a/src/main/java/treehouse/server/api/user/business/UserService.java +++ b/src/main/java/treehouse/server/api/user/business/UserService.java @@ -36,6 +36,11 @@ public UserResponseDTO.checkName checkName(UserRequestDTO.checkName request){ return UserMapper.toCheckNameDTO(userQueryAdapter.checkName(request)); } + @Transactional(readOnly = true) + public User findById(Long id){ + return userQueryAdapter.findById(id); + } + @Transactional public UserResponseDTO.registerUser register(UserRequestDTO.registerUser request){ User user = UserMapper.toUser(request.getUserName(), request.getPhoneNumber()); diff --git a/src/main/java/treehouse/server/api/user/implement/UserQueryAdapter.java b/src/main/java/treehouse/server/api/user/implement/UserQueryAdapter.java index 69a854c..6ba9e18 100644 --- a/src/main/java/treehouse/server/api/user/implement/UserQueryAdapter.java +++ b/src/main/java/treehouse/server/api/user/implement/UserQueryAdapter.java @@ -33,4 +33,8 @@ public Optional findByPhoneNumber(String phone){ public User findById(Long id){ return userRepository.findById(id).orElseThrow(()->new UserException(GlobalErrorCode.MEMBER_NOT_FOUND)); } + + public Optional optionalUserFindById(Long id){ + return userRepository.findById(id); + } } diff --git a/src/main/java/treehouse/server/global/config/GlobalWebConfig.java b/src/main/java/treehouse/server/global/config/GlobalWebConfig.java new file mode 100644 index 0000000..0282625 --- /dev/null +++ b/src/main/java/treehouse/server/global/config/GlobalWebConfig.java @@ -0,0 +1,27 @@ +package treehouse.server.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import treehouse.server.global.security.handler.annotation.resolver.AuthMemberArgumentResolver; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class GlobalWebConfig implements WebMvcConfigurer { + + private final AuthMemberArgumentResolver authMemberArgumentResolver; + + /** + * ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์˜ ํŠน์ • ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง€์›ํ•˜๋Š” ์ปค์Šคํ…€ํ•œ ArgumentResolver๋ฅผ ์ถ”๊ฐ€ + * @param resolverList + */ + @Override + public void addArgumentResolvers(List resolverList) { + resolverList.add(authMemberArgumentResolver); + } +} diff --git a/src/main/java/treehouse/server/global/config/SwaggerConfig.java b/src/main/java/treehouse/server/global/config/SwaggerConfig.java index 9399b64..d475f89 100644 --- a/src/main/java/treehouse/server/global/config/SwaggerConfig.java +++ b/src/main/java/treehouse/server/global/config/SwaggerConfig.java @@ -1,7 +1,10 @@ package treehouse.server.global.config; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,8 +18,21 @@ public OpenAPI SpringCodeBaseAPI() { .description("Treehouse API ๋ช…์„ธ์„œ") .version("1.0.0"); + String jwtSchemeName = "JWT TOKEN"; + // API ์š”์ฒญํ—ค๋”์— ์ธ์ฆ์ •๋ณด ํฌํ•จ + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); + // SecuritySchemes ๋“ฑ๋ก + Components components = new Components() + .addSecuritySchemes(jwtSchemeName, new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT")); + return new OpenAPI() .addServersItem(new Server().url("/")) - .info(info); + .info(info) + .addSecurityItem(securityRequirement) + .components(components); } } diff --git a/src/main/java/treehouse/server/global/entity/Invitation/Invitation.java b/src/main/java/treehouse/server/global/entity/Invitation/Invitation.java index b44b702..6e5fea5 100644 --- a/src/main/java/treehouse/server/global/entity/Invitation/Invitation.java +++ b/src/main/java/treehouse/server/global/entity/Invitation/Invitation.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import lombok.*; +import treehouse.server.global.entity.User.User; import treehouse.server.global.entity.common.BaseDateTimeEntity; import treehouse.server.global.entity.member.Member; import treehouse.server.global.entity.treeHouse.TreeHouse; @@ -29,6 +30,10 @@ public class Invitation extends BaseDateTimeEntity { @JoinColumn(name = "senderId") @ManyToOne(fetch = FetchType.LAZY) private Member sender; + + @JoinColumn(name = "receiverId") + @ManyToOne(fetch = FetchType.LAZY) + private User receiver; @JoinColumn(name = "treeId") @ManyToOne(fetch = FetchType.LAZY) private TreeHouse treeHouse; diff --git a/src/main/java/treehouse/server/global/security/handler/annotation/AuthMember.java b/src/main/java/treehouse/server/global/security/handler/annotation/AuthMember.java index aa8e790..d13bbfd 100644 --- a/src/main/java/treehouse/server/global/security/handler/annotation/AuthMember.java +++ b/src/main/java/treehouse/server/global/security/handler/annotation/AuthMember.java @@ -1,4 +1,11 @@ package treehouse.server.global.security.handler.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) // ๋Ÿฐํƒ€์ž„ ์ค‘์— ์–ด๋…ธํ…Œ์ด์…˜ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๊ณ  ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ • +@Target(ElementType.PARAMETER) // ์–ด๋…ธํ…Œ์ด์…˜์„ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋งŒ ์ ์šฉ public @interface AuthMember { } diff --git a/src/main/java/treehouse/server/global/security/handler/annotation/resolver/AuthMemberArgumentResolver.java b/src/main/java/treehouse/server/global/security/handler/annotation/resolver/AuthMemberArgumentResolver.java new file mode 100644 index 0000000..5b91c89 --- /dev/null +++ b/src/main/java/treehouse/server/global/security/handler/annotation/resolver/AuthMemberArgumentResolver.java @@ -0,0 +1,62 @@ +package treehouse.server.global.security.handler.annotation.resolver; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import treehouse.server.api.user.business.UserMapper; +import treehouse.server.api.user.business.UserService; +import treehouse.server.global.entity.User.User; +import treehouse.server.global.exception.GlobalErrorCode; +import treehouse.server.global.exception.ThrowClass.GeneralException; +import treehouse.server.global.security.handler.annotation.AuthMember; + +@Component +@RequiredArgsConstructor +public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver { + + /** + * supportsParameter + * - ํ•ด๋‹น ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๋ฐ˜ํ™˜ + * - AuthMember ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜, Member ํƒ€์ž…์ด ์•„๋‹Œ ๊ฒฝ์šฐ false ๋ฐ˜ํ™˜ + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + AuthMember authUser = parameter.getParameterAnnotation(AuthMember.class); // ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ @AuthMember ์ฐพ๊ธฐ + if (authUser == null) return false; + if (parameter.getParameterType().equals(User.class) == false) { + return false; + } + return true; + } + + /** + * resolveArgument + * ์‹ค์ œ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ๊ฐ’์„ ํ•ด์„ํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ + * ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ „๋‹ฌํ•  ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ + * - SecurityContextHolder์—์„œ ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์™€์„œ User ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ + */ + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + Object principal = null; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null) { + principal = authentication.getPrincipal(); + } + if (principal == null || principal.getClass() == String.class) { //Authentication ๊ฐ์ฒด๊ฐ€ null์ด๊ฑฐ๋‚˜, principal์ด String ํƒ€์ž…('anonymousUser')์ธ ๊ฒฝ์šฐ + throw new GeneralException(GlobalErrorCode.MEMBER_NOT_FOUND); + } + + UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication; + + User user = UserMapper.toUserSecurity(authenticationToken.getName()); + return user; + } +}