diff --git a/src/main/java/com/api/trip/common/security/config/SecurityConfig.java b/src/main/java/com/api/trip/common/security/config/SecurityConfig.java index e071602..72ae929 100644 --- a/src/main/java/com/api/trip/common/security/config/SecurityConfig.java +++ b/src/main/java/com/api/trip/common/security/config/SecurityConfig.java @@ -3,7 +3,6 @@ import com.api.trip.common.security.jwt.JwtTokenFilter; import com.api.trip.common.security.jwt.JwtTokenProvider; import com.api.trip.common.security.oauth.CustomOAuth2UserService; -import com.api.trip.common.security.oauth.OAuthFailureHandler; import com.api.trip.common.security.oauth.OAuthSuccessHandler; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -18,17 +17,15 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; - -@RequiredArgsConstructor @Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) +@RequiredArgsConstructor public class SecurityConfig { private final JwtTokenProvider jwtTokenProvider; private final CustomOAuth2UserService customOAuth2UserService; private final OAuthSuccessHandler OAuthSuccessHandler; - private final OAuthFailureHandler OAuthFailureHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ @@ -43,11 +40,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ .addHeaderWriter(new XFrameOptionsHeaderWriter( XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)) ) -// .oauth2Login(oauth -> oauth -// .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(customOAuth2UserService)) -// .successHandler(OAuthSuccessHandler) -// .failureHandler(OAuthFailureHandler) -// ) + .oauth2Login(oauth -> oauth + .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint + .userService(customOAuth2UserService) + ) + .successHandler(OAuthSuccessHandler) + ) .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) .build(); } @@ -56,4 +54,5 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } + } diff --git a/src/main/java/com/api/trip/common/security/oauth/CustomOAuth2UserService.java b/src/main/java/com/api/trip/common/security/oauth/CustomOAuth2UserService.java index 7784963..a4f68f8 100644 --- a/src/main/java/com/api/trip/common/security/oauth/CustomOAuth2UserService.java +++ b/src/main/java/com/api/trip/common/security/oauth/CustomOAuth2UserService.java @@ -1,8 +1,5 @@ package com.api.trip.common.security.oauth; -import com.api.trip.domain.member.model.Member; -import com.api.trip.domain.member.repository.MemberRepository; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; @@ -15,37 +12,23 @@ import java.util.Collections; import java.util.Map; -import java.util.Optional; @Slf4j @Service -@RequiredArgsConstructor public class CustomOAuth2UserService implements OAuth2UserService { - private final MemberRepository memberRepository; - @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); - String oauthType = userRequest.getClientRegistration().getRegistrationId(); + String registrationId = userRequest.getClientRegistration().getRegistrationId(); String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); - OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(oauthType, userNameAttributeName, oAuth2User.getAttributes()); - Map customAttributeMap = oAuth2Attribute.convertToMap(); - - String email = (String) customAttributeMap.get("email"); - Optional findMember = memberRepository.findByEmail(email); - - boolean exist = findMember.isPresent(); - String authority = exist ? findMember.get().getRole().getValue() : "ROLE_MEMBER"; - - log.debug("회원 존재 여부: {}", exist); - log.debug("회원 권한 여부: {}", authority); + OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); + Map memberAttribute = oAuth2Attribute.convertToMap(); - customAttributeMap.put("exist", exist); - return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(authority)), customAttributeMap, userNameAttributeName); + return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_MEMBER")), + memberAttribute, "email"); } } diff --git a/src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java b/src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java index 5cace48..2e976e5 100644 --- a/src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java +++ b/src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java @@ -15,53 +15,54 @@ public class OAuth2Attribute { private Map attributes; // 소셜 로그인 사용자의 속성 정보를 담는 Map - private String userNameAttributeName; // 사용자 속성의 키 값 + private String attributeKey; // 사용자 속성의 키 값 private String email; // 이메일 - private String nickname; // 이름 - private String profileImg; // 프로필 사진 - private String oauthType; // 제공자 정보 + private String name; // 이름 + private String picture; // 프로필 사진 - static OAuth2Attribute of(String oauthType, String userNameAttributeName, Map attributes) { - return switch (oauthType) { - case "kakao" -> kakao(oauthType, userNameAttributeName, attributes); - case "google" -> google(oauthType, userNameAttributeName, attributes); - case "naver" -> naver(oauthType, userNameAttributeName, attributes); + static OAuth2Attribute of(String provider, String attributeKey, Map attributes) { + // 각 플랫폼 별로 제공해주는 데이터가 조금씩 다르기 때문에 분기 처리함. + return switch (provider) { + case "google" -> google(attributeKey, attributes); + case "kakao" -> kakao(attributeKey, attributes); + case "naver" -> naver(attributeKey, attributes); default -> throw new RuntimeException(); }; } + private static OAuth2Attribute google(String attributeKey, Map attributes) { + log.debug("google: {}", attributes); + return OAuth2Attribute.builder() + .email((String) attributes.get("email")) + .name((String) attributes.get("name")) + .picture((String)attributes.get("picture")) + .attributes(attributes) + .attributeKey(attributeKey) + .build(); + } - private static OAuth2Attribute kakao(String oauthType, String userNameAttributeName, Map attributes) { + private static OAuth2Attribute kakao(String attributeKey, Map attributes) { Map kakaoAccount = (Map) attributes.get("kakao_account"); Map kakaoProfile = (Map) kakaoAccount.get("profile"); return OAuth2Attribute.builder() - .userNameAttributeName(userNameAttributeName) + .email("KAKAO_" + (String) kakaoAccount.get("email")) + .name((String) kakaoProfile.get("nickname")) + .picture((String) kakaoProfile.get("profile_image_url")) .attributes(kakaoAccount) - .email((String) kakaoAccount.get("email")) - .nickname((String) kakaoProfile.get("nickname")) - .profileImg((String) kakaoProfile.get("profile_image_url")) - .oauthType(oauthType) - .build(); - } - - private static OAuth2Attribute google(String oauthType, String userNameAttributeName, Map attributes) { - return OAuth2Attribute.builder() - .userNameAttributeName(userNameAttributeName) - .attributes(attributes) - .email((String) attributes.get("email")) - .oauthType(oauthType) + .attributeKey(attributeKey) .build(); } - private static OAuth2Attribute naver(String oauthType, String userNameAttributeName, Map attributes) { + private static OAuth2Attribute naver(String attributeKey, Map attributes) { Map response = (Map) attributes.get("response"); return OAuth2Attribute.builder() .email((String) response.get("email")) + .name((String) response.get("nickname")) + .picture((String) response.get("profile_image")) .attributes(response) - .oauthType(oauthType) - .userNameAttributeName(userNameAttributeName) + .attributeKey(attributeKey) .build(); } @@ -69,12 +70,11 @@ private static OAuth2Attribute naver(String oauthType, String userNameAttributeN // OAuth2Attribute -> Map public Map convertToMap() { Map map = new HashMap<>(); - - map.put("id", userNameAttributeName); + map.put("id", attributeKey); + map.put("key", attributeKey); map.put("email", email); - map.put("nickname", nickname); - map.put("profileImg", profileImg); - map.put("oauthType", oauthType); + map.put("name", name); + map.put("picture", picture); return map; } diff --git a/src/main/java/com/api/trip/common/security/oauth/OAuthFailureHandler.java b/src/main/java/com/api/trip/common/security/oauth/OAuthFailureHandler.java deleted file mode 100644 index f0aaaf5..0000000 --- a/src/main/java/com/api/trip/common/security/oauth/OAuthFailureHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.api.trip.common.security.oauth; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -@Slf4j -@Component -public class OAuthFailureHandler implements AuthenticationFailureHandler { - - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { - response.sendRedirect("http://localhost:8080/"); - } -} diff --git a/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java b/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java index c41d8c3..c5130af 100644 --- a/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java +++ b/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java @@ -1,63 +1,82 @@ package com.api.trip.common.security.oauth; +import com.api.trip.common.security.jwt.JwtToken; import com.api.trip.common.security.jwt.JwtTokenProvider; -import jakarta.servlet.ServletException; +import com.api.trip.domain.member.controller.dto.LoginResponse; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.model.MemberRole; +import com.api.trip.domain.member.repository.MemberRepository; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Optional; @Slf4j @Component @RequiredArgsConstructor public class OAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; + private final ObjectMapper objectMapper; @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); - log.debug("OAuth2User: {}", oAuth2User); String email = oAuth2User.getAttribute("email"); + Optional findMember = memberRepository.findByEmail(email); - String role = oAuth2User.getAuthorities().stream() - .findFirst() - .orElseThrow(IllegalAccessError::new) - .getAuthority(); - - /** 미구현 - if (isExist) { - JwtToken jwtToken = jwtTokenProvider.createJwtToken(email, role); - log.debug("JWT TOKEN: {} {}", jwtToken.getAccessToken(), jwtToken.getRefreshToken()); - - String targetUrl = UriComponentsBuilder.fromUriString("/") - .queryParam("accessToken", jwtToken.getAccessToken()) - .queryParam("refreshToken", jwtToken.getRefreshToken()) - .build() - .encode(StandardCharsets.UTF_8) - .toUriString(); - - getRedirectStrategy().sendRedirect(request, response, targetUrl); - } else { - // 회원이 존재하는 않는 경우 회원 가입 후 토큰 발급 - - String targetUrl = UriComponentsBuilder.fromUriString("/") - .queryParam("email", email) - .queryParam("name", nickname) - .build() - .encode(StandardCharsets.UTF_8) - .toUriString(); - - getRedirectStrategy().sendRedirect(request, response, targetUrl); + // 회원이 아닌 경우에 회원 가입 진행 + + Long memberId = 0L; + String role = ""; + + if (findMember.isEmpty()) { + String name = oAuth2User.getAttribute("name"); + String picture = oAuth2User.getAttribute("picture"); + + Member member = Member.of(email, "", name, picture); + memberRepository.save(member); + + memberId = member.getId(); + role = member.getRole().getValue(); } - */ + + // OAuth2User 객체에서 권한 가져옴 + JwtToken jwtToken = jwtTokenProvider.createJwtToken(email, role); + + // 쿠키 세팅 + response.addHeader(HttpHeaders.SET_COOKIE, createCookie("tokenType", "Bearer")); + response.addHeader(HttpHeaders.SET_COOKIE, createCookie("accessToken", jwtToken.getAccessToken())); + response.addHeader(HttpHeaders.SET_COOKIE, createCookie("refreshToken", jwtToken.getRefreshToken())); + response.addHeader(HttpHeaders.SET_COOKIE, createCookie("memberId", String.valueOf(memberId))); + response.sendRedirect("/home"); + } + + private static String createCookie(String name, String value) { + return ResponseCookie.from(name, value) + .path("/") + .httpOnly(true) + .sameSite("None") + .secure(true) + .build() + .toString(); } + } diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java b/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java index 7a7629d..769b650 100644 --- a/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java +++ b/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java @@ -1,7 +1,5 @@ package com.api.trip.domain.interestitem.controller.dto; -import com.api.trip.domain.interestitem.InterestItem; -import com.api.trip.domain.member.model.Member; import lombok.Getter; @Getter diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java index ad91aae..33f1a7f 100644 --- a/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java +++ b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java @@ -1,9 +1,8 @@ package com.api.trip.domain.interestitem.controller.dto; -import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.interestitem.model.InterestItem; import com.api.trip.domain.item.controller.dto.GetItemResponse; import com.api.trip.domain.item.controller.dto.GetItemsResponse; -import com.api.trip.domain.item.model.Item; import lombok.Builder; import lombok.Getter; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/api/trip/domain/interestitem/InterestItem.java b/src/main/java/com/api/trip/domain/interestitem/model/InterestItem.java similarity index 94% rename from src/main/java/com/api/trip/domain/interestitem/InterestItem.java rename to src/main/java/com/api/trip/domain/interestitem/model/InterestItem.java index 7c08783..2ec336a 100644 --- a/src/main/java/com/api/trip/domain/interestitem/InterestItem.java +++ b/src/main/java/com/api/trip/domain/interestitem/model/InterestItem.java @@ -1,4 +1,4 @@ -package com.api.trip.domain.interestitem; +package com.api.trip.domain.interestitem.model; import com.api.trip.common.auditing.entity.BaseTimeEntity; import com.api.trip.domain.item.model.Item; diff --git a/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java b/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java index 9641209..7ecfe1e 100644 --- a/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java +++ b/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java @@ -1,7 +1,6 @@ package com.api.trip.domain.interestitem.repository; -import com.api.trip.domain.interestitem.InterestItem; -import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.interestitem.model.InterestItem; import com.api.trip.domain.member.model.Member; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java b/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java index bbabeb0..6eaeef9 100644 --- a/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java +++ b/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java @@ -1,10 +1,9 @@ package com.api.trip.domain.interestitem.service; -import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.interestitem.model.InterestItem; import com.api.trip.domain.interestitem.controller.dto.CreateInterestItemRequest; import com.api.trip.domain.interestitem.controller.dto.GetInterestItemsResponse; import com.api.trip.domain.interestitem.repository.InterestItemRepository; -import com.api.trip.domain.item.controller.dto.CreateItemRequest; import com.api.trip.domain.item.controller.dto.GetItemsResponse; import com.api.trip.domain.item.model.Item; import com.api.trip.domain.item.repository.ItemRepository; @@ -17,8 +16,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.stream.Collectors; - @Service @Transactional @RequiredArgsConstructor diff --git a/src/main/java/com/api/trip/domain/item/controller/ItemController.java b/src/main/java/com/api/trip/domain/item/controller/ItemController.java index ce9ff5d..6aed3c3 100644 --- a/src/main/java/com/api/trip/domain/item/controller/ItemController.java +++ b/src/main/java/com/api/trip/domain/item/controller/ItemController.java @@ -10,6 +10,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequestMapping("/api/items") @RequiredArgsConstructor @@ -31,10 +33,18 @@ public ResponseEntity getItem(@PathVariable Long ItemId) { public ResponseEntity getItems( @PageableDefault(size = 8) Pageable pageable, @RequestParam int sortCode, - @RequestParam String search + @RequestParam String search, + @RequestParam List tagNames ) { + GetItemsResponse itemsDetail; + + if(tagNames.size() == 0) { + itemsDetail = itemService.getItemsDetail(pageable, sortCode, search); + } + else + itemsDetail = itemService.getItemsDetailByTag(pageable, sortCode, tagNames); - return ResponseEntity.ok(itemService.getItemsDetail(pageable, sortCode, search)); + return ResponseEntity.ok(itemsDetail); } diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java b/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java index d81f1eb..68cddb9 100644 --- a/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java +++ b/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java @@ -5,6 +5,8 @@ import jakarta.persistence.Column; import lombok.Getter; +import java.util.List; + @Getter public class CreateItemRequest { @@ -21,6 +23,8 @@ public class CreateItemRequest { private long minPrice; + private List tagNames; + public Item toEntity(Member writer){ return Item.builder() .productId(productId) diff --git a/src/main/java/com/api/trip/domain/item/service/ItemService.java b/src/main/java/com/api/trip/domain/item/service/ItemService.java index e4b7018..e379de2 100644 --- a/src/main/java/com/api/trip/domain/item/service/ItemService.java +++ b/src/main/java/com/api/trip/domain/item/service/ItemService.java @@ -6,6 +6,7 @@ import com.api.trip.domain.item.controller.dto.GetItemsResponse; import com.api.trip.domain.item.model.Item; import com.api.trip.domain.item.repository.ItemRepository; +import com.api.trip.domain.itemtag.service.ItemTagService; import com.api.trip.domain.member.model.Member; import com.api.trip.domain.member.service.MemberService; import lombok.RequiredArgsConstructor; @@ -14,6 +15,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @Transactional @RequiredArgsConstructor @@ -21,10 +24,12 @@ public class ItemService { private final MemberService memberService; private final ItemRepository itemRepository; + private final ItemTagService itemTagService; public Long createItem(CreateItemRequest itemRequest) { Member member = memberService.getAuthenticationMember(); Item item = itemRequest.toEntity(member); + itemTagService.createItemTag(item, itemRequest.getTagNames()); return itemRepository.save(item).getId(); } @@ -46,11 +51,16 @@ public GetItemResponse getItemDetail(Long ItemId) { @Transactional(readOnly = true) public GetItemsResponse getItemsDetail(Pageable pageable, int sortCode, String search) { Page itemPage = itemRepository.findItems(pageable, sortCode, search); - itemPage.getContent(); return GetItemsResponse.of(itemPage); } + @Transactional(readOnly = true) + public GetItemsResponse getItemsDetailByTag(Pageable pageable, int sortCode, List tagNames) { + Page itemsByTag = itemTagService.getItemsByTag(pageable, sortCode, tagNames); + + return GetItemsResponse.of(itemsByTag); + } public void deleteItem(Long ItemId) { Member member = memberService.getAuthenticationMember(); diff --git a/src/main/java/com/api/trip/domain/itemtag/model/ItemTag.java b/src/main/java/com/api/trip/domain/itemtag/model/ItemTag.java new file mode 100644 index 0000000..00927db --- /dev/null +++ b/src/main/java/com/api/trip/domain/itemtag/model/ItemTag.java @@ -0,0 +1,35 @@ +package com.api.trip.domain.itemtag.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.tag.model.Tag; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ItemTag extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") + private Tag tag; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; + + @Builder + private ItemTag(Tag tag, Item item) { + this.tag = tag; + this.item = item; + } +} diff --git a/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepository.java b/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepository.java new file mode 100644 index 0000000..f7b01f0 --- /dev/null +++ b/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepository.java @@ -0,0 +1,7 @@ +package com.api.trip.domain.itemtag.repository; + +import com.api.trip.domain.itemtag.model.ItemTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ItemTagRepository extends JpaRepository, ItemTagRepositoryCustom { +} diff --git a/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepositoryCustom.java b/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepositoryCustom.java new file mode 100644 index 0000000..fccfa38 --- /dev/null +++ b/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepositoryCustom.java @@ -0,0 +1,12 @@ +package com.api.trip.domain.itemtag.repository; + +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.tag.model.Tag; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface ItemTagRepositoryCustom { + Page findItemsByTags(Pageable pageable, int sortCode, List tags); +} diff --git a/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepositoryCustomImpl.java new file mode 100644 index 0000000..28a57f5 --- /dev/null +++ b/src/main/java/com/api/trip/domain/itemtag/repository/ItemTagRepositoryCustomImpl.java @@ -0,0 +1,70 @@ +package com.api.trip.domain.itemtag.repository; + +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.itemtag.model.ItemTag; +import com.api.trip.domain.tag.model.Tag; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static com.api.trip.domain.item.model.QItem.item; +import static com.api.trip.domain.itemtag.model.QItemTag.itemTag; + +public class ItemTagRepositoryCustomImpl implements ItemTagRepositoryCustom{ + + private final JPAQueryFactory jpaQueryFactory; + + public ItemTagRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public Page findItemsByTags(Pageable pageable, int sortCode, List tags) { + List result = jpaQueryFactory.selectFrom(itemTag) + .where( + itemTag.tag.in(tags) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(orderBySortCode(sortCode)) + .fetch(); + + + List items = result.stream().map(ItemTag::getItem).collect(Collectors.toList()); + + JPAQuery countQuery = jpaQueryFactory.select(item.count()) + .from(item) + .where( + item.isDeleted.eq(false) + ); + + return PageableExecutionUtils.getPage(items, pageable, countQuery::fetchOne); + } + + private OrderSpecifier[] orderBySortCode(int sortCode) { + List orderSpecifiers = new ArrayList<>(); + + + switch (sortCode) + { + case 1: + orderSpecifiers.add(new OrderSpecifier(Order.ASC, item.id)); + case 2: + orderSpecifiers.add(new OrderSpecifier(Order.DESC, item.likeCount)); + case 3: + orderSpecifiers.add(new OrderSpecifier(Order.ASC, item.likeCount)); + + } + orderSpecifiers.add(new OrderSpecifier(Order.DESC, item.id)); + return orderSpecifiers.toArray(new OrderSpecifier[orderSpecifiers.size()]); + } +} diff --git a/src/main/java/com/api/trip/domain/itemtag/service/ItemTagService.java b/src/main/java/com/api/trip/domain/itemtag/service/ItemTagService.java new file mode 100644 index 0000000..ba7e6eb --- /dev/null +++ b/src/main/java/com/api/trip/domain/itemtag/service/ItemTagService.java @@ -0,0 +1,43 @@ +package com.api.trip.domain.itemtag.service; + +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.itemtag.model.ItemTag; +import com.api.trip.domain.itemtag.repository.ItemTagRepository; +import com.api.trip.domain.tag.model.Tag; +import com.api.trip.domain.tag.service.TagService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class ItemTagService { + + private final ItemTagRepository itemTagRepository; + private final TagService tagService; + + public void createItemTag(Item item, List tagNames){ + List tags = tagService.getTags(tagNames); + + for(Tag tag : tags){ + itemTagRepository.save( + ItemTag.builder() + .item(item) + .tag(tag) + .build() + ); + + } + } + + public Page getItemsByTag(Pageable pageable, int sortCode, List tagNames) { + List tags = tagService.getTags(tagNames); + return itemTagRepository.findItemsByTags(pageable, sortCode, tags); + } +} diff --git a/src/main/java/com/api/trip/domain/tag/model/Tag.java b/src/main/java/com/api/trip/domain/tag/model/Tag.java new file mode 100644 index 0000000..5358366 --- /dev/null +++ b/src/main/java/com/api/trip/domain/tag/model/Tag.java @@ -0,0 +1,26 @@ +package com.api.trip.domain.tag.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Tag extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Builder + private Tag(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/api/trip/domain/tag/repository/TagRepository.java b/src/main/java/com/api/trip/domain/tag/repository/TagRepository.java new file mode 100644 index 0000000..4f32abd --- /dev/null +++ b/src/main/java/com/api/trip/domain/tag/repository/TagRepository.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.tag.repository; + +import com.api.trip.domain.tag.model.Tag; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TagRepository extends JpaRepository { + + List findByNameIn(List tagNames); +} diff --git a/src/main/java/com/api/trip/domain/tag/service/TagService.java b/src/main/java/com/api/trip/domain/tag/service/TagService.java new file mode 100644 index 0000000..d980d30 --- /dev/null +++ b/src/main/java/com/api/trip/domain/tag/service/TagService.java @@ -0,0 +1,29 @@ +package com.api.trip.domain.tag.service; + +import com.api.trip.domain.tag.model.Tag; +import com.api.trip.domain.tag.repository.TagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class TagService { + + private final TagRepository tagRepository; + + public void createTag(String tagName){ + Tag tag = Tag.builder().name(tagName).build(); + tagRepository.save(tag); + } + public List getTags(List tagNames){ + return tagRepository.findByNameIn(tagNames); + } + /* + @Todo + 필요 시 삭제 기능 구현 + */ +}