Skip to content

Commit

Permalink
[feat] 5주차 리뷰사항 개선 (#110)
Browse files Browse the repository at this point in the history
* #106 fix: ChatRoom 생성자 매개변수 변경

* #106 fix: null 처리

* #106 fix: validateIsSeller 개선

* #106 fix: 상품을 찾지 못한 경우 에러 코드를 NOT_FOUND로 설정

* #106 fix: 주석 제거

* #106 fix: 중복된 메소드 제거

* #106 fix: deleteAllInBatch 제거

* #106 fix: 매직넘버 제거

* #106 fix: 정적 임포트 제거

* #106 fix: 레디스 서비스에서 Oauth 도메인 부분을 따로 빼서 OauthRedisService로 분리

* #106 fix: 레디스 서비스 도메인별 분리

* #106 fix: 회원 동네 선택시 쿼리 개선

* #106 fix: 디미터의 법칙 개선

* [feat] 채팅 목록 조회 API 구현 (#113)

* [fix] 상품 목록 조회시 tradingRegion, status 응답형식 변경 (#108)

* #107 fix: regions.csv 수정

* #107 fix: 응답형식 문제 해결

* #111 fix: 썸네일 저장 로직 추가 (#112)

* #96 feat: 한 채팅방안에 채팅 메시지 조회 API 구현

* #96 feat: messageIndex를 이용한 채팅 로그 목록 조회

* #96 feat: toString 정의

* #96 test: readMessages 테스트 코드 추가

* #96 feat: 채팅 메시지 목록 요청시 대기시간 10초로 증가 및 조건문 추가

- 조건문 내용은 messageIndex가 채팅 리스트의 개수보다 큰 경우 빈 컬렉션을 반환하도록 합니다.

* #96 fix: 응답형식 변경

* #96 fix: 응답형식 변경

* #114 fix: buyer추가
  • Loading branch information
yonghwankim-dev authored Sep 26, 2023
1 parent 4c4feb0 commit 50e71fb
Show file tree
Hide file tree
Showing 43 changed files with 661 additions and 260 deletions.
23 changes: 23 additions & 0 deletions backend/src/main/java/codesquard/app/annotation/MessageIndex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package codesquard.app.annotation;

import static java.lang.annotation.ElementType.*;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;

import codesquard.app.config.validation.MessageIndexValidator;

@Target(value = {PARAMETER, FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MessageIndexValidator.class)
public @interface MessageIndex {

String message() default "messageIndex는 0 이상이어야 합니다.";

Class[] groups() default {};

Class[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
package codesquard.app.api.chat;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import codesquard.app.annotation.MessageIndex;
import codesquard.app.api.chat.request.ChatLogSendRequest;
import codesquard.app.api.chat.response.ChatLogListResponse;
import codesquard.app.api.chat.response.ChatLogSendResponse;
import codesquard.app.api.response.ApiResponse;
import codesquard.app.domain.oauth.support.AuthPrincipal;
import codesquard.app.domain.oauth.support.Principal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Validated
@RequiredArgsConstructor
@RequestMapping("/api")
@RestController
public class ChatLogRestController {

private final Map<DeferredResult<ApiResponse<ChatLogListResponse>>, Integer> chatRequests = new ConcurrentHashMap<>();

private final ChatLogService chatLogService;

@ResponseStatus(HttpStatus.CREATED)
Expand All @@ -29,6 +43,32 @@ public ApiResponse<ChatLogSendResponse> sendMessage(
@RequestBody ChatLogSendRequest request,
@AuthPrincipal Principal sender) {
ChatLogSendResponse response = chatLogService.sendMessage(request, chatRoomId, sender);

this.chatRequests.forEach((key, messageIndex) ->
key.setResult(ApiResponse.ok("채팅 메시지 목록 조회가 완료되었습니다.",
chatLogService.readMessages(chatRoomId, messageIndex, sender))));
return ApiResponse.created("메시지 전송이 완료되었습니다.", response);
}

@GetMapping("/chats/{chatRoomId}")
public DeferredResult<ApiResponse<ChatLogListResponse>> readMessages(
@PathVariable Long chatRoomId,
@RequestParam(required = false, defaultValue = "0") @MessageIndex int messageIndex,
@AuthPrincipal Principal principal) {
log.info("메시지 읽기 요청 : chatRoomId={}, messageIndex={}, 요청한 아이디={}", chatRoomId, messageIndex,
principal.getLoginId());
DeferredResult<ApiResponse<ChatLogListResponse>> deferredResult = new DeferredResult<>(10000L);
this.chatRequests.put(deferredResult, messageIndex);

deferredResult.onCompletion(() -> chatRequests.remove(deferredResult));
deferredResult.onTimeout(() -> deferredResult.setErrorResult(
ApiResponse.of(HttpStatus.REQUEST_TIMEOUT, "새로운 채팅 메시지가 존재하지 않습니다.", null)));

ChatLogListResponse response = chatLogService.readMessages(chatRoomId, messageIndex, principal);

if (!response.isEmptyChat()) {
deferredResult.setResult(ApiResponse.ok("채팅 메시지 목록 조회가 완료되었습니다.", response));
}
return deferredResult;
}
}
70 changes: 63 additions & 7 deletions backend/src/main/java/codesquard/app/api/chat/ChatLogService.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
package codesquard.app.api.chat;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import codesquard.app.api.chat.request.ChatLogSendRequest;
import codesquard.app.api.chat.response.ChatLogItemResponse;
import codesquard.app.api.chat.response.ChatLogListResponse;
import codesquard.app.api.chat.response.ChatLogMessageResponse;
import codesquard.app.api.chat.response.ChatLogSendResponse;
import codesquard.app.api.errors.errorcode.ChatRoomErrorCode;
import codesquard.app.api.errors.errorcode.ItemErrorCode;
import codesquard.app.api.errors.exception.RestApiException;
import codesquard.app.domain.chat.ChatLog;
import codesquard.app.domain.chat.ChatLogRepository;
import codesquard.app.domain.chat.ChatRoom;
import codesquard.app.domain.chat.ChatRoomRepository;
import codesquard.app.domain.item.Item;
import codesquard.app.domain.item.ItemRepository;
import codesquard.app.domain.member.Member;
import codesquard.app.domain.oauth.support.Principal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -25,16 +35,62 @@ public class ChatLogService {

private final ChatLogRepository chatLogRepository;
private final ChatRoomRepository chatRoomRepository;
private final ItemRepository itemRepository;

@Transactional
public ChatLogSendResponse sendMessage(ChatLogSendRequest request, Long chatRoomId, Principal sender) {
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
ChatRoom chatRoom = findChatRoomBy(chatRoomId);
Member buyer = chatRoom.getBuyer();
Member seller = chatRoom.getItem().getMember();

ChatLog chatLog;
if (sender.isBuyer(buyer)) {
chatLog = new ChatLog(request.getMessage(), sender.getLoginId(), seller.getLoginId(), chatRoom);
} else {
chatLog = new ChatLog(request.getMessage(), sender.getLoginId(), chatRoom.getBuyerLoginId(), chatRoom);
}

return ChatLogSendResponse.from(chatLogRepository.save(chatLog));
}

public ChatLogListResponse readMessages(Long chatRoomId, int messageIndex, Principal principal) {
ChatRoom chatRoom = findChatRoomBy(chatRoomId);
Item item = findItemBy(chatRoom);

String chatPartnerName = getChatPartnerName(principal, item, chatRoom);
List<ChatLog> chatLogs = chatLogRepository.findAllByChatRoomIdOrderByCreatedAtAsc(chatRoomId);
log.debug("메시지 읽기에서 채팅 로그 결과 : chatLogs.size={}", chatLogs.size());
if (messageIndex > chatLogs.size()) {
return new ChatLogListResponse(chatPartnerName, ChatLogItemResponse.from(item), Collections.emptyList());
}

List<ChatLog> chatLogsAfterIndex = chatLogs.subList(messageIndex, chatLogs.size());
List<ChatLogMessageResponse> chats = IntStream.range(0, chatLogsAfterIndex.size())
.mapToObj(idx -> {
ChatLog chatLog = chatLogsAfterIndex.get(idx);
return ChatLogMessageResponse.from(idx, chatLog, principal);
}).collect(Collectors.toUnmodifiableList());

return new ChatLogListResponse(
chatPartnerName,
ChatLogItemResponse.from(item),
chats);
}

private ChatRoom findChatRoomBy(Long chatRoomId) {
return chatRoomRepository.findById(chatRoomId)
.orElseThrow(() -> new RestApiException(ChatRoomErrorCode.NOT_FOUND_CHATROOM));
String seller = chatRoom.getItem().getMember().getLoginId();
}

private Item findItemBy(ChatRoom chatRoom) {
return itemRepository.findById(chatRoom.getItem().getId())
.orElseThrow(() -> new RestApiException(ItemErrorCode.ITEM_NOT_FOUND));
}

ChatLog chatLog = new ChatLog(request.getMessage(), sender.getLoginId(), seller, LocalDateTime.now(),
chatRoom);
ChatLog saveChatLog = chatLogRepository.save(chatLog);
return ChatLogSendResponse.from(saveChatLog);
private String getChatPartnerName(Principal principal, Item item, ChatRoom chatRoom) {
if (principal.isSeller(item.getMember())) {
return chatRoom.getBuyer().getLoginId();
}
return item.getMember().getLoginId();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package codesquard.app.api.chat;

import java.time.LocalDateTime;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -36,7 +34,7 @@ public ChatRoomCreateResponse createChatRoom(Long itemId, Principal sender) {
Item item = findItemBy(itemId);
Member senderMember = findMemberBy(sender.getMemberId());

ChatRoom chatRoom = new ChatRoom(LocalDateTime.now(), senderMember, item);
ChatRoom chatRoom = new ChatRoom(senderMember, item);

ChatRoom saveChatRoom = chatRoomRepository.save(chatRoom);
log.debug("채팅방 저장 결과 : chatRoom={}", saveChatRoom);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package codesquard.app.api.chat.response;

import codesquard.app.domain.item.Item;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ChatLogItemResponse {
private String title;
private String thumbnailUrl;
private Long price;

public static ChatLogItemResponse from(Item item) {
return new ChatLogItemResponse(item.getTitle(), item.getThumbnailUrl(), item.getPrice());
}

@Override
public String toString() {
return String.format("%s, %s(title=%s, price=%d)", "채팅 메시지 아이템 응답", this.getClass().getSimpleName(), title,
price);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package codesquard.app.api.chat.response;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnore;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class ChatLogListResponse {
private String chatPartnerName;
private ChatLogItemResponse item;
private List<ChatLogMessageResponse> chat;

@JsonIgnore
public boolean isEmptyChat() {
return chat.isEmpty();
}

@Override
public String toString() {
return String.format("%s, %s(chatPartnerName=%s, item=%s, chat=%s)", "채팅 메시지 목록 응답",
this.getClass().getSimpleName(), chatPartnerName, item, chat);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package codesquard.app.api.chat.response;

import com.fasterxml.jackson.annotation.JsonProperty;

import codesquard.app.domain.chat.ChatLog;
import codesquard.app.domain.oauth.support.Principal;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ChatLogMessageResponse {
private int messageIndex;
private boolean isMe;
private String message;

public static ChatLogMessageResponse from(int messageIndex, ChatLog chatLog, Principal principal) {
boolean isMe = chatLog.isSender(principal.getLoginId());
return new ChatLogMessageResponse(messageIndex, isMe, chatLog.getMessage());
}

@JsonProperty("isMe")
public boolean isMe() {
return isMe;
}

@Override
public String toString() {
return String.format("%s, %s(messageIndex=%d, isMe=%s, message=%s)", "채팅 메시지 응답",
this.getClass().getSimpleName(), messageIndex, isMe, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.HashMap;
import java.util.Map;

import javax.validation.ConstraintViolationException;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand Down Expand Up @@ -42,4 +44,23 @@ public ResponseEntity<ApiResponse<Object>> handleMethodArgumentNotValidException
return ResponseEntity.badRequest().body(body);
}

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Object>> handleConstraintViolationException(
ConstraintViolationException exception) {
log.error("ConstraintViolationException 발생 : {}", exception.toString());
ApiResponse<Object> body = ApiResponse.of(
HttpStatus.BAD_REQUEST,
"유효하지 않은 입력형식입니다.",
exception.getConstraintViolations().stream()
.map(error -> {
Map<String, String> errors = new HashMap<>();

errors.put("field", error.getPropertyPath().toString());
errors.put("defaultMessage", error.getMessage());
return errors;
}).distinct()
);
return ResponseEntity.badRequest().body(body);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package codesquard.app.api.item;

import java.util.ArrayList;
import java.util.List;

import javax.validation.Valid;
Expand All @@ -26,7 +25,7 @@
import codesquard.app.api.item.request.ItemStatusModifyRequest;
import codesquard.app.api.item.response.ItemDetailResponse;
import codesquard.app.api.item.response.ItemResponses;
import codesquard.app.api.redis.RedisService;
import codesquard.app.api.redis.ItemViewRedisService;
import codesquard.app.api.response.ApiResponse;
import codesquard.app.domain.oauth.support.AuthPrincipal;
import codesquard.app.domain.oauth.support.Principal;
Expand All @@ -38,7 +37,7 @@
public class ItemController {

private final ItemService itemService;
private final RedisService redisService;
private final ItemViewRedisService itemViewRedisService;

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(HttpStatus.CREATED)
Expand All @@ -61,7 +60,7 @@ public ApiResponse<ItemResponses> findAll(@RequestParam String region,
@GetMapping("/{itemId}")
public ApiResponse<ItemDetailResponse> findDetailItem(@PathVariable Long itemId,
@AuthPrincipal Principal principal) {
redisService.addViewCount(itemId);
itemViewRedisService.addViewCount(itemId);
ItemDetailResponse response = itemService.findDetailItemBy(itemId, principal.getMemberId());
return ApiResponse.ok("상품 상세 조회에 성공하였습니다.", response);
}
Expand All @@ -72,9 +71,6 @@ public ApiResponse<Void> modifyItem(@PathVariable Long itemId,
@Valid @RequestPart("item") ItemModifyRequest request,
@RequestPart(value = "thumbnailImage", required = false) MultipartFile thumbnailImage,
@AuthPrincipal Principal principal) {
if (addImages == null) {
addImages = new ArrayList<>();
}
itemService.modifyItem(itemId, request, addImages, thumbnailImage, principal);
return ApiResponse.ok("상품 수정을 완료하였습니다.", null);
}
Expand Down
Loading

0 comments on commit 50e71fb

Please sign in to comment.