diff --git a/src/main/java/com/api/farmingsoon/common/interceptor/StompInterceptor.java b/src/main/java/com/api/farmingsoon/common/interceptor/StompInterceptor.java index 94b5fa1..dd056c2 100644 --- a/src/main/java/com/api/farmingsoon/common/interceptor/StompInterceptor.java +++ b/src/main/java/com/api/farmingsoon/common/interceptor/StompInterceptor.java @@ -6,6 +6,7 @@ import com.api.farmingsoon.common.util.CookieUtils; import com.api.farmingsoon.common.util.JwtUtils; import com.api.farmingsoon.domain.chatroom.event.ChatRoomConnectEvent; +import com.api.farmingsoon.domain.chatroom.event.ChatRoomDisConnectEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -28,17 +29,26 @@ public class StompInterceptor implements ChannelInterceptor { @Override public Message preSend(Message message, MessageChannel channel) { StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); - log.info("command : " + accessor.getCommand()); - - - if (StompCommand.CONNECT.equals(accessor.getCommand())) + StompCommand command = accessor.getCommand(); + log.info("command : " + command); + /** + * @Description + * 1. 모든 메시지 읽음 처리 + * 2. Redis에 채팅방 참여 정보 저장 + */ + if (StompCommand.CONNECT.equals(command)) eventPublisher.publishEvent(ChatRoomConnectEvent.builder() - .memberId(Long.valueOf(accessor.getFirstNativeHeader("memberId"))) - .chatRoomId(Long.valueOf(accessor.getFirstNativeHeader("chatRoomId"))) - .build() + .connectMemberId(Long.valueOf(accessor.getFirstNativeHeader("memberId"))) + .chatRoomId(Long.valueOf(accessor.getFirstNativeHeader("chatRoomId"))) + .sessionId(accessor.getSessionId()) + .build() ); - - + else if (StompCommand.DISCONNECT.equals(command)) { + eventPublisher.publishEvent(ChatRoomDisConnectEvent.builder() + .chatRoomId(Long.valueOf(accessor.getFirstNativeHeader("chatRoomId"))) + .sessionId(accessor.getSessionId()) + .build()); + } return message; } diff --git a/src/main/java/com/api/farmingsoon/common/redis/RedisService.java b/src/main/java/com/api/farmingsoon/common/redis/RedisService.java index c2bb808..2de2fd0 100644 --- a/src/main/java/com/api/farmingsoon/common/redis/RedisService.java +++ b/src/main/java/com/api/farmingsoon/common/redis/RedisService.java @@ -42,21 +42,27 @@ public Set getKeySet(String domain) { return redisTemplate.keys(domain); } - public boolean isExistsKey(String key){ - return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + public boolean isNotExistsKey(String key){ + return Boolean.FALSE.equals(redisTemplate.hasKey(key)); } - public void addToSet(String key, Long itemId){ - if(!isExistsKey(key)) {// 키가 없다면(set이 없다면) - redisTemplate.opsForSet().add(key, String.valueOf(itemId)); // set생성 - redisTemplate.expire(key, TimeUtils.getRemainingTimeUntilMidnight(), TimeUnit.SECONDS); // 만료기간 설정 - } - else // 기존 키 값으로 된 set에 추가 - redisTemplate.opsForSet().add(key,String.valueOf(itemId)); - + public void addToSet(String key, String value){ + redisTemplate.opsForSet().add(key,value); + } + public void setExpireTime(String key, Long ttl){ + redisTemplate.expire(key, ttl , TimeUnit.SECONDS); // 만료기간 설정 } - public boolean isExistInSet(String key, Long itemId){ - return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, String.valueOf(itemId))); + public void createSet(String key, String value){ + redisTemplate.opsForSet().add(key, value); // set생성 + } + public void deleteToSet(String key, String value){ + redisTemplate.opsForSet().remove(key, value); + } + public Long getSetSize(String key){ + return redisTemplate.opsForSet().size(key); } + public boolean isNotExistInSet(String key, String value){ + return Boolean.FALSE.equals(redisTemplate.opsForSet().isMember(key, value)); + } } diff --git a/src/main/java/com/api/farmingsoon/common/redis/listener/KeyExpirationListener.java b/src/main/java/com/api/farmingsoon/common/redis/listener/KeyExpirationListener.java index 4268754..1d09c06 100644 --- a/src/main/java/com/api/farmingsoon/common/redis/listener/KeyExpirationListener.java +++ b/src/main/java/com/api/farmingsoon/common/redis/listener/KeyExpirationListener.java @@ -26,7 +26,7 @@ public void onMessage(Message key, byte[] pattern) { String[] expiredKey = key.toString().split("_"); if (expiredKey[0].equals("bidEnd")) { applicationEventPublisher.publishEvent(new BidEndKeyExpiredEvent((Long.valueOf(expiredKey[1])))); ; // itemId - } else if (expiredKey[0].equals("chatting")) { // memberId + } else if (expiredKey[0].equals("debouncing")) { // memberId applicationEventPublisher.publishEvent(new ChatNotificationDebounceKeyExpiredEvent(Long.valueOf(expiredKey[1]))); // receiverId } } diff --git a/src/main/java/com/api/farmingsoon/domain/chat/controller/ChatController.java b/src/main/java/com/api/farmingsoon/domain/chat/controller/ChatController.java index 580a9c0..33d5825 100644 --- a/src/main/java/com/api/farmingsoon/domain/chat/controller/ChatController.java +++ b/src/main/java/com/api/farmingsoon/domain/chat/controller/ChatController.java @@ -25,10 +25,5 @@ public class ChatController { public void sendMessage(ChatMessageRequest chatMessageRequest) { chatService.create(chatMessageRequest); } - @MessageMapping("/chat/read") - public void readMessage(ReadMessageRequest readMessageRequest) { - chatService.read(readMessageRequest); - } - } diff --git a/src/main/java/com/api/farmingsoon/domain/chat/dto/ChatResponse.java b/src/main/java/com/api/farmingsoon/domain/chat/dto/ChatResponse.java index 54a0912..2b73307 100644 --- a/src/main/java/com/api/farmingsoon/domain/chat/dto/ChatResponse.java +++ b/src/main/java/com/api/farmingsoon/domain/chat/dto/ChatResponse.java @@ -17,6 +17,7 @@ public class ChatResponse { private Long senderId; private Boolean isRead; private LocalDateTime createAt; + private final String type = "SEND"; @Builder private ChatResponse(Long senderId, String message, Boolean isRead, Long chatId, LocalDateTime createAt) { diff --git a/src/main/java/com/api/farmingsoon/domain/chat/dto/ChattingConnectResponse.java b/src/main/java/com/api/farmingsoon/domain/chat/dto/ChattingConnectResponse.java new file mode 100644 index 0000000..ca4bcf7 --- /dev/null +++ b/src/main/java/com/api/farmingsoon/domain/chat/dto/ChattingConnectResponse.java @@ -0,0 +1,16 @@ +package com.api.farmingsoon.domain.chat.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class ChattingConnectResponse { + + private Long connectMemberId; + private final String type = "SEND"; + + public ChattingConnectResponse(Long connectMemberId) { + this.connectMemberId = connectMemberId; + } +} diff --git a/src/main/java/com/api/farmingsoon/domain/chat/listener/ChatEventListener.java b/src/main/java/com/api/farmingsoon/domain/chat/listener/ChatEventListener.java index fe57d87..f6f0e84 100644 --- a/src/main/java/com/api/farmingsoon/domain/chat/listener/ChatEventListener.java +++ b/src/main/java/com/api/farmingsoon/domain/chat/listener/ChatEventListener.java @@ -25,8 +25,8 @@ public class ChatEventListener { public void sendChatAndDebounceNotification(ChatSaveEvent event) throws InterruptedException { messagingTemplate.convertAndSend("/sub/chat-room/" + event.getChatRoomId(), event.getChatResponse()); - if(!redisService.isExistsKey("chatting_" + event.getReceiverId())) // 알림 디바운싱 - redisService.setData("chatting_" + event.getReceiverId(),"", 2L,TimeUnit.SECONDS); + if(redisService.isNotExistsKey("debouncing_" + event.getReceiverId())) // 알림 디바운싱 + redisService.setData("debouncing_" + event.getReceiverId(),"", 2L,TimeUnit.SECONDS); } } \ No newline at end of file diff --git a/src/main/java/com/api/farmingsoon/domain/chat/service/ChatService.java b/src/main/java/com/api/farmingsoon/domain/chat/service/ChatService.java index f1c5dc5..10523d2 100644 --- a/src/main/java/com/api/farmingsoon/domain/chat/service/ChatService.java +++ b/src/main/java/com/api/farmingsoon/domain/chat/service/ChatService.java @@ -10,9 +10,11 @@ import com.api.farmingsoon.domain.chat.model.Chat; import com.api.farmingsoon.domain.chat.repository.ChatRepository; import com.api.farmingsoon.domain.chatroom.model.ChatRoom; +import com.api.farmingsoon.domain.chatroom.service.ChatRoomRedisService; import com.api.farmingsoon.domain.chatroom.service.ChatRoomService; import com.api.farmingsoon.domain.member.model.Member; import com.api.farmingsoon.domain.member.service.MemberService; +import com.api.farmingsoon.domain.notification.event.NotReadChatEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -31,23 +33,31 @@ public class ChatService { private final ChatRoomService chatRoomService; private final MemberService memberService; private final ApplicationEventPublisher eventPublisher; + private final ChatRoomRedisService chatRoomRedisService; @Transactional public void create(ChatMessageRequest chatMessageRequest) { + Long connectMemberSize = chatRoomRedisService.getConnectMemberSize("chatRoom_" + chatMessageRequest.getChatRoomId()); + ChatRoom chatRoom = chatRoomService.getChatRoom(chatMessageRequest.getChatRoomId()); Member sender = memberService.getMemberById(chatMessageRequest.getSenderId()); Chat chat = chatRepository.save( Chat.builder(). sender(sender) .message(chatMessageRequest.getMessage()) - .isRead(false) + .isRead(connectMemberSize == 2) // 채팅방에 둘 모두 존재한다면 읽음으로 처리 .chatRoom(chatRoom).build() ); + Member receiver = ChatRoom.resolveToReceiver(chatRoom, sender.getEmail()); + + if(connectMemberSize == 1) // 채팅방에 상대방이 없다면 알림을 전송 + eventPublisher.publishEvent(new NotReadChatEvent(receiver.getId())); + eventPublisher.publishEvent( ChatSaveEvent.builder() .chatRoomId(chatMessageRequest.getChatRoomId()) - .receiverId(ChatRoom.resolveToReceiver(chatRoom, sender.getEmail()).getId()) + .receiverId(receiver.getId()) .chatResponse(ChatResponse.of(chat)) .build()); @@ -58,13 +68,6 @@ public ChatListResponse getChats(Long chatRoomId, Pageable pageable) { return ChatListResponse.of(chatRepository.findByChatRoomOrderByIdAsc(chatRoom, pageable)); } - @Transactional - public void read(ReadMessageRequest readMessageRequest) { - Chat chat = chatRepository.findById(readMessageRequest.getChatId()).orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_CHAT)); - chat.read(); - log.info(chat.getId() + " "+ chat.getIsRead()); - } - @Transactional public void readAllMyNotReadChatList(Long chatRoomId, Long memberId) { ChatRoom chatRoom = chatRoomService.getChatRoom(chatRoomId); diff --git a/src/main/java/com/api/farmingsoon/domain/chatroom/event/ChatRoomConnectEvent.java b/src/main/java/com/api/farmingsoon/domain/chatroom/event/ChatRoomConnectEvent.java index 6f189f7..6062472 100644 --- a/src/main/java/com/api/farmingsoon/domain/chatroom/event/ChatRoomConnectEvent.java +++ b/src/main/java/com/api/farmingsoon/domain/chatroom/event/ChatRoomConnectEvent.java @@ -8,12 +8,14 @@ @Getter public class ChatRoomConnectEvent { - private Long memberId; + private Long connectMemberId; private Long chatRoomId; + private String sessionId; @Builder - private ChatRoomConnectEvent(Long memberId, Long chatRoomId) { - this.memberId = memberId; + private ChatRoomConnectEvent(Long connectMemberId, Long chatRoomId, String sessionId) { + this.connectMemberId = connectMemberId; this.chatRoomId = chatRoomId; + this.sessionId = sessionId; } } diff --git a/src/main/java/com/api/farmingsoon/domain/chatroom/event/ChatRoomDisConnectEvent.java b/src/main/java/com/api/farmingsoon/domain/chatroom/event/ChatRoomDisConnectEvent.java new file mode 100644 index 0000000..8bd20dc --- /dev/null +++ b/src/main/java/com/api/farmingsoon/domain/chatroom/event/ChatRoomDisConnectEvent.java @@ -0,0 +1,19 @@ +package com.api.farmingsoon.domain.chatroom.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class ChatRoomDisConnectEvent { + + private Long chatRoomId; + private String sessionId; + + @Builder + private ChatRoomDisConnectEvent(Long chatRoomId, String sessionId) { + this.chatRoomId = chatRoomId; + this.sessionId = sessionId; + } +} diff --git a/src/main/java/com/api/farmingsoon/domain/chatroom/listener/ChatRoomEventListener.java b/src/main/java/com/api/farmingsoon/domain/chatroom/listener/ChatRoomEventListener.java index dcac40a..1903fc7 100644 --- a/src/main/java/com/api/farmingsoon/domain/chatroom/listener/ChatRoomEventListener.java +++ b/src/main/java/com/api/farmingsoon/domain/chatroom/listener/ChatRoomEventListener.java @@ -1,20 +1,40 @@ package com.api.farmingsoon.domain.chatroom.listener; +import com.api.farmingsoon.common.sse.SseService; +import com.api.farmingsoon.domain.chat.dto.ChattingConnectResponse; import com.api.farmingsoon.domain.chat.service.ChatService; import com.api.farmingsoon.domain.chatroom.event.ChatRoomConnectEvent; +import com.api.farmingsoon.domain.chatroom.service.ChatRoomRedisService; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Component; @RequiredArgsConstructor @Component public class ChatRoomEventListener { private final ChatService chatService; + private final ChatRoomRedisService chatRoomRedisService; + private final SimpMessagingTemplate messagingTemplate; + + + /** + * @Description + * 1. 채팅방에 연결됐음을 저장 + * 2. 연결된 사람의 채팅방에 안읽었던 메시지를 모두 읽음 처리 + * 3. 채팅방에 상대방이 연결되었음을 알리는 알림 + */ + @EventListener + public void readAllChatAndSaveConnectMember(ChatRoomConnectEvent event){ + chatRoomRedisService.connectChatRoom(event.getChatRoomId(), event.getSessionId()); + chatService.readAllMyNotReadChatList(event.getChatRoomId(), event.getConnectMemberId()); + messagingTemplate.convertAndSend("/sub/chat-room/" + event.getChatRoomId(), new ChattingConnectResponse(event.getConnectMemberId())); + + } @EventListener - public void readAllChatMessage(ChatRoomConnectEvent event){ - chatService.readAllMyNotReadChatList(event.getChatRoomId(), event.getMemberId()); + public void deleteConnectMember(ChatRoomConnectEvent event){ + chatRoomRedisService.disConnectChatRoom(event.getChatRoomId(), event.getSessionId()); } } diff --git a/src/main/java/com/api/farmingsoon/domain/chatroom/service/ChatRoomRedisService.java b/src/main/java/com/api/farmingsoon/domain/chatroom/service/ChatRoomRedisService.java new file mode 100644 index 0000000..d36156d --- /dev/null +++ b/src/main/java/com/api/farmingsoon/domain/chatroom/service/ChatRoomRedisService.java @@ -0,0 +1,28 @@ +package com.api.farmingsoon.domain.chatroom.service; + +import com.api.farmingsoon.common.redis.RedisService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ChatRoomRedisService { + private final RedisService redisService; + + public void connectChatRoom(Long chatRoomId, String sessionId) { + redisService.addToSet("chatRoom_" + chatRoomId, sessionId); + } + + /** + * 채팅방에 남은 사람이 한명이라면 나갈 때 키 삭제 + */ + public void disConnectChatRoom(Long chatRoomId, String sessionId) { + if(redisService.getSetSize("chatRoom_" + chatRoomId) == 1){ + redisService.deleteData("chatRoom_" + chatRoomId); + } + redisService.deleteToSet("chatRoom_" + chatRoomId, sessionId); + } + public Long getConnectMemberSize(String key){ + return redisService.getSetSize(key); + } +} diff --git a/src/main/java/com/api/farmingsoon/domain/item/controller/ItemController.java b/src/main/java/com/api/farmingsoon/domain/item/controller/ItemController.java index 3a4d406..794dc58 100644 --- a/src/main/java/com/api/farmingsoon/domain/item/controller/ItemController.java +++ b/src/main/java/com/api/farmingsoon/domain/item/controller/ItemController.java @@ -6,7 +6,6 @@ import com.api.farmingsoon.common.util.CookieUtils; import com.api.farmingsoon.domain.item.dto.*; import com.api.farmingsoon.domain.item.service.ItemService; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -18,8 +17,6 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import java.util.Optional; - @RestController @Slf4j @RequestMapping("/api/items") @@ -60,6 +57,7 @@ public Response getItemList( ItemListResponse items = itemService.getItemList(category, keyword, pageable, sortcode); return Response.success(HttpStatus.OK, "상품 목록 조회 성공!", items); } + @GetMapping("/me") public Response getMyItemList( @PageableDefault(size = 12, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { @@ -87,5 +85,16 @@ public Response soldOut(@PathVariable(name = "itemId") Long itemId, @Reque itemService.soldOut(itemId, buyerId); return Response.success(HttpStatus.OK, "상품 판매 완료!"); } +/* + @GetMapping("/test") + public Response getItemListBySubQuery( + @PageableDefault(size = 12) Pageable pageable, + @RequestParam(value = "sortcode", defaultValue = "recent") String sortcode, + @RequestParam(value = "category", required = false) String category, + @RequestParam(value = "keyword", required = false) String keyword) { + ItemListBySubQueryResponse itemListBySubQuery = itemService.getItemListBySubQuery(category, keyword, pageable, sortcode); + return Response.success(HttpStatus.OK, "상품 목록 조회 성공!", itemListBySubQuery); + } + */ } diff --git a/src/main/java/com/api/farmingsoon/domain/item/domain/Item.java b/src/main/java/com/api/farmingsoon/domain/item/domain/Item.java index c538968..bf34479 100644 --- a/src/main/java/com/api/farmingsoon/domain/item/domain/Item.java +++ b/src/main/java/com/api/farmingsoon/domain/item/domain/Item.java @@ -7,6 +7,7 @@ import com.api.farmingsoon.domain.member.model.Member; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; @@ -57,8 +58,10 @@ public class Item extends BaseTimeEntity { // *Todo 양방향 안쓰는 쪽으로 고려해보기 @OneToMany(mappedBy = "item") + @BatchSize(size = 12) private List bidList; + @BatchSize(size = 12) @OneToMany(mappedBy = "item") private List likeableItemList; diff --git a/src/main/java/com/api/farmingsoon/domain/item/dto/ItemBySubQueryResponse.java b/src/main/java/com/api/farmingsoon/domain/item/dto/ItemBySubQueryResponse.java new file mode 100644 index 0000000..57efa91 --- /dev/null +++ b/src/main/java/com/api/farmingsoon/domain/item/dto/ItemBySubQueryResponse.java @@ -0,0 +1,45 @@ +package com.api.farmingsoon.domain.item.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + + +@NoArgsConstructor +@Getter +public class ItemBySubQueryResponse { + private Long itemId; // 상품 접근 + private String title; + private String description; + private LocalDateTime expiredAt; + private Integer highestPrice; + private Integer hopePrice; + private Integer lowestPrice; + private String itemStatus; + private Integer bidCount; + private Integer likeCount; + private Integer viewCount; + private String thumbnailImgUrl; + private Boolean likeStatus; + + @QueryProjection + public ItemBySubQueryResponse(Long itemId, String title, String description, LocalDateTime expiredAt, Integer highestPrice, Integer hopePrice, Integer lowestPrice, String itemStatus, Integer bidCount, Integer likeCount, Integer viewCount, String thumbnailImgUrl, Boolean likeStatus) { + this.itemId = itemId; + this.title = title; + this.description = description; + this.expiredAt = expiredAt; + this.highestPrice = highestPrice; + this.hopePrice = hopePrice; + this.lowestPrice = lowestPrice; + this.itemStatus = itemStatus; + this.bidCount = bidCount; + this.likeCount = likeCount; + this.viewCount = viewCount; + this.thumbnailImgUrl = thumbnailImgUrl; + this.likeStatus = likeStatus; + } + + +} diff --git a/src/main/java/com/api/farmingsoon/domain/item/dto/ItemListBySubQueryResponse.java b/src/main/java/com/api/farmingsoon/domain/item/dto/ItemListBySubQueryResponse.java new file mode 100644 index 0000000..ac9883c --- /dev/null +++ b/src/main/java/com/api/farmingsoon/domain/item/dto/ItemListBySubQueryResponse.java @@ -0,0 +1,20 @@ +package com.api.farmingsoon.domain.item.dto; + +import com.api.farmingsoon.common.pagenation.Pagination; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class ItemListBySubQueryResponse { + + private Pagination pagination; // 페이지 관련 데이터 + private List itemBySubQueryResponseList; // 페이지 관련 데이터 + + public ItemListBySubQueryResponse(Pagination pagination, List itemBySubQueryResponseList) { + this.pagination = pagination; + this.itemBySubQueryResponseList = itemBySubQueryResponseList; + } +} diff --git a/src/main/java/com/api/farmingsoon/domain/item/repository/ItemRepositoryCustom.java b/src/main/java/com/api/farmingsoon/domain/item/repository/ItemRepositoryCustom.java index b330546..eb0ede2 100644 --- a/src/main/java/com/api/farmingsoon/domain/item/repository/ItemRepositoryCustom.java +++ b/src/main/java/com/api/farmingsoon/domain/item/repository/ItemRepositoryCustom.java @@ -1,6 +1,7 @@ package com.api.farmingsoon.domain.item.repository; import com.api.farmingsoon.domain.item.domain.Item; +import com.api.farmingsoon.domain.item.dto.ItemBySubQueryResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -13,4 +14,6 @@ public interface ItemRepositoryCustom { List findNotEndBidItemList(); List findBiddingItemList(); + + //Page findItemListBySubQuery(String category, String keyword, Pageable pageable, String sortcode); } diff --git a/src/main/java/com/api/farmingsoon/domain/item/repository/ItemRepositoryCustomImpl.java b/src/main/java/com/api/farmingsoon/domain/item/repository/ItemRepositoryCustomImpl.java index 5689d28..31fbc8c 100644 --- a/src/main/java/com/api/farmingsoon/domain/item/repository/ItemRepositoryCustomImpl.java +++ b/src/main/java/com/api/farmingsoon/domain/item/repository/ItemRepositoryCustomImpl.java @@ -2,16 +2,17 @@ import com.api.farmingsoon.domain.item.domain.Item; import com.api.farmingsoon.domain.item.domain.ItemStatus; +import com.api.farmingsoon.domain.item.dto.ItemBySubQueryResponse; import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import java.time.LocalDateTime; import java.util.ArrayList; @@ -19,7 +20,6 @@ import static com.api.farmingsoon.domain.bid.model.QBid.bid; import static com.api.farmingsoon.domain.item.domain.QItem.item; -import static com.api.farmingsoon.domain.member.model.QMember.member; @Slf4j @RequiredArgsConstructor @@ -27,11 +27,11 @@ public class ItemRepositoryCustomImpl implements ItemRepositoryCustom { private final JPAQueryFactory queryFactory; + @Override public Page findItemList(String category, String keyword, Pageable pageable, String sortcode) { List content = queryFactory .selectFrom(item) - .innerJoin(item.member, member).fetchJoin() .leftJoin(item.bidList, bid) .where(eqCategory(category), containsKeyword(keyword)) .groupBy(item.id) @@ -43,14 +43,48 @@ public Page findItemList(String category, String keyword, Pageable pageabl Long total = queryFactory .select(item.count()) .from(item) - .innerJoin(item.member, member) .where(eqCategory(category), containsKeyword(keyword)) .fetchOne(); return new PageImpl<>(content, pageable, total); } +/* @Override + public Page findItemResponseList(String category, String keyword, Pageable pageable, String sortcode) { + List list = queryFactory.select( + new QItemResponseBySubQuery( + item.id, + item.title, + item.description, + item.expiredAt, + //queryFactory.select(item.bidList.any().price.max()).from(bid), + queryFactory.select(bid.price.max()).from(bid).where(bid.item.eq(item)), + item.hopePrice, + queryFactory.select(item.bidList.any().price.min()).from(bid), + item.itemStatus.stringValue(), + item.bidList.size(), + item.likeableItemList.size(), + item.viewCount, + item.thumbnailImageUrl, + Expressions.FALSE + ) + ).from(item) + .where(eqCategory(category), containsKeyword(keyword)) + .groupBy(item.id) + .orderBy(getAllOrderSpecifiers(sortcode)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(item.count()) + .from(item) + .where(eqCategory(category), containsKeyword(keyword)) + .fetchOne(); + + return new PageImpl<>(list, pageable, total); + }*/ @Override public List findNotEndBidItemList() { return queryFactory.selectFrom(item) @@ -65,6 +99,8 @@ public List findBiddingItemList() { .fetch(); } + + private BooleanExpression eqCategory(String category) { log.debug("카테고리: {}", category); return category != null ? item.category.eq(category) : null; diff --git a/src/main/java/com/api/farmingsoon/domain/item/service/ItemRedisService.java b/src/main/java/com/api/farmingsoon/domain/item/service/ItemRedisService.java index d1beb92..c6e7c30 100644 --- a/src/main/java/com/api/farmingsoon/domain/item/service/ItemRedisService.java +++ b/src/main/java/com/api/farmingsoon/domain/item/service/ItemRedisService.java @@ -1,6 +1,7 @@ package com.api.farmingsoon.domain.item.service; import com.api.farmingsoon.common.redis.RedisService; +import com.api.farmingsoon.common.util.TimeUtils; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -17,13 +18,27 @@ public void setBidEndTime(Long itemId, Integer expire){ } - // @Description 중복된 접근이 아니라면 조회수를 증가시키고 접근 처리 + /** + * @Description + * 1. 중복된 접근이 아니라면 조회수를 증가시키고 접근 처리 + * 2. set이 없다면 만들고 만료기간 자정으로 설정 + * 3. 있다면 추가 + */ + @Async("testExecutor") public void handleViewCount(String cookieValueOfViewer, Long itemId) { - if (!redisService.isExistInSet(cookieValueOfViewer, itemId)) + if (redisService.isNotExistInSet(cookieValueOfViewer, String.valueOf(itemId))) { redisService.increaseData("viewCount_item_" + itemId); - redisService.addToSet(cookieValueOfViewer, itemId); + if(redisService.isNotExistsKey(cookieValueOfViewer)) + { + redisService.createSet(cookieValueOfViewer, String.valueOf(itemId)); + redisService.setExpireTime(cookieValueOfViewer, TimeUtils.getRemainingTimeUntilMidnight()); + } + else + { + redisService.addToSet(cookieValueOfViewer, String.valueOf(itemId)); + } } } } diff --git a/src/main/java/com/api/farmingsoon/domain/item/service/ItemService.java b/src/main/java/com/api/farmingsoon/domain/item/service/ItemService.java index 0a7397a..e6bd621 100644 --- a/src/main/java/com/api/farmingsoon/domain/item/service/ItemService.java +++ b/src/main/java/com/api/farmingsoon/domain/item/service/ItemService.java @@ -1,5 +1,6 @@ package com.api.farmingsoon.domain.item.service; +import com.api.farmingsoon.common.pagenation.Pagination; import com.api.farmingsoon.common.redis.RedisService; import com.api.farmingsoon.common.util.Transaction; import com.api.farmingsoon.domain.item.dto.*; @@ -24,13 +25,9 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import java.time.Duration; -import java.time.LocalDateTime; import java.util.*; -import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor @@ -81,7 +78,9 @@ public Long saveItemAndImage(Item item, List imageUrls) { @Transactional(readOnly = true) public ItemListResponse getItemList(String category, String keyword, Pageable pageable, String sortcode) { Optional viewer = authenticationUtils.getOptionalMember(); - return ItemListResponse.of(itemRepository.findItemList(category, keyword, pageable, sortcode), viewer); + Page itemList = itemRepository.findItemList(category, keyword, pageable, sortcode); + + return ItemListResponse.of(itemList, viewer); } @Transactional(readOnly = true) public ItemDetailResponse getItemDetail(Long itemId) { @@ -175,4 +174,12 @@ public void bidEndByScheduler(){ public List findBiddingItemList(){ return itemRepository.findBiddingItemList(); } +/* + @Description 서브쿼리 테스트 전용 + public ItemListBySubQueryResponse getItemListBySubQuery(String category, String keyword, Pageable pageable, String sortcode) { + Page itemResponseList = itemRepository.findItemListBySubQuery(category, keyword, pageable, sortcode); + return new ItemListBySubQueryResponse(Pagination.of(itemResponseList), itemResponseList.getContent()); + + } +*/ } diff --git a/src/main/java/com/api/farmingsoon/domain/notification/event/NotReadChatEvent.java b/src/main/java/com/api/farmingsoon/domain/notification/event/NotReadChatEvent.java new file mode 100644 index 0000000..11afcc2 --- /dev/null +++ b/src/main/java/com/api/farmingsoon/domain/notification/event/NotReadChatEvent.java @@ -0,0 +1,15 @@ +package com.api.farmingsoon.domain.notification.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class NotReadChatEvent { + + private Long receiverId; + + public NotReadChatEvent(Long receiverId) { + this.receiverId = receiverId; + } +} diff --git a/src/main/java/com/api/farmingsoon/domain/notification/listener/NotificationEventListener.java b/src/main/java/com/api/farmingsoon/domain/notification/listener/NotificationEventListener.java index ae483a7..0c3b429 100644 --- a/src/main/java/com/api/farmingsoon/domain/notification/listener/NotificationEventListener.java +++ b/src/main/java/com/api/farmingsoon/domain/notification/listener/NotificationEventListener.java @@ -2,6 +2,7 @@ import com.api.farmingsoon.common.sse.SseService; import com.api.farmingsoon.domain.notification.event.ChatNotificationDebounceKeyExpiredEvent; +import com.api.farmingsoon.domain.notification.event.NotReadChatEvent; import com.api.farmingsoon.domain.notification.event.NotificationSaveEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,8 +27,13 @@ public void sendNotification(NotificationSaveEvent event) throws InterruptedExce } @EventListener - public void sendChatNotification(ChatNotificationDebounceKeyExpiredEvent event) throws InterruptedException { - sseService.sendToClient("CHATTING", event.getReceiverId(), "새로운 채팅 메시지가 있습니다."); - log.info("채팅 알림 전송"); + public void sendChatRoomUpdateNotification(ChatNotificationDebounceKeyExpiredEvent event) throws InterruptedException { + sseService.sendToClient("CHATROOM_UPDATE", event.getReceiverId(), "채팅방 목록을 업데이트 해주세요."); + log.info("채팅방 업데이트 알림 전송"); + } + @EventListener + public void sendNewChatNotification(NotReadChatEvent event) throws InterruptedException { + sseService.sendToClient("NEW_CHAT", event.getReceiverId(), "새로운 채팅 메시지가 있습니다."); + log.info("새로운 채팅 알림 전송"); } } diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 4a87d3d..6fd5379 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -5,4 +5,6 @@ logging: level: org: hibernate: - SQL: debug + type: + descriptor: + sql: trace diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fd29d2e..62b7937 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,7 +11,12 @@ spring: h2: console: enabled: true - jpa: +# jpa: +# show-sql: true +# properties: +# hibernate: +# format_sql: true +# highlight_sql: true hibernate: ddl-auto: create # defer-datasource-initialization: true diff --git a/src/test/java/com/api/farmingsoon/domain/item/ItemIntegrationTest.java b/src/test/java/com/api/farmingsoon/domain/item/ItemIntegrationTest.java index fd232e7..6a34dbb 100644 --- a/src/test/java/com/api/farmingsoon/domain/item/ItemIntegrationTest.java +++ b/src/test/java/com/api/farmingsoon/domain/item/ItemIntegrationTest.java @@ -124,8 +124,20 @@ void beforeEach(){ .build(); } + @DisplayName("상품 등록 후 상세 조회 성공") + @WithUserDetails(value = "user1@naver.com", setupBefore = TestExecutionEvent.TEST_EXECUTION) + @Test + void getItems() throws Exception { + + MvcResult mvcResult2 = mockMvc.perform(get("/api/items")) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + } + @DisplayName("상품 등록 성공") @WithUserDetails(value = "user1@naver.com", setupBefore = TestExecutionEvent.TEST_EXECUTION) @Test