diff --git a/src/main/java/com/assu/server/domain/auth/controller/AuthController.java b/src/main/java/com/assu/server/domain/auth/controller/AuthController.java index d046fca6..35a2744b 100644 --- a/src/main/java/com/assu/server/domain/auth/controller/AuthController.java +++ b/src/main/java/com/assu/server/domain/auth/controller/AuthController.java @@ -37,28 +37,29 @@ public class AuthController { private final PhoneAuthService phoneAuthService; + private final EmailAuthService emailAuthService; private final SignUpService signUpService; private final LoginService loginService; private final LogoutService logoutService; private final SSUAuthService ssuAuthService; private final WithdrawalService withdrawalService; - private final VerificationService verificationService; @Operation( - summary = "휴대폰 인증번호 발송 API", - description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed801bbcd9f61c3e5f5457?source=copy_link)\n" + + summary = "휴대폰 번호 중복가입 확인 및 인증번호 발송 API", + description = "# [v1.1 (2025-09-25)](https://clumsy-seeder-416.notion.site/2241197c19ed801bbcd9f61c3e5f5457?source=copy_link)\n" + "- 입력한 휴대폰 번호로 1회용 인증번호(OTP)를 발송합니다.\n" + + "- 중복된 전화번호가 있으면 에러를 반환합니다.\n" + "- 유효시간/재요청 제한 정책은 서버 설정에 따릅니다.\n" + "\n**Request Body:**\n" + " - `phoneNumber` (String, required): 인증번호를 받을 휴대폰 번호\n" + "\n**Response:**\n" + " - 성공 시 200(OK)과 성공 메시지 반환" ) - @PostMapping("/phone-verification/send") - public BaseResponse sendAuthNumber( + @PostMapping("/phone-verification/check-and-send") + public BaseResponse checkPhoneAvailabilityAndSendAuthNumber( @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthSendRequest request ) { - phoneAuthService.sendAuthNumber(request.getPhoneNumber()); + phoneAuthService.checkAndSendAuthNumber(request.getPhoneNumber()); return BaseResponse.onSuccess(SuccessStatus.SEND_AUTH_NUMBER_SUCCESS, null); } @@ -84,23 +85,7 @@ public BaseResponse checkAuthNumber( return BaseResponse.onSuccess(SuccessStatus.VERIFY_AUTH_NUMBER_SUCCESS, null); } - @Operation(summary = "전화번호 중복 체크 API", - description = "# [v1.0 (2025-09-18)](https://clumsy-seeder-416.notion.site/2551197c19ed808a9757f7f0fc4cf09b?source=copy_link)\n" + - "- 입력한 전화번호가 이미 가입된 사용자가 있는지 확인합니다.\n" + - "- 중복된 전화번호가 있으면 에러를 반환합니다.\n" + - "\n**Request Body:**\n" + - " - `phoneNumber` (String, required): 확인할 전화번호 (010XXXXXXXX 형식)\n" + - "\n**Response:**\n" + - " - 성공 시 200(OK)과 사용 가능 메시지 반환\n" + - " - 중복 시 404(NOT_FOUND)와 에러 메시지 반환") - @PostMapping("/phone-verification/check") - public BaseResponse checkPhoneNumberAvailability( - @RequestBody @Valid VerificationRequestDTO.PhoneVerificationCheckRequest request) { - verificationService.checkPhoneNumberAvailability(request); - return BaseResponse.onSuccess(SuccessStatus._OK, null); - } - - @Operation(summary = "이메일 중복 체크 API", + @Operation(summary = "이메일 형식 및 중복가입 확인 API", description = "# [v1.0 (2025-09-18)](https://clumsy-seeder-416.notion.site/2551197c19ed802d8f6dd373dd045f3a?source=copy_link)\n" + "- 입력한 이메일이 이미 가입된 사용자가 있는지 확인합니다.\n" + "- 중복된 이메일이 있으면 에러를 반환합니다.\n" + @@ -112,7 +97,7 @@ public BaseResponse checkPhoneNumberAvailability( @PostMapping("/email-verification/check") public BaseResponse checkEmailAvailability( @RequestBody @Valid VerificationRequestDTO.EmailVerificationCheckRequest request) { - verificationService.checkEmailAvailability(request); + emailAuthService.checkEmailAvailability(request); return BaseResponse.onSuccess(SuccessStatus._OK, null); } diff --git a/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java b/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java new file mode 100644 index 00000000..c3f9bb0e --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java @@ -0,0 +1,8 @@ +package com.assu.server.domain.auth.service; + +import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; + +public interface EmailAuthService { + + void checkEmailAvailability(VerificationRequestDTO.EmailVerificationCheckRequest request); +} diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java similarity index 52% rename from src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java rename to src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java index 85cc43ef..c852420b 100644 --- a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java @@ -3,32 +3,18 @@ import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.domain.auth.repository.CommonAuthRepository; -import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class VerificationServiceImpl implements VerificationService { +public class EmailAuthServiceImpl implements EmailAuthService { - private final MemberRepository memberRepository; private final CommonAuthRepository commonAuthRepository; @Override - public void checkPhoneNumberAvailability( - VerificationRequestDTO.PhoneVerificationCheckRequest request) { - - boolean exists = memberRepository.existsByPhoneNum(request.getPhoneNumber()); - - if (exists) { - throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); - } - } - - @Override - public void checkEmailAvailability( - VerificationRequestDTO.EmailVerificationCheckRequest request) { + public void checkEmailAvailability(VerificationRequestDTO.EmailVerificationCheckRequest request) { boolean exists = commonAuthRepository.existsByEmail(request.getEmail()); diff --git a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java index 3b807005..1b345c37 100644 --- a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java +++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java @@ -1,6 +1,6 @@ package com.assu.server.domain.auth.service; public interface PhoneAuthService { - void sendAuthNumber(String phoneNumber); + void checkAndSendAuthNumber(String phoneNumber); void verifyAuthNumber(String phoneNumber, String authNumber); } diff --git a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java index 6db58c44..239650c5 100644 --- a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java @@ -1,5 +1,6 @@ package com.assu.server.domain.auth.service; +import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.util.RandomNumberUtil; import com.assu.server.domain.auth.exception.CustomAuthException; @@ -18,11 +19,18 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { private final StringRedisTemplate redisTemplate; private final AligoSmsClient aligoSmsClient; + private final MemberRepository memberRepository; private static final Duration AUTH_CODE_TTL = Duration.ofMinutes(5); // 인증번호 5분 유효 @Override - public void sendAuthNumber(String phoneNumber) { + public void checkAndSendAuthNumber(String phoneNumber) { + boolean exists = memberRepository.existsByPhoneNum(phoneNumber); + + if (exists) { + throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); + } + String authNumber = RandomNumberUtil.generateSixDigit(); redisTemplate.opsForValue().set(phoneNumber, authNumber, AUTH_CODE_TTL); diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationService.java b/src/main/java/com/assu/server/domain/auth/service/VerificationService.java deleted file mode 100644 index aaf28237..00000000 --- a/src/main/java/com/assu/server/domain/auth/service/VerificationService.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.assu.server.domain.auth.service; - -import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; - -public interface VerificationService { - void checkPhoneNumberAvailability( - VerificationRequestDTO.PhoneVerificationCheckRequest request); - - void checkEmailAvailability( - VerificationRequestDTO.EmailVerificationCheckRequest request); -} diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index d7893a9b..4f780063 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -2,8 +2,11 @@ import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatResponseDTO; +import com.assu.server.domain.chat.dto.ChatRoomUpdateDTO; +import com.assu.server.domain.chat.repository.MessageRepository; import com.assu.server.domain.chat.service.ChatService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import com.assu.server.global.util.PresenceTracker; import com.assu.server.global.util.PrincipalDetails; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -24,6 +27,8 @@ public class ChatController { private final ChatService chatService; private final SimpMessagingTemplate simpMessagingTemplate; + private final PresenceTracker presenceTracker; + private final MessageRepository messageRepository; @Operation( summary = "채팅방을 생성하는 API", @@ -61,10 +66,33 @@ public BaseResponse> ) @MessageMapping("/send") public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { - log.info("[WS] handleMessage IN: {}", request); // ★ 호출 여부 확인 - ChatResponseDTO.SendMessageResponseDTO response = chatService.handleMessage(request); - log.info("[WS] handleMessage SAVED id={}", response.messageId()); // 저장 확인용 - simpMessagingTemplate.convertAndSend("/sub/chat/" + request.roomId(), response); + // 먼저 접속 여부 확인 후 unreadCount 계산 + boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId()); + int unreadForSender = receiverInRoom ? 0 : 1; + request.setUnreadCountForSender(unreadForSender); + + ChatResponseDTO.SendMessageResponseDTO saved = chatService.handleMessage(request); + simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), saved); + + if (!receiverInRoom) { + Long totalUnreadCount = messageRepository.countUnreadMessagesByRoomAndReceiver( + request.getRoomId(), + request.getReceiverId() + ); + + ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.builder() + .roomId(request.getRoomId()) + .lastMessage(saved.message()) + .lastMessageTime(saved.sentAt()) + .unreadCount(totalUnreadCount) + .build(); + + simpMessagingTemplate.convertAndSendToUser( + request.getReceiverId().toString(), + "/queue/updates", + updateDTO + ); + } } @Operation( diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index 6082b4c3..b0b7877d 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -57,7 +57,8 @@ public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO reque .chattingRoom(room) .sender(sender) .receiver(receiver) - .message(request.message()) + .message(request.getMessage()) + .unreadCount(request.getUnreadCountForSender()) .build(); } @@ -70,6 +71,7 @@ public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message me .message(message.getMessage()) .sentAt(message.getCreatedAt()) .messageType(message.getType()) + .unreadCountForSender(message.getUnreadCount()) .build(); } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java index ad20a6f1..1e0f3ac9 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java @@ -22,6 +22,9 @@ public class ChatMessageDTO { private String message; private LocalDateTime sendTime; + @JsonProperty("unreadCountForSender") + private Integer unreadCount; + @JsonProperty("isRead") private boolean isRead; diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java index 90798fcd..575c46b9 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java @@ -1,6 +1,7 @@ package com.assu.server.domain.chat.dto; import lombok.Getter; +import lombok.Setter; public class ChatRequestDTO { @Getter @@ -9,10 +10,13 @@ public static class CreateChatRoomRequestDTO { private Long partnerId; } - public record ChatMessageRequestDTO( - Long roomId, - Long senderId, - Long receiverId, - String message - ) {} + @Getter + @Setter + public static class ChatMessageRequestDTO { + private Long roomId; + private Long senderId; + private Long receiverId; + private String message; + private int unreadCountForSender; + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index 29bbaa16..37df4725 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -33,8 +33,15 @@ public record SendMessageResponseDTO( String message, MessageType messageType, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") - LocalDateTime sentAt - ) {} + LocalDateTime sentAt, + Integer unreadCountForSender + ) { + public SendMessageResponseDTO withUnreadCountForSender(Integer count) { + return new SendMessageResponseDTO( + messageId, roomId, senderId, receiverId, message, messageType, sentAt, count + ); + } + } // 메시지 읽음 처리 public record ReadMessageResponseDTO( diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java new file mode 100644 index 00000000..b83ee0c0 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java @@ -0,0 +1,15 @@ +package com.assu.server.domain.chat.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class ChatRoomUpdateDTO { + private Long roomId; + private String lastMessage; + private LocalDateTime lastMessageTime; + private Long unreadCount; // 해당 채팅방의 총 안읽은 메시지 수 +} diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java index e668060a..ba06e811 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/Message.java +++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java @@ -36,6 +36,8 @@ public class Message extends BaseEntity { private String message; + private Integer unreadCount; + // private LocalDateTime sendTime; // private LocalDateTime readTime; @@ -47,5 +49,6 @@ public class Message extends BaseEntity { public void markAsRead() { this.isRead = true; + this.unreadCount = 0; } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java index 8d5a316c..9fe4581c 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java @@ -7,7 +7,6 @@ import org.springframework.data.repository.query.Param; import java.util.List; -import java.util.Optional; public interface MessageRepository extends JpaRepository { @Query(""" @@ -18,6 +17,15 @@ public interface MessageRepository extends JpaRepository { """) List findUnreadMessagesByRoomAndReceiver(Long roomId, Long receiverId); + @Query(""" + SELECT COUNT(m) + FROM Message m + WHERE m.chattingRoom.id = :roomId + AND m.receiver.id = :receiverId + AND m.isRead = false + """) + Long countUnreadMessagesByRoomAndReceiver(@Param("roomId") Long roomId, @Param("receiverId") Long receiverId); + @Query(""" SELECT new com.assu.server.domain.chat.dto.ChatMessageDTO ( @@ -25,6 +33,7 @@ public interface MessageRepository extends JpaRepository { m.id, m.message, m.createdAt, + m.unreadCount, m.isRead, CASE WHEN m.sender.id = :memberId THEN true ELSE false diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index df09fde5..62feb6d2 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -83,22 +83,18 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C @Transactional public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { // 유효성 검사 - ChattingRoom room = chatRepository.findById(request.roomId()) + ChattingRoom room = chatRepository.findById(request.getRoomId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); - Member sender = memberRepository.findById(request.senderId()) + Member sender = memberRepository.findById(request.getSenderId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); - Member receiver = memberRepository.findById(request.receiverId()) + Member receiver = memberRepository.findById(request.getReceiverId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); Message message = ChatConverter.toMessageEntity(request, room, sender, receiver); -// messageRepository.save(message); - log.info("saved message start"); Message saved = messageRepository.saveAndFlush(message); - log.info("saved message middle"); log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); - log.info("saved message end"); boolean exists = messageRepository.existsById(saved.getId()); log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제 return ChatConverter.toSendMessageDTO(saved); diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java index d3125434..32556850 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -37,16 +37,8 @@ Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc( Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(Long adminId, Long partnerId, List statuses); // Admin 기준 (SUSPEND) - @Query(""" -select p -from Paper p -left join fetch p.partner pt -left join fetch p.store s -where p.isActivated = :status - and p.admin.id = :adminId -order by p.createdAt desc -""") - List findAllSuspendedByAdminWithPartner( + @Query(" select p from Paper p left join fetch p.partner pt left join fetch p.store s where p.isActivated = :status and p.admin.id = :adminId and p.partner is null order by p.createdAt desc") + List findAllSuspendedByAdminWithNoPartner( @Param("status") ActivationStatus status, @Param("adminId") Long adminId ); @@ -55,4 +47,6 @@ List findAllSuspendedByAdminWithPartner( List findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Sort sort); Page findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Pageable pageable); Optional findTopPaperByStoreId(Long storeId); + long countByStore_Id(Long storeId); + } diff --git a/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java index b189af00..e791fe54 100644 --- a/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java +++ b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java @@ -253,7 +253,7 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long pa @Transactional public List getSuspendedPapers(Long adminId) { List suspendedPapers = - paperRepository.findAllSuspendedByAdminWithPartner(ActivationStatus.SUSPEND, adminId); + paperRepository.findAllSuspendedByAdminWithNoPartner(ActivationStatus.SUSPEND, adminId); return suspendedPapers.stream() .map(paper -> PartnershipResponseDTO.SuspendedPaperDTO.builder() @@ -405,7 +405,7 @@ public void deletePartnership(Long paperId) { Paper paper = paperRepository.findById(paperId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); - // 1. paperContent + goods 삭제 + // 0. paperContent + goods 삭제 List contentsToDelete = paperContentRepository.findByPaperId(paperId); if (contentsToDelete != null && !contentsToDelete.isEmpty()) { List contentIds = contentsToDelete.stream() @@ -416,14 +416,25 @@ public void deletePartnership(Long paperId) { paperContentRepository.deleteAll(contentsToDelete); } + // 1. store 참조를 미리 잡아두기 (paper 삭제 후 사용) + Store store = paper.getStore(); + boolean isTempStore = (store != null && paper.getPartner() == null); + // 2. paper 삭제 paperRepository.delete(paper); - // 3. 임시 store 삭제 (partner가 null인 경우만) - Store store = paper.getStore(); - if (store != null && paper.getPartner() == null) { - storeRepository.delete(store); + // 3) 임시 store 삭제 (재사용 중이면 보존) + if (isTempStore) { + Long storeId = store.getId(); + + // 남은 paper 참조 수 + long remainingPaperRefs = paperRepository.countByStore_Id(storeId); + + if (remainingPaperRefs == 0) { + storeRepository.delete(store); + } } + } @Override diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index 7256cfa7..8865e50c 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -52,6 +52,7 @@ public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.Wri ); pu.setIsReviewed(true); partnershipUsageRepository.save(pu); + recalcAndUpdateStoreRate(review.getStore().getId()); return ReviewConverter.writeReviewResultDTO(review); } @@ -147,11 +148,16 @@ public Page checkPartnerReview(Long me @Override @Transactional public ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(Long reviewId) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new DatabaseException(ErrorStatus._BAD_REQUEST)); + + Long storeId = review.getStore().getId(); + recalcAndUpdateStoreRate(storeId); + reviewRepository.deleteById(reviewId); return ReviewResponseDTO.DeleteReviewResponseDTO.builder() .reviewId(reviewId) .build(); - } private void updateReviewImageUrls(Review review) { @@ -219,4 +225,21 @@ public ReviewResponseDTO.StandardScoreResponseDTO myStoreAverage(Long memberId) .build(); } + + private void recalcAndUpdateStoreRate(Long storeId) { + // 이 시점에 영속성 컨텍스트의 변경분을 DB로 내보내 평균에 반영 + reviewRepository.flush(); + + Float avg = reviewRepository.standardScoreWithStatus( + storeId, ReportedStatus.NORMAL, ReportedStatus.NORMAL + ); + if (avg == null) avg = 0f; + + int rounded = (int) (Math.round(avg * 10f) / 10f); + + storeRepository.findById(storeId).ifPresent(s -> { + s.setRate(rounded); + storeRepository.save(s); + }); + } } diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java index 7c460ba2..f348d99f 100644 --- a/src/main/java/com/assu/server/domain/store/entity/Store.java +++ b/src/main/java/com/assu/server/domain/store/entity/Store.java @@ -24,6 +24,7 @@ public class Store extends BaseEntity { @JoinColumn(name = "partner_id") private Partner partner; + @Setter private Integer rate; @Setter diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java index cfb67c4d..2bf0bc05 100644 --- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java @@ -122,4 +122,5 @@ AND ST_Contains(ST_GeomFromText(:wkt, 4326), s.point) Optional findByPartnerId(Long partnerId); + } diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index f21a23ae..7ca4c047 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -53,9 +53,9 @@ public enum ErrorStatus implements BaseErrorCode { NO_PAPER_FOR_STORE(HttpStatus.NOT_FOUND, "ADMIN_4005", "존재하지 않는 paper ID입니다."), NO_AVAILABLE_PARTNER(HttpStatus.NOT_FOUND, "MEMBER_4009", "제휴업체를 찾을 수 없습니다."), NO_SUCH_STORE_WITH_THAT_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4006","해당 store ID에 해당하는 partner ID가 존재하지 않습니다."), - EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 전화번호입니다."), - EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4008","이미 존재하는 이메일입니다."), - EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4009","이미 존재하는 학번입니다."), + EXISTED_PHONE(HttpStatus.CONFLICT,"MEMBER_4007","이미 존재하는 전화번호입니다."), + EXISTED_EMAIL(HttpStatus.CONFLICT,"MEMBER_4008","이미 존재하는 이메일입니다."), + EXISTED_STUDENT(HttpStatus.CONFLICT,"MEMBER_4009","이미 존재하는 학번입니다."), MEMBER_ALREADY_WITHDRAWN(HttpStatus.BAD_REQUEST, "MEMBER_4010", "이미 탈퇴된 회원입니다."), diff --git a/src/main/java/com/assu/server/global/util/PresenceTracker.java b/src/main/java/com/assu/server/global/util/PresenceTracker.java new file mode 100644 index 00000000..e6f139b7 --- /dev/null +++ b/src/main/java/com/assu/server/global/util/PresenceTracker.java @@ -0,0 +1,90 @@ +package com.assu.server.global.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.SessionDisconnectEvent; +import org.springframework.web.socket.messaging.SessionSubscribeEvent; +import org.springframework.web.socket.messaging.SessionUnsubscribeEvent; + +import java.security.Principal; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Component +@Slf4j +public class PresenceTracker { + private final Map> roomSubscribers = new ConcurrentHashMap<>(); + private final Map sessionToMember = new ConcurrentHashMap<>(); + private final Map> sessionToRooms = new ConcurrentHashMap<>(); + + private Long parseRoomId(String dest) { // "/sub/chat/26" -> 26 + if (dest == null) return null; + String[] p = dest.split("/"); + if (p.length >= 4 && "chat".equals(p[2])) return Long.valueOf(p[3]); + return null; + } + + private Long memberIdFrom(Principal user) { + if (user == null) return null; + // StompAuthChannelInterceptor 에서 Principal.name을 memberId로 넣어두었다고 가정 + return Long.valueOf(user.getName()); + } + + @EventListener + public void onSubscribe(SessionSubscribeEvent e) { + var acc = StompHeaderAccessor.wrap(e.getMessage()); + Long roomId = parseRoomId(acc.getDestination()); + Long memberId = memberIdFrom(e.getUser()); + if (roomId == null || memberId == null) return; + + String sessionId = acc.getSessionId(); + sessionToMember.put(sessionId, memberId); + sessionToRooms.computeIfAbsent(sessionId, k -> ConcurrentHashMap.newKeySet()).add(roomId); + roomSubscribers.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(memberId); + + log.debug("SUB: member {} -> room {}", memberId, roomId); + } + + @EventListener + public void onUnsubscribe(SessionUnsubscribeEvent e) { + var acc = StompHeaderAccessor.wrap(e.getMessage()); + String sessionId = acc.getSessionId(); + var rooms = sessionToRooms.getOrDefault(sessionId, Set.of()); + Long memberId = sessionToMember.get(sessionId); + if (memberId != null) { + for (Long roomId : rooms) { + var set = roomSubscribers.get(roomId); + if (set != null) { + set.remove(memberId); + if (set.isEmpty()) roomSubscribers.remove(roomId); + } + } + } + sessionToRooms.remove(sessionId); + log.debug("UNSUB: session {}", sessionId); + } + + @EventListener + public void onDisconnect(SessionDisconnectEvent e) { + String sessionId = e.getSessionId(); + Long memberId = sessionToMember.remove(sessionId); + var rooms = sessionToRooms.remove(sessionId); + if (memberId != null && rooms != null) { + for (Long roomId : rooms) { + var set = roomSubscribers.get(roomId); + if (set != null) { + set.remove(memberId); + if (set.isEmpty()) roomSubscribers.remove(roomId); + } + } + } + log.debug("DISCONNECT: session {}", sessionId); + } + + public boolean isInRoom(Long memberId, Long roomId) { + return roomSubscribers.getOrDefault(roomId, Set.of()).contains(memberId); + } +}