diff --git a/.github/workflows/dev_deply.yml b/.github/workflows/dev_deply.yml index 0d2d6aa..3b202ce 100644 --- a/.github/workflows/dev_deply.yml +++ b/.github/workflows/dev_deply.yml @@ -95,6 +95,7 @@ jobs: -e KAKAO_PROVIDER_TOKEN_URI=${{secrets.KAKAO_PROVIDER_TOKEN_URI}} \ -e KAKAO_PROVIDER_USER_INFO_URI=${{secrets.KAKAO_PROVIDER_USER_INFO_URI}} \ -e PORTONE_API_SECRET=${{secrets.PORTONE_API_SECRET}} \ + -e WEBHOOK_URL=${{secrets.WEBHOOK_URL}} \ ${{ secrets.DOCKERHUB_USERNAME }}/bill-api:latest sudo docker rm $(sudo docker ps --filter 'status=exited' -a -q) diff --git a/src/main/java/site/billbill/apiserver/api/auth/controller/AuthController.java b/src/main/java/site/billbill/apiserver/api/auth/controller/AuthController.java index dfd927c..bfac926 100644 --- a/src/main/java/site/billbill/apiserver/api/auth/controller/AuthController.java +++ b/src/main/java/site/billbill/apiserver/api/auth/controller/AuthController.java @@ -11,8 +11,10 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import site.billbill.apiserver.api.auth.dto.request.*; +import site.billbill.apiserver.api.auth.dto.response.NicknameResponse; import site.billbill.apiserver.api.auth.service.AuthService; import site.billbill.apiserver.api.auth.service.OAuthService; +import site.billbill.apiserver.api.users.service.UserService; import site.billbill.apiserver.common.response.BaseResponse; import site.billbill.apiserver.common.utils.jwt.dto.JwtDto; @@ -30,6 +32,7 @@ public class AuthController { private final AuthService authService; private final OAuthService oAuthService; + private final UserService userService; @Operation(summary = "회원 가입(일반)", description = "일반 회원 가입 API") @ResponseStatus(HttpStatus.CREATED) @@ -64,4 +67,14 @@ public BaseResponse kakaoCallback(@RequestParam("code") String code) { public BaseResponse identity(@RequestBody IdentityVerificationRequest request) { return null; } + + @Operation(summary = "닉네임 중복검사", description = "닉네임 중복검사") + @ResponseStatus(HttpStatus.OK) + @GetMapping("/nickname") + public BaseResponse getNicknameValidity(@RequestParam String nickname) { + return new BaseResponse<>(NicknameResponse.builder() + .valid(authService.getNicknameValidity(nickname)) + .build()); + } + } diff --git a/src/main/java/site/billbill/apiserver/api/auth/dto/response/NicknameResponse.java b/src/main/java/site/billbill/apiserver/api/auth/dto/response/NicknameResponse.java new file mode 100644 index 0000000..38d8087 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/auth/dto/response/NicknameResponse.java @@ -0,0 +1,10 @@ +package site.billbill.apiserver.api.auth.dto.response; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class NicknameResponse { + private boolean valid; +} diff --git a/src/main/java/site/billbill/apiserver/api/auth/service/AuthService.java b/src/main/java/site/billbill/apiserver/api/auth/service/AuthService.java index 14667f6..54060ef 100644 --- a/src/main/java/site/billbill/apiserver/api/auth/service/AuthService.java +++ b/src/main/java/site/billbill/apiserver/api/auth/service/AuthService.java @@ -13,4 +13,6 @@ public interface AuthService { JwtDto reissue(String refreshToken); boolean identifyUser(IdentityRequest request); + + boolean getNicknameValidity(String nickname); } diff --git a/src/main/java/site/billbill/apiserver/api/auth/service/AuthServiceImpl.java b/src/main/java/site/billbill/apiserver/api/auth/service/AuthServiceImpl.java index b234eff..960b724 100644 --- a/src/main/java/site/billbill/apiserver/api/auth/service/AuthServiceImpl.java +++ b/src/main/java/site/billbill/apiserver/api/auth/service/AuthServiceImpl.java @@ -118,6 +118,11 @@ public boolean identifyUser(IdentityRequest request) { return false; } + @Override + public boolean getNicknameValidity(String nickname) { + return !userRepository.existsByNickname(nickname); + } + /** * Method that if user is withdrawn * diff --git a/src/main/java/site/billbill/apiserver/api/borrowPosts/controller/PostsController.java b/src/main/java/site/billbill/apiserver/api/borrowPosts/controller/PostsController.java index 6c92950..4f56905 100644 --- a/src/main/java/site/billbill/apiserver/api/borrowPosts/controller/PostsController.java +++ b/src/main/java/site/billbill/apiserver/api/borrowPosts/controller/PostsController.java @@ -10,6 +10,7 @@ import org.jboss.logging.MDC; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; +import org.springframework.security.core.parameters.P; import org.springframework.web.bind.annotation.*; import site.billbill.apiserver.api.borrowPosts.dto.request.PostsRequest; import site.billbill.apiserver.api.borrowPosts.dto.response.PostsResponse; @@ -17,6 +18,8 @@ import site.billbill.apiserver.common.response.BaseResponse; import site.billbill.apiserver.common.utils.jwt.JWTUtil; +import java.util.List; + @Slf4j @RestController @Tag(name = "borrowPosts", description = "대여 게시물 관련") @@ -24,6 +27,7 @@ @RequiredArgsConstructor public class PostsController { private final PostsService postsService; + @Operation(summary = "게시물 생성", description = "게시물 생성 API") @PostMapping("") public BaseResponse uploadPostsController(@RequestBody @Valid PostsRequest.UploadRequest request){ @@ -46,17 +50,79 @@ public BaseResponse getPostsController( @Parameter(name = "order", description = "정렬 방향 (asc: 오름차순, desc: 내림차순)", example = "desc", in = ParameterIn.QUERY, required = false) @RequestParam(value ="order",required = false,defaultValue = "desc") String order, @Parameter(name = "sortBy", description = "정렬 기준 (예: price, createdAt, likeCount)", example = "createdAt", in = ParameterIn.QUERY, required = true) - @RequestParam(value="sortBy",required = true,defaultValue = "accuracy") String sortBy){ + @RequestParam(value="sortBy",required = true,defaultValue = "accuracy") String sortBy + ){ Sort.Direction direction = "asc".equalsIgnoreCase(order) ? Sort.Direction.ASC : Sort.Direction.DESC; return new BaseResponse<>(postsService.ViewAllPostService(category,page,direction,sortBy)); } + @Operation(summary = "게시물 검색", description = "게시물 검색 API") + @GetMapping("/search") + public BaseResponse getSearchPostsController( + @Parameter(name = "category", description = "카테고리 필터 (예: entire, camp, sports,tools )", example = "entire", in = ParameterIn.QUERY, required = false) + @RequestParam(value ="category",required = false,defaultValue = "entire") String category, + @Parameter(name = "page", description = "페이지 번호 (1부터 시작)", example = "1", in = ParameterIn.QUERY, required = false) + @RequestParam(value ="page",required = false,defaultValue = "1") int page, + @Parameter(name = "order", description = "정렬 방향 (asc: 오름차순, desc: 내림차순)", example = "desc", in = ParameterIn.QUERY, required = false) + @RequestParam(value ="order",required = false,defaultValue = "desc") String order, + @Parameter(name = "sortBy", description = "정렬 기준 (예: price, createdAt, likeCount)", example = "createdAt", in = ParameterIn.QUERY, required = true) + @RequestParam(value="sortBy",required = true,defaultValue = "accuracy") String sortBy, + @Parameter(name="keyword",description = "검색 키워드(예: 6인용+텐트)",in = ParameterIn.QUERY, required = true) + @RequestParam(value = "keyword",required = true) String keyword){ + + String userId = ""; + if(MDC.get(JWTUtil.MDC_USER_ID) != null) { + userId= MDC.get(JWTUtil.MDC_USER_ID).toString(); + } + Sort.Direction direction = "asc".equalsIgnoreCase(order) ? Sort.Direction.ASC : Sort.Direction.DESC; + return new BaseResponse<>(postsService.ViewSearchPostService(userId,category, page, direction, sortBy,keyword,false)); + } + @Operation(summary = "게시물 조회", description = "게시물 상세 조회") @GetMapping("/{postId}") public BaseResponse getPostController(@PathVariable(value = "postId",required = true)String postId){ + String userId = ""; + if(MDC.get(JWTUtil.MDC_USER_ID) != null) { + userId= MDC.get(JWTUtil.MDC_USER_ID).toString(); + } + return new BaseResponse<>(postsService.ViewPostService(postId,userId)); + } + @Operation(summary = "게시물 삭제", description = "게시물 삭제") + @DeleteMapping("/{postId}") + public BaseResponse deletePostController(@PathVariable(value = "postId",required = true)String postId){ + + String userId = ""; + if(MDC.get(JWTUtil.MDC_USER_ID) != null) { + userId= MDC.get(JWTUtil.MDC_USER_ID).toString(); + } + return new BaseResponse<>(postsService.deletePostService(postId,userId)); + } +// @Operation(summary = "저장한 검색어 불러오기", description = "저장한 검색어 불러오기") +// @GetMapping("/searchHist") +// public BaseResponse getSearchHistController(){ +// String userId = ""; +// if(MDC.get(JWTUtil.MDC_USER_ID) != null) { +// userId= MDC.get(JWTUtil.MDC_USER_ID).toString(); +// } +// return new BaseResponse<>(postsService.findSearchService(userId)); +// } - return new BaseResponse<>(postsService.ViewPostService(postId)); + @Operation(summary = "추천 검색어 불러오기", description = "추천 검색어 주기") + @GetMapping("/recommend") + public BaseResponse> getRecommendController(){ + return new BaseResponse<>(postsService.findRecommandService()); + } + @Operation(summary = "게시물 수정", description = "게시물 수정") + @PatchMapping("/{postId}") + public BaseResponse updatePostController(@PathVariable(value="postId",required = true)String postId, + @RequestBody @Valid PostsRequest.UploadRequest request){ + String userId = ""; + if(MDC.get(JWTUtil.MDC_USER_ID) != null) { + userId= MDC.get(JWTUtil.MDC_USER_ID).toString(); + } + return new BaseResponse<>(postsService.UpdatePostService(postId,userId,request)); } + } diff --git a/src/main/java/site/billbill/apiserver/api/borrowPosts/converter/PostsConverter.java b/src/main/java/site/billbill/apiserver/api/borrowPosts/converter/PostsConverter.java index ce16992..9697a2f 100644 --- a/src/main/java/site/billbill/apiserver/api/borrowPosts/converter/PostsConverter.java +++ b/src/main/java/site/billbill/apiserver/api/borrowPosts/converter/PostsConverter.java @@ -2,10 +2,10 @@ import site.billbill.apiserver.api.borrowPosts.dto.request.PostsRequest; import site.billbill.apiserver.api.borrowPosts.dto.response.PostsResponse; -import site.billbill.apiserver.model.post.ItemsBorrowJpaEntity; -import site.billbill.apiserver.model.post.ItemsBorrowStatusJpaEntity; -import site.billbill.apiserver.model.post.ItemsJpaEntity; +import site.billbill.apiserver.model.post.*; import site.billbill.apiserver.model.user.UserJpaEntity; +import site.billbill.apiserver.model.user.UserSearchHistJpaEntity; +import site.billbill.apiserver.repository.borrowPosts.SearchKeywordStatRepository; import java.time.format.DateTimeFormatter; import java.util.List; @@ -20,7 +20,7 @@ public static PostsResponse.UploadResponse toUploadResponse(String id){ postId(id). build(); } - public static ItemsJpaEntity toItem(String postId,PostsRequest.UploadRequest request, UserJpaEntity user){ + public static ItemsJpaEntity toItem(String postId, PostsRequest.UploadRequest request, UserJpaEntity user, ItemsCategoryJpaEntity category){ return ItemsJpaEntity.builder() .id(postId) .title(request.getTitle()) @@ -29,6 +29,7 @@ public static ItemsJpaEntity toItem(String postId,PostsRequest.UploadRequest req .owner(user) .viewCount(0) .images(request.getImages()) + .category(category) .itemStatus(request.getItemStatus()).build(); } public static ItemsBorrowJpaEntity toItemBorrow(ItemsJpaEntity item, PostsRequest.UploadRequest request){ @@ -50,6 +51,7 @@ public static ItemsBorrowStatusJpaEntity toItemBorrowStatus(ItemsJpaEntity item, public static PostsResponse.Post toPost(ItemsJpaEntity item,ItemsBorrowJpaEntity borrowItem){ return PostsResponse.Post.builder() .postId(item.getId()) + .title(item.getTitle()) .image(Optional.ofNullable(item.getImages()) .filter(images -> !images.isEmpty()) .map(images -> images.get(0)) @@ -64,7 +66,7 @@ public static PostsResponse.Post toPost(ItemsJpaEntity item,ItemsBorrowJpaEntity public static PostsResponse.ViewAllResultResponse toViewAllList(List posts){ return PostsResponse.ViewAllResultResponse.builder().result(posts).build(); } - public static PostsResponse.ViewPostResponse toViewPost(ItemsJpaEntity item, ItemsBorrowJpaEntity borrowItem, List noRental, String status){ + public static PostsResponse.ViewPostResponse toViewPost(ItemsJpaEntity item, ItemsBorrowJpaEntity borrowItem, List noRental, String status,UserJpaEntity user){ return PostsResponse.ViewPostResponse.builder() .postId(item.getId()) .title(item.getTitle()) @@ -77,6 +79,8 @@ public static PostsResponse.ViewPostResponse toViewPost(ItemsJpaEntity item, Ite .itemStatus(status) .categoryId(item.getCategory().getId()) .categoryName(item.getCategory().getName()) + .userId(user.getUserId()) + .userName(user.getNickname()) .build(); } public static PostsResponse.NoRentalPeriodResponse toNoRentalPeriod(ItemsBorrowStatusJpaEntity borrowStatus){ @@ -85,5 +89,26 @@ public static PostsResponse.NoRentalPeriodResponse toNoRentalPeriod(ItemsBorrowS .endDate(borrowStatus.getEndDate().format(DATE_FORMATTER)) .build(); } + public static UserSearchHistJpaEntity toUserSearch(UserJpaEntity user,String keyword){ + return UserSearchHistJpaEntity.builder() + .keyword(keyword) + .user(user).build(); + } + public static SearchKeywordStatsJpaEntity toSearchKeywordStats(String keyword){ + return SearchKeywordStatsJpaEntity.builder() + .keyword(keyword) + .searchCount(1).build(); + } + public static PostsResponse.saveSearch toUserSearchHist(UserSearchHistJpaEntity userSeachHistory){ + return PostsResponse.saveSearch.builder().id(userSeachHistory.getSearchId()) + .keyword(userSeachHistory.getKeyword()).build(); + } + public static PostsResponse.saveSearchListResponse toUserSearhList(List savedSearches){ + return PostsResponse.saveSearchListResponse.builder().results(savedSearches).build(); + } + public static String toRecommandSearch(SearchKeywordStatsJpaEntity searchKeywordStats){ + return searchKeywordStats.getKeyword(); + } + } diff --git a/src/main/java/site/billbill/apiserver/api/borrowPosts/dto/request/PostsRequest.java b/src/main/java/site/billbill/apiserver/api/borrowPosts/dto/request/PostsRequest.java index f7922a5..732c723 100644 --- a/src/main/java/site/billbill/apiserver/api/borrowPosts/dto/request/PostsRequest.java +++ b/src/main/java/site/billbill/apiserver/api/borrowPosts/dto/request/PostsRequest.java @@ -29,7 +29,8 @@ public static class UploadRequest { private String content; @Schema(description = "이미지", example = "[\"이미지링크\",\"이미지링크\"]") private List images; - + @Schema(description = "카테고리", example = "camp, sports, tools") + private String category; private List noRental; } diff --git a/src/main/java/site/billbill/apiserver/api/borrowPosts/dto/response/PostsResponse.java b/src/main/java/site/billbill/apiserver/api/borrowPosts/dto/response/PostsResponse.java index 24b172c..455c196 100644 --- a/src/main/java/site/billbill/apiserver/api/borrowPosts/dto/response/PostsResponse.java +++ b/src/main/java/site/billbill/apiserver/api/borrowPosts/dto/response/PostsResponse.java @@ -27,6 +27,7 @@ public static class ViewAllResultResponse{ public static class Post{ private String postId; private String image; + private String title; private int price; private String userId; private String userName; @@ -47,8 +48,24 @@ public static class ViewPostResponse{ private List noRentalPeriod; private String categoryId; private String categoryName; + private String userId; + private String userName; } + @Getter + @Setter + @Builder + public static class saveSearchListResponse{ + private List results; + } + @Getter + @Setter + @Builder + public static class saveSearch{ + private Long id; + private String keyword; + } + @Getter @Setter @Builder diff --git a/src/main/java/site/billbill/apiserver/api/borrowPosts/service/PostsService.java b/src/main/java/site/billbill/apiserver/api/borrowPosts/service/PostsService.java index d707c99..e3e15f9 100644 --- a/src/main/java/site/billbill/apiserver/api/borrowPosts/service/PostsService.java +++ b/src/main/java/site/billbill/apiserver/api/borrowPosts/service/PostsService.java @@ -4,10 +4,23 @@ import site.billbill.apiserver.api.borrowPosts.dto.request.PostsRequest; import site.billbill.apiserver.api.borrowPosts.dto.response.PostsResponse; +import java.util.List; + public interface PostsService { PostsResponse.UploadResponse uploadPostService(PostsRequest.UploadRequest request,String userId); PostsResponse.ViewAllResultResponse ViewAllPostService(String category, int page, Sort.Direction direction, String orderType); - PostsResponse.ViewPostResponse ViewPostService(String postId); + PostsResponse.ViewPostResponse ViewPostService(String postId,String userId); + + String deletePostService(String postId,String userId); + + String UpdatePostService(String postId,String userId,PostsRequest.UploadRequest request); + + PostsResponse.ViewAllResultResponse ViewSearchPostService(String userId,String category, int page, Sort.Direction direction, String orderType,String keyword,boolean state); + + PostsResponse.saveSearchListResponse findSearchService(String userId); + + List findRecommandService(); + } diff --git a/src/main/java/site/billbill/apiserver/api/borrowPosts/service/PostsServiceImpl.java b/src/main/java/site/billbill/apiserver/api/borrowPosts/service/PostsServiceImpl.java index f14a829..7b14d5f 100644 --- a/src/main/java/site/billbill/apiserver/api/borrowPosts/service/PostsServiceImpl.java +++ b/src/main/java/site/billbill/apiserver/api/borrowPosts/service/PostsServiceImpl.java @@ -1,41 +1,33 @@ package site.billbill.apiserver.api.borrowPosts.service; import lombok.RequiredArgsConstructor; -import lombok.extern.java.Log; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; -import site.billbill.apiserver.api.borrowPosts.controller.PostsController; +import org.springframework.transaction.annotation.Transactional; import site.billbill.apiserver.api.borrowPosts.converter.PostsConverter; import site.billbill.apiserver.api.borrowPosts.dto.request.PostsRequest; import site.billbill.apiserver.api.borrowPosts.dto.response.PostsResponse; import site.billbill.apiserver.common.enums.exception.ErrorCode; import site.billbill.apiserver.common.utils.ULID.ULIDUtil; import site.billbill.apiserver.exception.CustomException; -import site.billbill.apiserver.model.post.ItemsBorrowJpaEntity; -import site.billbill.apiserver.model.post.ItemsBorrowStatusJpaEntity; -import site.billbill.apiserver.model.post.ItemsJpaEntity; +import site.billbill.apiserver.model.post.*; import site.billbill.apiserver.model.user.UserJpaEntity; -import site.billbill.apiserver.repository.borrowPosts.ItemsBorrowRepository; -import site.billbill.apiserver.repository.borrowPosts.ItemsBorrowStatusRepository; -import site.billbill.apiserver.repository.borrowPosts.ItemsCategoryRepository; -import site.billbill.apiserver.repository.borrowPosts.ItemsRepository; +import site.billbill.apiserver.model.user.UserSearchHistJpaEntity; +import site.billbill.apiserver.repository.borrowPosts.*; import site.billbill.apiserver.repository.user.UserRepository; +import site.billbill.apiserver.repository.user.UserSearchHistRepository; -import javax.swing.text.html.Option; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @org.springframework.stereotype.Service @RequiredArgsConstructor @Slf4j - public class PostsServiceImpl implements PostsService { private final UserRepository userRepository; @@ -43,17 +35,19 @@ public class PostsServiceImpl implements PostsService { private final ItemsBorrowRepository itemsBorrowRepository; private final ItemsBorrowStatusRepository itemsBorrowStatusRepository; private final ItemsCategoryRepository itemsCategoryRepository; + private final UserSearchHistRepository userSearchHistRepository; + private final SearchKeywordStatRepository searchKeywordStatRepository; public PostsResponse.UploadResponse uploadPostService(PostsRequest.UploadRequest request,String userId){ //먼저 item 생성, Optional isUser=userRepository.findById(userId); String postsId = ULIDUtil.generatorULID("BORROW"); - + ItemsCategoryJpaEntity category = itemsCategoryRepository.findByName(request.getCategory()); UserJpaEntity user=new UserJpaEntity(); if(isUser.isPresent()){ user=isUser.get(); } //Item 생성 - ItemsJpaEntity newItem= PostsConverter.toItem(postsId,request,user); + ItemsJpaEntity newItem= PostsConverter.toItem(postsId,request,user,category); itemsRepository.save(newItem); ItemsJpaEntity item=itemsRepository.findById(postsId).orElse(newItem); //BorrowItem 생성 @@ -76,47 +70,16 @@ public PostsResponse.UploadResponse uploadPostService(PostsRequest.UploadRequest public PostsResponse.ViewAllResultResponse ViewAllPostService( String category, int page, Sort.Direction direction, String orderType) { - // 기본 정렬 필드와 방향 설정 - String sortField = switch (orderType) { - case "price" -> "price"; - case "createdAt" -> "createdAt"; - case "likeCount" -> "likeCount"; - default -> "createdAt"; // 기본 정렬 - }; - - direction = (direction == null) ? Sort.Direction.DESC : direction; - - Pageable pageable = PageRequest.of( - Math.max(0, page - 1), // 페이지 번호 조정 (0부터 시작) - 20, - Sort.by(direction, sortField) - ); - - // Repository 호출 - Page itemsPage = itemsRepository.findItemsWithConditions(category, pageable, sortField); - - // 빈 결과 체크 - if (itemsPage.isEmpty()) { - log.warn("No items found for category: {}, page: {}, sortField: {}", category, page, sortField); - return PostsConverter.toViewAllList(List.of()); - } - - // 데이터 변환 - List borrowItems = itemsPage.getContent().stream() - .map(item -> { - ItemsBorrowJpaEntity borrowItem = itemsBorrowRepository.findById(item.getId()).orElse(null); - if (borrowItem == null) { - log.warn("No borrow item found for item ID: {}", item.getId()); - } - return PostsConverter.toPost(item, borrowItem); - }).toList(); - - return PostsConverter.toViewAllList(borrowItems); + Pageable pageable = createPageable(page, direction, orderType); + List items = findAndConvertItems(category, pageable, null); + return PostsConverter.toViewAllList(items); } - public PostsResponse.ViewPostResponse ViewPostService(String postId){ + public PostsResponse.ViewPostResponse ViewPostService(String postId,String userId){ ItemsJpaEntity item=itemsRepository.findById(postId).orElse(null); ItemsBorrowJpaEntity borrowItem=itemsBorrowRepository.findById(postId).orElse(null); + UserJpaEntity user = userRepository.findById(userId).orElse(null); + List borrowStatus=itemsBorrowStatusRepository.findAllByItemIdAndBorrowStatusCode(postId,"RENTAL_NOT_POSSIBLE"); List noRentalPeriods=borrowStatus.stream().map(PostsConverter::toNoRentalPeriod).toList(); if(item==null){ @@ -141,9 +104,137 @@ public PostsResponse.ViewPostResponse ViewPostService(String postId){ break; } - return PostsConverter.toViewPost(item,borrowItem,noRentalPeriods,status); + return PostsConverter.toViewPost(item,borrowItem,noRentalPeriods,status,user); + + } + @Transactional + public String deletePostService(String postId,String userId){ + ItemsJpaEntity item= itemsRepository.findById(postId).orElse(null); + UserJpaEntity user=userRepository.findById(userId).orElse(null); + if(item==null){ + throw new CustomException(ErrorCode.BadRequest, "올바른 게시물 아이디가 아닙니다.", HttpStatus.BAD_REQUEST); + }else if(!item.getOwner().equals(user)){ + throw new CustomException(ErrorCode.BadRequest, "해당 게시물 작성자가 아닙니다.", HttpStatus.BAD_REQUEST); + } + + item.setDelYn(true); + + + return "Succes"; + } + @Transactional + public String UpdatePostService(String postId,String userId,PostsRequest.UploadRequest request) { + ItemsJpaEntity item = itemsRepository.findById(postId).orElse(null); + UserJpaEntity user = userRepository.findById(userId).orElse(null); + ItemsCategoryJpaEntity category = itemsCategoryRepository.findByName(request.getCategory()); + ItemsBorrowJpaEntity borrowItem = itemsBorrowRepository.findById(postId).orElse(null); + + if (item == null) { + throw new CustomException(ErrorCode.BadRequest, "올바른 게시물 아이디가 아닙니다.", HttpStatus.BAD_REQUEST); + } else if (!item.getOwner().equals(user)) { + throw new CustomException(ErrorCode.BadRequest, "해당 게시물 작성자가 아닙니다.", HttpStatus.BAD_REQUEST); + } + + item.setTitle(request.getTitle()); + item.setImages(request.getImages()); + item.setItemStatus(request.getItemStatus()); + item.setCategory(category); + item.setContent(request.getContent()); + borrowItem.setDeposit(request.getDeposit()); + borrowItem.setPrice(request.getPrice()); + + List existingStatuses = itemsBorrowStatusRepository.findAllByItemIdAndBorrowStatusCode(postId, "RENTAL_NOT_POSSIBLE"); + itemsBorrowStatusRepository.deleteAll(existingStatuses); + //대여 불가 날짜 새로 배정, 똑같아도 새로 배정되는 느낌 + if (request.getNoRental() != null && !request.getNoRental().isEmpty()) { + List newStatuses = request.getNoRental().stream() + .map(status -> PostsConverter.toItemBorrowStatus(item, "RENTAL_NOT_POSSIBLE", status)) + .toList(); + itemsBorrowStatusRepository.saveAll(newStatuses); + + } + return "success"; + } + @Transactional + public PostsResponse.ViewAllResultResponse ViewSearchPostService(String userId, String category, int page, Sort.Direction direction, String orderType,String keyword,boolean state){ + UserJpaEntity user = userRepository.findById(userId).orElse(null); + + Pageable pageable = createPageable(page, direction, orderType); + List items = findAndConvertItems(category, pageable, keyword); + //사용자가 검색어 저장을 허용했을 경우 + String tempKeyword = keyword.replaceAll("\\+", " "); +// if(state){ +// +// UserSearchHistJpaEntity userSearchHist= PostsConverter.toUserSearch(user,tempKeyword); +// userSearchHistRepository.save(userSearchHist); +// } + //추천 검색어를 위해 검색어 를 저장 + SearchKeywordStatsJpaEntity searchKeywordStats = searchKeywordStatRepository.findByKeyword(tempKeyword); + if(searchKeywordStats!=null){ + int count=searchKeywordStats.getSearchCount()+1; + searchKeywordStats.setSearchCount(count); + }else{ + searchKeywordStats = PostsConverter.toSearchKeywordStats(tempKeyword); + searchKeywordStatRepository.save(searchKeywordStats); + } + return PostsConverter.toViewAllList(items); + } + public PostsResponse.saveSearchListResponse findSearchService(String userId){ + UserJpaEntity user = userRepository.findById(userId).orElse(null); + List searchHists=userSearchHistRepository.findByUserAndDelYnOrderByCreatedAtDesc(user,false); + List result= searchHists.stream().map(searchHist-> PostsConverter.toUserSearchHist(searchHist)).toList(); + return PostsConverter.toUserSearhList(result); } + + public List findRecommandService(){ + List searchKeywordStats=searchKeywordStatRepository.findAllByOrderBySearchCountDesc(); + List result =searchKeywordStats.stream().map(searchKeywordStat-> PostsConverter.toRecommandSearch(searchKeywordStat)).toList(); + return result; + } + //모듈화 코드 + + private Pageable createPageable(int page, Sort.Direction direction, String orderType) { + //카테고리 필드 + String sortField = switch (orderType) { + case "price" -> "price"; + case "createdAt" -> "createdAt"; + case "likeCount" -> "likeCount"; + default -> "createdAt"; // 기본 정렬 + }; + //정렬 순서 + direction = (direction == null) ? Sort.Direction.DESC : direction; + //페이지 생성 + return PageRequest.of( + Math.max(0, page - 1), // 페이지 번호 조정 (0부터 시작) + 20, + Sort.by(direction, sortField) + ); + } + + private List findAndConvertItems(String category, Pageable pageable, String keyword) { + // Repository 호출 + Page itemsPage = itemsRepository.findItemsWithConditions(category, pageable, null, keyword); + + // 빈 결과 체크 + if (itemsPage.isEmpty()) { + log.warn("No items found for category: {}", category); + return List.of(); + } + + // 데이터 변환 + return itemsPage.getContent().stream() + .map(item -> { + ItemsBorrowJpaEntity borrowItem = itemsBorrowRepository.findById(item.getId()).orElse(null); + if (borrowItem == null) { + log.warn("No borrow item found for item ID: {}", item.getId()); + } + return PostsConverter.toPost(item, borrowItem); + }) + .toList(); + } + + } diff --git a/src/main/java/site/billbill/apiserver/api/chat/controller/ChatController.java b/src/main/java/site/billbill/apiserver/api/chat/controller/ChatController.java new file mode 100644 index 0000000..48dd616 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/chat/controller/ChatController.java @@ -0,0 +1,52 @@ +package site.billbill.apiserver.api.chat.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jboss.logging.MDC; +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.RestController; +import site.billbill.apiserver.api.chat.dto.request.ChatRequest; +import site.billbill.apiserver.api.chat.dto.response.ChatResponse; +import site.billbill.apiserver.api.chat.service.ChatService; +import site.billbill.apiserver.common.response.BaseResponse; +import site.billbill.apiserver.common.utils.jwt.JWTUtil; + +@Slf4j +@RestController +@Tag(name = "Chat", description = "Chat API") +@RequestMapping("/api/v1/chat") +@RequiredArgsConstructor +public class ChatController { + private final ChatService chatService; + + @Operation(summary = "채팅방 나가기", description = "채팅방 나가기 API") + @PatchMapping("/{channelId}") + public BaseResponse leaveChatChannel(@PathVariable(value = "channelId") String channelId) { + log.info("api 호출 정상적~"); + String userId = MDC.get(JWTUtil.MDC_USER_ID).toString(); + return new BaseResponse<>(chatService.leaveChatChannel(channelId,userId)); + } + + @Operation(summary = "채팅방 생성 및 id 조회", description = "빌리기 버튼 누를 때 api") + @PostMapping("") + public BaseResponse startChannel(@RequestBody ChatRequest.borrowInfo request) { + log.info("api 호출 정상적~"); + String userId = MDC.get(JWTUtil.MDC_USER_ID).toString(); + return new BaseResponse<>(chatService.startChannel(request, userId)); + } + + @Operation(summary = "채팅방 info 조회", description = "채팅방 info 조회 API") + @GetMapping("/{channelId}") + public BaseResponse getInfoChannel(@PathVariable(value = "channelId") String channelId) { + log.info("api 호출 정상적~"); + String userId = MDC.get(JWTUtil.MDC_USER_ID).toString(); + return new BaseResponse<>(chatService.getInfoChannel(channelId,userId)); + } +} diff --git a/src/main/java/site/billbill/apiserver/api/chat/converter/ChatConverter.java b/src/main/java/site/billbill/apiserver/api/chat/converter/ChatConverter.java new file mode 100644 index 0000000..b9c8d16 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/chat/converter/ChatConverter.java @@ -0,0 +1,39 @@ +package site.billbill.apiserver.api.chat.converter; + +import java.time.LocalDate; +import site.billbill.apiserver.api.chat.dto.response.ChatResponse; +import site.billbill.apiserver.model.chat.ChatChannelJpaEntity; +import site.billbill.apiserver.model.post.ItemsJpaEntity; +import site.billbill.apiserver.model.user.UserJpaEntity; + +public class ChatConverter { + public static ChatChannelJpaEntity toChatChannel(String channelId, ItemsJpaEntity item, UserJpaEntity owner, + UserJpaEntity contact, LocalDate startedAt, LocalDate endedAt) { + return ChatChannelJpaEntity.builder() + .channelId(channelId) + .item(item) + .owner(owner) + .contact(contact) + .startedAt(startedAt) + .endedAt(endedAt) + .build(); + } + + public static ChatResponse.ViewChannelInfoResponse toViewChannelInfo(ChatChannelJpaEntity channel, UserJpaEntity opponent, ItemsJpaEntity item, int totalPrice, + String status, String userId) { + + return ChatResponse.ViewChannelInfoResponse.builder() + .opponentId(opponent.getUserId()) + .opponentNickname(opponent.getNickname()) + .opponentProfileUrl(opponent.getProfile()) + .itemFirstUrl(item.getImages().get(0)) + .postIsDel(item.isDelYn()) + .postId(item.getId()) + .totalPrice(totalPrice) + .itemState(status) + .startedAt(channel.getStartedAt()) + .endedAt(channel.getEndedAt()) + .myId(userId) + .build(); + } +} diff --git a/src/main/java/site/billbill/apiserver/api/chat/dto/request/ChatRequest.java b/src/main/java/site/billbill/apiserver/api/chat/dto/request/ChatRequest.java new file mode 100644 index 0000000..d39d71d --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/chat/dto/request/ChatRequest.java @@ -0,0 +1,17 @@ +package site.billbill.apiserver.api.chat.dto.request; + +import java.time.LocalDate; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +public class ChatRequest { + @Getter + @Setter + @Builder + public static class borrowInfo { + private LocalDate startedAt; + private LocalDate endedAt; + private String postId; + } +} diff --git a/src/main/java/site/billbill/apiserver/api/chat/dto/response/ChatResponse.java b/src/main/java/site/billbill/apiserver/api/chat/dto/response/ChatResponse.java new file mode 100644 index 0000000..cc764b5 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/chat/dto/response/ChatResponse.java @@ -0,0 +1,26 @@ +package site.billbill.apiserver.api.chat.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import lombok.Builder; +import lombok.Getter; + +public class ChatResponse { + @Getter + @Builder + public static class ViewChannelInfoResponse { + String opponentId; + String opponentNickname; + String opponentProfileUrl; + String myId; + String itemFirstUrl; + boolean postIsDel; + String postId; + int totalPrice; + String itemState; + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate startedAt; + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate endedAt; + } +} diff --git a/src/main/java/site/billbill/apiserver/api/chat/service/ChatService.java b/src/main/java/site/billbill/apiserver/api/chat/service/ChatService.java new file mode 100644 index 0000000..ac7db1e --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/chat/service/ChatService.java @@ -0,0 +1,12 @@ +package site.billbill.apiserver.api.chat.service; + +import site.billbill.apiserver.api.chat.dto.request.ChatRequest; +import site.billbill.apiserver.api.chat.dto.response.ChatResponse.ViewChannelInfoResponse; + +public interface ChatService { + String leaveChatChannel(String postId, String userId); + + ViewChannelInfoResponse getInfoChannel(String channelId, String userId); + + String startChannel(ChatRequest.borrowInfo request, String userId); +} diff --git a/src/main/java/site/billbill/apiserver/api/chat/service/ChatServiceImpl.java b/src/main/java/site/billbill/apiserver/api/chat/service/ChatServiceImpl.java new file mode 100644 index 0000000..5ce6c63 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/chat/service/ChatServiceImpl.java @@ -0,0 +1,92 @@ +package site.billbill.apiserver.api.chat.service; + +import java.time.temporal.ChronoUnit; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import site.billbill.apiserver.api.chat.converter.ChatConverter; +import site.billbill.apiserver.api.chat.dto.request.ChatRequest; +import site.billbill.apiserver.api.chat.dto.response.ChatResponse; +import site.billbill.apiserver.common.enums.exception.ErrorCode; +import site.billbill.apiserver.common.utils.ULID.ULIDUtil; +import site.billbill.apiserver.exception.CustomException; +import site.billbill.apiserver.model.chat.ChatChannelJpaEntity; +import site.billbill.apiserver.model.post.ItemsBorrowJpaEntity; +import site.billbill.apiserver.model.post.ItemsJpaEntity; +import site.billbill.apiserver.model.user.UserJpaEntity; +import site.billbill.apiserver.repository.borrowPosts.ItemsBorrowRepository; +import site.billbill.apiserver.repository.borrowPosts.ItemsRepository; +import site.billbill.apiserver.repository.chat.ChatRepository; +import site.billbill.apiserver.repository.user.UserRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatServiceImpl implements ChatService { + private static final Logger log = LoggerFactory.getLogger(ChatServiceImpl.class); + private final ChatRepository chatRepository; + private final UserRepository userRepository; + private final ItemsRepository itemsRepository; + private final ItemsBorrowRepository itemsBorrowRepository; + private final WebhookServiceImpl webhookService; + + @Transactional + public String leaveChatChannel(String channelId, String userId) { + ChatChannelJpaEntity chatChannel = chatRepository.findById(channelId) + .orElseThrow(() -> new CustomException(ErrorCode.NotFound, "채널을 찾을 수 없습니다.", HttpStatus.NOT_FOUND)); + userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.NotFound, "회원을 찾을 수 없습니다.", HttpStatus.NOT_FOUND)); + chatChannel.processLeftUser(userId); + chatChannel.checkAndUpdateDelete(); + chatRepository.save(chatChannel); + + return "success"; + } + + @Transactional + public String startChannel(ChatRequest.borrowInfo request, String userId) { + ItemsJpaEntity item = itemsRepository.findById(request.getPostId()) + .orElseThrow(() -> new CustomException(ErrorCode.NotFound, "게시물을 찾을 수 없습니다.", HttpStatus.NOT_FOUND)); + UserJpaEntity contact = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.NotFound, "회원을 찾을 수 없습니다.", HttpStatus.NOT_FOUND)); + List chatChannel = chatRepository.findByItemAndStartAndEndDate(item, request.getStartedAt(), request.getEndedAt()); + String newChannelId = ULIDUtil.generatorULID("CHANNEL"); + + if (chatChannel.isEmpty()) { + ChatChannelJpaEntity newChatChannel = ChatConverter.toChatChannel(newChannelId, item, item.getOwner(), + contact, request.getStartedAt(), request.getEndedAt()); + chatRepository.save(newChatChannel); + webhookService.sendWebhookForChatRoomCreate(newChannelId, userId, item.getOwner().getUserId()); + return newChannelId; + } + return chatChannel.get(0).getChannelId(); + } + + public ChatResponse.ViewChannelInfoResponse getInfoChannel(String channelId, String userId) { + ChatChannelJpaEntity chatChannel = chatRepository.findById(channelId) + .orElseThrow(() -> new CustomException(ErrorCode.NotFound, "채널을 찾을 수 없습니다.", HttpStatus.NOT_FOUND)); + userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.NotFound, "회원을 찾을 수 없습니다.", HttpStatus.NOT_FOUND)); + ItemsJpaEntity item = chatChannel.getItem(); + ItemsBorrowJpaEntity itemBorrow = itemsBorrowRepository.findByItem(item); + + long daysBetween = ChronoUnit.DAYS.between(chatChannel.getStartedAt(), chatChannel.getEndedAt()) + 1; + int totalPrice = (int) (daysBetween * itemBorrow.getPrice()); + UserJpaEntity opponent = chatChannel.getOpponent(userId); + + String status = switch (item.getItemStatus()) { + case 1 -> "상"; + case 2 -> "중상"; + case 3 -> "중"; + case 4 -> "중하"; + case 5 -> "하"; + default -> ""; + }; + + return ChatConverter.toViewChannelInfo(chatChannel, opponent, item, totalPrice, status, userId); + } +} diff --git a/src/main/java/site/billbill/apiserver/api/chat/service/WebhookService.java b/src/main/java/site/billbill/apiserver/api/chat/service/WebhookService.java new file mode 100644 index 0000000..1010434 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/chat/service/WebhookService.java @@ -0,0 +1,5 @@ +package site.billbill.apiserver.api.chat.service; + +public interface WebhookService { + void sendWebhookForChatRoomCreate(String channelId, String contact, String owner); +} diff --git a/src/main/java/site/billbill/apiserver/api/chat/service/WebhookServiceImpl.java b/src/main/java/site/billbill/apiserver/api/chat/service/WebhookServiceImpl.java new file mode 100644 index 0000000..d7c6b1d --- /dev/null +++ b/src/main/java/site/billbill/apiserver/api/chat/service/WebhookServiceImpl.java @@ -0,0 +1,38 @@ +package site.billbill.apiserver.api.chat.service; + +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +@Service +public class WebhookServiceImpl { + private final WebClient webClient; + + @Value("${webhook.url}") + private String webhookUrl; + + @Autowired + public WebhookServiceImpl(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl(webhookUrl).build(); + } + + public void sendWebhookForChatRoomCreate(String channelId, String contact, String owner) { + Map payload = new HashMap<>(); + payload.put("channelId", channelId); + payload.put("contactId", contact); + payload.put("ownerId", owner); + + webClient.post() + .uri("") + .bodyValue(payload) + .retrieve() + .bodyToMono(Void.class) + .doOnError(error -> log.error("Webhook 호출 실패: {}", error.getMessage())) + .subscribe(); + } +} diff --git a/src/main/java/site/billbill/apiserver/api/users/controller/UserController.java b/src/main/java/site/billbill/apiserver/api/users/controller/UserController.java index 5bdb533..5164c53 100644 --- a/src/main/java/site/billbill/apiserver/api/users/controller/UserController.java +++ b/src/main/java/site/billbill/apiserver/api/users/controller/UserController.java @@ -12,6 +12,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import site.billbill.apiserver.api.auth.dto.request.DeviceRequest; import site.billbill.apiserver.api.users.dto.request.BlacklistRequest; import site.billbill.apiserver.api.users.dto.response.*; import site.billbill.apiserver.api.users.service.UserService; @@ -116,4 +117,12 @@ public BaseResponse> getWishlists( Pageable pageable = PageRequest.of((page < 1 ? 0 : page - 1), size); return new BaseResponse<>(userService.getWishlists(pageable)); } + + @Operation(summary = "내 디바이스 업데이트", description = "내 디바이스 정보를 업데이트하는 API") + @ResponseStatus(HttpStatus.OK) + @PostMapping("/device") + public BaseResponse updateDevice(@RequestBody DeviceRequest request) { + userService.updateDevice(request); + return new BaseResponse<>(null); + } } diff --git a/src/main/java/site/billbill/apiserver/api/users/service/UserService.java b/src/main/java/site/billbill/apiserver/api/users/service/UserService.java index 83d6f11..6f5c93f 100644 --- a/src/main/java/site/billbill/apiserver/api/users/service/UserService.java +++ b/src/main/java/site/billbill/apiserver/api/users/service/UserService.java @@ -1,6 +1,7 @@ package site.billbill.apiserver.api.users.service; import org.springframework.data.domain.Pageable; +import site.billbill.apiserver.api.auth.dto.request.DeviceRequest; import site.billbill.apiserver.api.users.dto.response.*; import site.billbill.apiserver.common.utils.posts.ItemHistoryType; @@ -24,4 +25,6 @@ public interface UserService { List getPostHistory(Pageable pageable, ItemHistoryType type); List getWishlists(Pageable pageable); + + void updateDevice(DeviceRequest request); } diff --git a/src/main/java/site/billbill/apiserver/api/users/service/UserServiceImpl.java b/src/main/java/site/billbill/apiserver/api/users/service/UserServiceImpl.java index 36f0fef..15e9c4a 100644 --- a/src/main/java/site/billbill/apiserver/api/users/service/UserServiceImpl.java +++ b/src/main/java/site/billbill/apiserver/api/users/service/UserServiceImpl.java @@ -7,17 +7,20 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import site.billbill.apiserver.api.auth.dto.request.DeviceRequest; import site.billbill.apiserver.api.users.dto.response.*; import site.billbill.apiserver.common.enums.exception.ErrorCode; import site.billbill.apiserver.common.utils.jwt.JWTUtil; import site.billbill.apiserver.common.utils.posts.ItemHistoryType; import site.billbill.apiserver.exception.CustomException; import site.billbill.apiserver.model.user.UserBlacklistJpaEntity; +import site.billbill.apiserver.model.user.UserDeviceJpaEntity; import site.billbill.apiserver.model.user.UserIdentityJpaEntity; import site.billbill.apiserver.model.user.UserJpaEntity; import site.billbill.apiserver.model.user.UserLocationJpaEntity; import site.billbill.apiserver.repository.borrowPosts.ItemsRepository; import site.billbill.apiserver.repository.user.UserBlacklistRepository; +import site.billbill.apiserver.repository.user.UserDeviceRepository; import site.billbill.apiserver.repository.user.UserIdentityRepository; import site.billbill.apiserver.repository.user.UserLocationReposity; import site.billbill.apiserver.repository.user.UserRepository; @@ -35,6 +38,7 @@ public class UserServiceImpl implements UserService { private final ItemsRepository itemsRepository; private final JWTUtil jWTUtil; private final UserLocationReposity userLocationReposity; + private final UserDeviceRepository userDeviceRepository; @Override public ProfileResponse getProfileInfo() { @@ -131,4 +135,19 @@ public List getWishlists(Pageable pageable) { return itemsRepository.getWishlists(userId, pageable); } + + @Override + public void updateDevice(DeviceRequest request) { + String userId = MDC.get(JWTUtil.MDC_USER_ID); + Optional userDeviceOptional = userDeviceRepository.findById(userId); + + if(userDeviceOptional.isEmpty()) throw new CustomException(ErrorCode.NotFound, "디바이스 정보가 존재하지 않습니다.", HttpStatus.NOT_FOUND); + + UserDeviceJpaEntity userDevice = userDeviceOptional.get(); + + userDevice.setDeviceToken(request.getDeviceToken()); + userDevice.setDeviceType(request.getDeviceType()); + userDevice.setAppVersion(request.getAppVersion()); + userDeviceRepository.save(userDevice); + } } diff --git a/src/main/java/site/billbill/apiserver/common/enums/chat/ChannelState.java b/src/main/java/site/billbill/apiserver/common/enums/chat/ChannelState.java new file mode 100644 index 0000000..79d2c0c --- /dev/null +++ b/src/main/java/site/billbill/apiserver/common/enums/chat/ChannelState.java @@ -0,0 +1,9 @@ +package site.billbill.apiserver.common.enums.chat; + +public enum ChannelState { + PRE, // 거래전 + ACCEPTED, // 거래수락 + CONFIRMED, // 거래확정 + CANCELLED // 거래취소 +} + diff --git a/src/main/java/site/billbill/apiserver/common/utils/jwt/JWTUtil.java b/src/main/java/site/billbill/apiserver/common/utils/jwt/JWTUtil.java index 5e5f368..c2e6509 100644 --- a/src/main/java/site/billbill/apiserver/common/utils/jwt/JWTUtil.java +++ b/src/main/java/site/billbill/apiserver/common/utils/jwt/JWTUtil.java @@ -70,6 +70,7 @@ public JwtDto generateJwtDto(String userId, UserRole role) { .compact(); return JwtDto.builder() + .userId(userId) .accessToken(accessToken) .refreshToken(refreshToken) .grantType("Bearer") diff --git a/src/main/java/site/billbill/apiserver/common/utils/jwt/dto/JwtDto.java b/src/main/java/site/billbill/apiserver/common/utils/jwt/dto/JwtDto.java index 00081fc..1cfcef0 100644 --- a/src/main/java/site/billbill/apiserver/common/utils/jwt/dto/JwtDto.java +++ b/src/main/java/site/billbill/apiserver/common/utils/jwt/dto/JwtDto.java @@ -11,6 +11,8 @@ @NoArgsConstructor @AllArgsConstructor public class JwtDto { + @Schema(description = "유저 ID", example = "USER-XXXXX...") + private String userId; @Schema(description = "엑세스 토큰", example = "accessToken(20min)") private String accessToken; @Schema(description = "리프레쉬 토큰", example = "refreshToken(4weeks)") diff --git a/src/main/java/site/billbill/apiserver/model/chat/ChatChannelJpaEntity.java b/src/main/java/site/billbill/apiserver/model/chat/ChatChannelJpaEntity.java index f270210..2421a4f 100644 --- a/src/main/java/site/billbill/apiserver/model/chat/ChatChannelJpaEntity.java +++ b/src/main/java/site/billbill/apiserver/model/chat/ChatChannelJpaEntity.java @@ -2,8 +2,11 @@ import jakarta.persistence.*; +import java.time.LocalDate; import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; import site.billbill.apiserver.common.converter.BooleanConverter; +import site.billbill.apiserver.common.enums.chat.ChannelState; import site.billbill.apiserver.model.BaseTime; import site.billbill.apiserver.model.post.ItemsJpaEntity; import site.billbill.apiserver.model.user.UserJpaEntity; @@ -26,7 +29,54 @@ public class ChatChannelJpaEntity extends BaseTime { @ManyToOne @JoinColumn(name="contact_id") private UserJpaEntity contact; + @Column(name = "owner_left", nullable = false) + @Convert(converter = BooleanConverter.class) + private boolean ownerLeft = false; + @Column(name = "contact_left", nullable = false) + @Convert(converter = BooleanConverter.class) + private boolean contactLeft = false; + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Column(name = "started_at", nullable = false) + private LocalDate startedAt; + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Column(name = "ended_at", nullable = false) + private LocalDate endedAt; @Column(name = "del_yn", nullable = false) @Convert(converter = BooleanConverter.class) private boolean delYn = false; + @Column(name = "clo_yn", nullable = false) + @Convert(converter = BooleanConverter.class) + private boolean cloYn = false; + @Builder.Default + @Enumerated(EnumType.STRING) + @Column(name = "channel_state") + private ChannelState channelState = ChannelState.PRE; + + public void processLeftUser(String userId) { + cloYn = true; + if (owner.getUserId().equals(userId)) { + ownerLeft = true; + return; + } + contactLeft = true; + } + + public void checkAndUpdateDelete() { + boolean allLeft = true; + + if (!ownerLeft || !contactLeft) { + allLeft = false; + } + + if (allLeft) { + delYn = true; + } + } + + public UserJpaEntity getOpponent(String userId) { + if (!owner.getUserId().equals(userId)) { + return owner; + } + return contact; + } } diff --git a/src/main/java/site/billbill/apiserver/model/post/ItemsCategoryJpaEntity.java b/src/main/java/site/billbill/apiserver/model/post/ItemsCategoryJpaEntity.java index 4e80abe..56f17b5 100644 --- a/src/main/java/site/billbill/apiserver/model/post/ItemsCategoryJpaEntity.java +++ b/src/main/java/site/billbill/apiserver/model/post/ItemsCategoryJpaEntity.java @@ -19,7 +19,7 @@ public class ItemsCategoryJpaEntity { private String id; @ManyToOne - @JoinColumn(name="uppder_cat_id",nullable = false) + @JoinColumn(name="upper_cat_id",nullable = false) private ItemsCategoryJpaEntity upperCategory; @Column(name="name") diff --git a/src/main/java/site/billbill/apiserver/model/post/SearchKeywordStatsJpaEntity.java b/src/main/java/site/billbill/apiserver/model/post/SearchKeywordStatsJpaEntity.java new file mode 100644 index 0000000..68733a6 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/model/post/SearchKeywordStatsJpaEntity.java @@ -0,0 +1,28 @@ +package site.billbill.apiserver.model.post; + +import jakarta.persistence.*; +import lombok.*; + +import site.billbill.apiserver.model.BaseTime; + +import java.time.LocalDate; + +@Entity +@Table(name = "search_keyword_stats") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SearchKeywordStatsJpaEntity extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="keyword_seq") + private long id; + + @Column(name="keyword",nullable = true) + private String keyword; + @Column(name = "search_count", nullable = false) + private int searchCount; + +} diff --git a/src/main/java/site/billbill/apiserver/model/user/UserSearchHistJpaEntity.java b/src/main/java/site/billbill/apiserver/model/user/UserSearchHistJpaEntity.java new file mode 100644 index 0000000..b28847d --- /dev/null +++ b/src/main/java/site/billbill/apiserver/model/user/UserSearchHistJpaEntity.java @@ -0,0 +1,30 @@ +package site.billbill.apiserver.model.user; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.DynamicUpdate; +import site.billbill.apiserver.common.converter.BooleanConverter; +import site.billbill.apiserver.model.BaseTime; +@DynamicUpdate +@Entity +@Table(name = "users_search_hist") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserSearchHistJpaEntity extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "search_seq", nullable = false) + private Long searchId; + @Column(name = "keyword", nullable = true) + private String keyword; + @Column(name = "del_yn", nullable = false) + @Convert(converter = BooleanConverter.class) + private boolean delYn; + @ManyToOne + @JoinColumn(name="user_id") + private UserJpaEntity user; + +} diff --git a/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemDslRepository.java b/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemDslRepository.java index 0e0f893..eca7ae7 100644 --- a/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemDslRepository.java +++ b/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemDslRepository.java @@ -11,11 +11,13 @@ import java.util.List; public interface ItemDslRepository { - Page findItemsWithConditions(String category, Pageable pageable, String sortField); + + Page findItemsWithConditions(String category, Pageable pageable, String sortField,String keyword); List getPostHistory(String userId, Pageable pageable); List getBorrowHistory(String userId, Pageable pageable, ItemHistoryType type); List getWishlists(String userId, Pageable pageable); + } diff --git a/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemDslRepositoryImpl.java b/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemDslRepositoryImpl.java index 512ed7c..02e824b 100644 --- a/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemDslRepositoryImpl.java +++ b/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemDslRepositoryImpl.java @@ -1,8 +1,12 @@ package site.billbill.apiserver.repository.borrowPosts; import com.querydsl.core.types.OrderSpecifier; + +import com.querydsl.core.types.dsl.BooleanExpression; + import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.Expressions; + import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -26,12 +30,12 @@ public class ItemDslRepositoryImpl implements ItemDslRepository { private final JPAQueryFactory queryFactory; @Override - public Page findItemsWithConditions(String category, Pageable pageable, String sortField) { + public Page findItemsWithConditions(String category, Pageable pageable, String sortField,String keyword) { QItemsJpaEntity items = QItemsJpaEntity.itemsJpaEntity; QItemsBorrowJpaEntity borrow = QItemsBorrowJpaEntity.itemsBorrowJpaEntity; QItemsCategoryJpaEntity categoryEntity = QItemsCategoryJpaEntity.itemsCategoryJpaEntity; - log.info("Category: {}, Sort Field: {}, Pageable: {}", category, sortField, pageable); + JPAQuery query = queryFactory.selectFrom(items) .leftJoin(borrow).on(items.id.eq(borrow.item.id)) // 식별 관계 조인 @@ -44,22 +48,34 @@ public Page findItemsWithConditions(String category, Pageable pa .fetchOne(); if (fetchedCategory == null) { - log.warn("Category '{}' not found. Returning empty result.", category); + log.warn("카테고리를 찾을 수 없음.", category); return new PageImpl<>(List.of(), pageable, 0); } query.where(items.category.eq(fetchedCategory)); } - // 정렬 조건 - if (sortField != null) { - OrderSpecifier orderSpecifier = getOrderSpecifier(sortField, pageable.getSort().getOrderFor(sortField)); - if (orderSpecifier != null) { - query.orderBy(orderSpecifier); - } else { - log.warn("Invalid sort field: {}, no sorting applied.", sortField); + //키워드 필터링 + if(keyword !=null && !keyword.isEmpty()){ + query.where(applyKeywordFilter(items,keyword)); + } + + // 정렬 조건 처리 + pageable.getSort().forEach(order -> { + OrderSpecifier orderSpecifier; + switch (order.getProperty()) { + case "price" -> orderSpecifier = order.isAscending() ? borrow.price.asc() : borrow.price.desc(); + case "createdAt" -> orderSpecifier = order.isAscending() ? items.createdAt.asc() : items.createdAt.desc(); + case "likeCount" -> orderSpecifier = order.isAscending() ? items.likeCount.asc() : items.likeCount.desc(); + default -> { + log.warn("Invalid sort field: {}", order.getProperty()); + return; } } + query.orderBy(orderSpecifier); + }); + + // 페이징 처리 List content = query.offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -99,6 +115,29 @@ private OrderSpecifier getOrderSpecifier(String sortField, Sort.Order sortOrd return null; // 기본 정렬 없음 } } + private BooleanExpression applyKeywordFilter(QItemsJpaEntity items, String keyword) { + if (keyword == null || keyword.isEmpty()) { + return null; + } + + // '+'를 기준으로 키워드 분리 + String[] keywords = keyword.split("\\+"); + + // 키워드 조건 생성 + BooleanExpression keywordCondition = null; + for (String key : keywords) { + BooleanExpression condition = items.title.containsIgnoreCase(key) + .or(items.content.containsIgnoreCase(key)); + + if (keywordCondition == null) { + keywordCondition = condition; + } else { + keywordCondition = keywordCondition.or(condition); // AND 대신 OR 사용 + } + } + + return keywordCondition; + } @Override public List getPostHistory(String userId, Pageable pageable) { diff --git a/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemsBorrowRepository.java b/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemsBorrowRepository.java index af47d56..ea74fd1 100644 --- a/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemsBorrowRepository.java +++ b/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemsBorrowRepository.java @@ -2,6 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import site.billbill.apiserver.model.post.ItemsBorrowJpaEntity; +import site.billbill.apiserver.model.post.ItemsJpaEntity; public interface ItemsBorrowRepository extends JpaRepository { + ItemsBorrowJpaEntity findByItem(ItemsJpaEntity item); } diff --git a/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemsCategoryRepository.java b/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemsCategoryRepository.java index a455f01..549eb9e 100644 --- a/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemsCategoryRepository.java +++ b/src/main/java/site/billbill/apiserver/repository/borrowPosts/ItemsCategoryRepository.java @@ -4,5 +4,5 @@ import site.billbill.apiserver.model.post.ItemsCategoryJpaEntity; public interface ItemsCategoryRepository extends JpaRepository { - ItemsCategoryRepository findByName(String name); + ItemsCategoryJpaEntity findByName(String name); } diff --git a/src/main/java/site/billbill/apiserver/repository/borrowPosts/SearchKeywordStatRepository.java b/src/main/java/site/billbill/apiserver/repository/borrowPosts/SearchKeywordStatRepository.java new file mode 100644 index 0000000..4462093 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/repository/borrowPosts/SearchKeywordStatRepository.java @@ -0,0 +1,11 @@ +package site.billbill.apiserver.repository.borrowPosts; + +import org.springframework.data.jpa.repository.JpaRepository; +import site.billbill.apiserver.model.post.SearchKeywordStatsJpaEntity; + +import java.util.List; + +public interface SearchKeywordStatRepository extends JpaRepository { + SearchKeywordStatsJpaEntity findByKeyword(String keyword); + List findAllByOrderBySearchCountDesc(); +} diff --git a/src/main/java/site/billbill/apiserver/repository/chat/ChatRepository.java b/src/main/java/site/billbill/apiserver/repository/chat/ChatRepository.java new file mode 100644 index 0000000..bdd015b --- /dev/null +++ b/src/main/java/site/billbill/apiserver/repository/chat/ChatRepository.java @@ -0,0 +1,20 @@ +package site.billbill.apiserver.repository.chat; + +import java.time.LocalDate; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import site.billbill.apiserver.model.chat.ChatChannelJpaEntity; +import site.billbill.apiserver.model.post.ItemsJpaEntity; + +public interface ChatRepository extends JpaRepository { +// List findByItemAndCloYnFalseAndDelYnFalse(ItemsJpaEntity item); + + @Query("SELECT c FROM ChatChannelJpaEntity c " + + "WHERE c.item = :item " + + "AND c.cloYn = false " + + "AND c.delYn = false " + + "AND c.startedAt = :startDate " + + "AND c.endedAt = :endDate") + List findByItemAndStartAndEndDate(ItemsJpaEntity item, LocalDate startDate, LocalDate endDate); +} diff --git a/src/main/java/site/billbill/apiserver/repository/user/UserRepository.java b/src/main/java/site/billbill/apiserver/repository/user/UserRepository.java index 9e56503..c5c0d37 100644 --- a/src/main/java/site/billbill/apiserver/repository/user/UserRepository.java +++ b/src/main/java/site/billbill/apiserver/repository/user/UserRepository.java @@ -10,4 +10,5 @@ public interface UserRepository extends JpaRepository, UserDslRepository { Optional findByUserIdAndWithdrawStatus(String userId, boolean withdrawStatus); Optional findByProviderId(String providerId); + boolean existsByNickname(String nickname); } diff --git a/src/main/java/site/billbill/apiserver/repository/user/UserSearchHistRepository.java b/src/main/java/site/billbill/apiserver/repository/user/UserSearchHistRepository.java new file mode 100644 index 0000000..9e8d1c4 --- /dev/null +++ b/src/main/java/site/billbill/apiserver/repository/user/UserSearchHistRepository.java @@ -0,0 +1,12 @@ +package site.billbill.apiserver.repository.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import site.billbill.apiserver.model.user.UserJpaEntity; +import site.billbill.apiserver.model.user.UserSearchHistJpaEntity; + +import java.util.List; + + +public interface UserSearchHistRepository extends JpaRepository { + List findByUserAndDelYnOrderByCreatedAtDesc(UserJpaEntity user,boolean delYn); +}