Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void> sendAuthNumber(
@PostMapping("/phone-verification/check-and-send")
public BaseResponse<Void> checkPhoneAvailabilityAndSendAuthNumber(
@RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthSendRequest request
) {
phoneAuthService.sendAuthNumber(request.getPhoneNumber());
phoneAuthService.checkAndSendAuthNumber(request.getPhoneNumber());
return BaseResponse.onSuccess(SuccessStatus.SEND_AUTH_NUMBER_SUCCESS, null);
}

Expand All @@ -84,23 +85,7 @@ public BaseResponse<Void> 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<Void> 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" +
Expand All @@ -112,7 +97,7 @@ public BaseResponse<Void> checkPhoneNumberAvailability(
@PostMapping("/email-verification/check")
public BaseResponse<Void> checkEmailAvailability(
@RequestBody @Valid VerificationRequestDTO.EmailVerificationCheckRequest request) {
verificationService.checkEmailAvailability(request);
emailAuthService.checkEmailAvailability(request);
return BaseResponse.onSuccess(SuccessStatus._OK, null);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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",
Expand Down Expand Up @@ -61,10 +66,33 @@ public BaseResponse<List<com.assu.server.domain.chat.dto.ChatRoomListResultDTO>>
)
@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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -70,6 +71,7 @@ public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message me
.message(message.getMessage())
.sentAt(message.getCreatedAt())
.messageType(message.getType())
.unreadCountForSender(message.getUnreadCount())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public class ChatMessageDTO {
private String message;
private LocalDateTime sendTime;

@JsonProperty("unreadCountForSender")
private Integer unreadCount;

@JsonProperty("isRead")
private boolean isRead;

Expand Down
16 changes: 10 additions & 6 deletions src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.assu.server.domain.chat.dto;

import lombok.Getter;
import lombok.Setter;

public class ChatRequestDTO {
@Getter
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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; // 해당 채팅방의 총 안읽은 메시지 수
}
3 changes: 3 additions & 0 deletions src/main/java/com/assu/server/domain/chat/entity/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class Message extends BaseEntity {

private String message;

private Integer unreadCount;

// private LocalDateTime sendTime;
// private LocalDateTime readTime;

Expand All @@ -47,5 +49,6 @@ public class Message extends BaseEntity {

public void markAsRead() {
this.isRead = true;
this.unreadCount = 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface MessageRepository extends JpaRepository<Message, Long> {
@Query("""
Expand All @@ -18,13 +17,23 @@ public interface MessageRepository extends JpaRepository<Message, Long> {
""")
List<Message> 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 (
m.chattingRoom.id,
m.id,
m.message,
m.createdAt,
m.unreadCount,
m.isRead,
CASE WHEN m.sender.id = :memberId THEN true
ELSE false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading