diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java index 7d1ce5be..85d7fc27 100644 --- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java +++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java @@ -1,8 +1,10 @@ package com.assu.server.domain.certification.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @@ -18,17 +20,18 @@ public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소 - config.setApplicationDestinationPrefixes("/certification"); // 클라이언트가 인증 요청을 보내는 주소 + config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소 } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/ws") // 클라이언트 WebSocket 연결 주소 - .setAllowedOriginPatterns("*").withSockJS(); // CORS 허용 + registry.addEndpoint("/ws").setAllowedOriginPatterns("*"); // 클라이언트 WebSocket 연결 주소 + // .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용 } @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(stompAuthChannelInterceptor); } + } diff --git a/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java b/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java index d2eb1c06..b5da450e 100644 --- a/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java +++ b/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java @@ -7,7 +7,9 @@ import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; // SLF4j 로그 추가 +@Slf4j // SLF4j 어노테이션 추가 @Component @RequiredArgsConstructor public class StompAuthChannelInterceptor implements ChannelInterceptor { @@ -17,19 +19,32 @@ public class StompAuthChannelInterceptor implements ChannelInterceptor { @Override public Message preSend(Message message, MessageChannel channel) { StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + log.info("StompCommand: {}", accessor.getCommand()); // StompCommand 로그 추가 if (StompCommand.CONNECT.equals(accessor.getCommand())) { + log.info("CONNECT command received."); // 프론트에서 connect 시 Authorization 헤더 넣어야 함 String authHeader = accessor.getFirstNativeHeader("Authorization"); + log.info("Authorization Header: {}", authHeader); // Authorization 헤더 로그 추가 + if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = jwtUtil.getTokenFromHeader(authHeader); + log.info("Extracted Token: {}", token); // 추출된 토큰 로그 추가 // JwtUtil 이용해서 Authentication 복원 Authentication authentication = jwtUtil.getAuthentication(token); + log.info("Authentication restored: {}", authentication); // 복원된 인증 정보 로그 추가 // WebSocket 세션에 Authentication(UserPrincipal) 저장 accessor.setUser(authentication); + log.info("User principal set on accessor."); + } else { + log.warn("Authorization header is missing or not in Bearer format."); } + } else if (StompCommand.SEND.equals(accessor.getCommand())) { + // SEND 명령어에 대한 로그 추가 (메시지 전송 시) + Object payload = message.getPayload(); + log.info("SEND command received. Destination: {}, Payload: {}", accessor.getDestination(), payload); } return message; diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java index 3d26da64..d86c9280 100644 --- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java +++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java @@ -60,27 +60,27 @@ public ResponseEntity> certifyGroup( - CertificationRequestDTO.groupSessionRequest dto , PrincipalDetails pd - - ) { - certificationService.handleCertification(dto, pd.getMember()); - - return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_CERTIFICATION_SUCCESS, null)); - } + // @MessageMapping("/certify") + // @Operation(summary = "그룹 세션 인증 api", description = "그룹에 대한 세션 인증 요청을 보냅니다.") + // public ResponseEntity> certifyGroup( + // CertificationRequestDTO.groupSessionRequest dto , PrincipalDetails pd + // + // ) { + // certificationService.handleCertification(dto, pd.getMember()); + // + // return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_CERTIFICATION_SUCCESS, null)); + // } @PostMapping("/certification/personal") @Operation(summary = "개인 인증 api", description = "사실 크게 필요없는데, 제휴 내역 통계를 위해 데이터를 post하는 api 입니다. " + "가게 별 제휴를 조회하고 people값이 null 인 제휴를 선택한 경우 그룹 인증 대신 요청하는 api 입니다.") - public ResponseEntity> personalCertification( + public ResponseEntity> personalCertification( @AuthenticationPrincipal PrincipalDetails pd, @RequestBody CertificationRequestDTO.personalRequest dto ) { certificationService.certificatePersonal(dto, pd.getMember()); - return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus.PERSONAL_CERTIFICATION_SUCCESS)); + return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.PERSONAL_CERTIFICATION_SUCCESS, "null")); } } diff --git a/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java new file mode 100644 index 00000000..ee919c59 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java @@ -0,0 +1,48 @@ +package com.assu.server.domain.certification.controller; + +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; + +import com.assu.server.domain.certification.dto.GroupSessionRequest; +import com.assu.server.domain.certification.service.CertificationService; +import com.assu.server.domain.member.entity.Member; +import com.assu.server.global.util.PrincipalDetails; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller // STOMP 메시지 처리를 위한 컨트롤러 +@RequiredArgsConstructor +public class GroupCertificationController { + + private final CertificationService certificationService; + + @MessageMapping("/certify") + public void certifyGroup(@Payload GroupSessionRequest dto, SimpMessageHeaderAccessor headerAccessor) { + try { + log.info("### SUCCESS ### 인증 요청 메시지 수신 - adminId: {}, sessionId: {}", dto.getAdminId(), dto.getSessionId()); + + // Authentication에서 Member 정보 추출 + Authentication auth = (Authentication) headerAccessor.getUser(); + if (auth != null && auth.getPrincipal() instanceof PrincipalDetails) { + PrincipalDetails principalDetails = (PrincipalDetails) auth.getPrincipal(); + // 실제 비즈니스 로직 호출 + certificationService.handleCertification(dto, principalDetails.getMember()); + log.info("### SUCCESS ### 그룹 인증 처리 완료"); + } + } catch (Exception e) { + log.error("### ERROR ### 인증 처리 실패", e); + } + } + + // @MessageMapping("/certify") + // public void certifyGroup(SimpMessageHeaderAccessor headerAccessor) { + // log.info("### DEBUG ### 메서드 진입!"); + // log.info("### DEBUG ### User: {}", headerAccessor.getUser()); + // log.info("### DEBUG ### SessionId: {}", headerAccessor.getSessionId()); + // } +} diff --git a/src/main/java/com/assu/server/domain/certification/controller/WebSocketTestController.java b/src/main/java/com/assu/server/domain/certification/controller/WebSocketTestController.java new file mode 100644 index 00000000..7460e5cc --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/controller/WebSocketTestController.java @@ -0,0 +1,16 @@ +package com.assu.server.domain.certification.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Controller; + +@Slf4j +@Controller +public class WebSocketTestController { + + @MessageMapping("/test") + public void test(@Payload String payload) { + log.info("### 테스트용 메시지 수신 성공! 페이로드: {}", payload); + } +} diff --git a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java index 0e4c812e..e4422cbc 100644 --- a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java +++ b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java @@ -4,6 +4,7 @@ import com.assu.server.domain.certification.dto.CertificationRequestDTO; import com.assu.server.domain.certification.dto.CertificationResponseDTO; import com.assu.server.domain.certification.entity.AssociateCertification; +import com.assu.server.domain.certification.entity.enums.SessionStatus; import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.store.entity.Store; @@ -12,6 +13,7 @@ public static AssociateCertification toAssociateCertification(CertificationReque return AssociateCertification.builder() .store(store) .partner(store.getPartner()) + .status(SessionStatus.OPENED) .isCertified(false) .peopleNumber(dto.getPeople()) .tableNumber(dto.getTableNumber()) diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java new file mode 100644 index 00000000..4221fe99 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java @@ -0,0 +1,49 @@ +package com.assu.server.domain.certification.dto; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +// public class CurrentProgress { +// private int count; +// +// +// @Getter +// public static class CertificationNumber{ +// public CertificationNumber(int count){ +// this.count= count; +// } +// +// int count; +// } +// +// @Getter +// public static class CompletedNotification{ +// public CompletedNotification(String message, List userIds){ +// +// this.message= message; +// this.userIds= userIds; +// } +// String message; +// List userIds; +// } + +// } +@Getter +@AllArgsConstructor +public class CertificationProgressResponseDTO { + private String type; + private Integer count; + private String message; + private List userIds; + + // 생성자들 + public static CertificationProgressResponseDTO progress(int count) { + return new CertificationProgressResponseDTO("progress", count, null, null); + } + + public static CertificationProgressResponseDTO completed(String message, List userIds) { + return new CertificationProgressResponseDTO("completed", userIds.size(), message, userIds); + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java index 971f337a..0ba578c4 100644 --- a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java +++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java @@ -1,7 +1,9 @@ package com.assu.server.domain.certification.dto; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; - +import lombok.NoArgsConstructor; public class CertificationRequestDTO { @@ -20,9 +22,11 @@ public static class personalRequest{ Integer tableNumber; } - @Getter - public static class groupSessionRequest{ - Long adminId; - Long sessionId; - } + // @Getter + // @NoArgsConstructor(access = AccessLevel.PROTECTED) + // @AllArgsConstructor + // public static class groupSessionRequest { + // private Long adminId; + // private Long sessionId; + // } } diff --git a/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java b/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java deleted file mode 100644 index 919e946e..00000000 --- a/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.assu.server.domain.certification.dto; - -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@AllArgsConstructor -public class CurrentProgress { - private int count; - - - @Getter - public static class CertificationNumber{ - public CertificationNumber(int count){ - - this.count= count; - } - int count; - } - - @Getter - public static class CompletedNotification{ - public CompletedNotification(String message, List userIds){ - - this.message= message; - this.userIds= userIds; - } - String message; - List userIds; - } - -} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/certification/dto/GroupSessionRequest.java b/src/main/java/com/assu/server/domain/certification/dto/GroupSessionRequest.java new file mode 100644 index 00000000..4099e376 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/dto/GroupSessionRequest.java @@ -0,0 +1,21 @@ +package com.assu.server.domain.certification.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@NoArgsConstructor +@Setter +public class GroupSessionRequest { + Long adminId; + Long sessionId; + + @Override + public String toString() { + return "GroupSessionRequest{" + + "adminId=" + adminId + + ", sessionId=" + sessionId + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/certification/handler/CertificationWebsocketHandler.java b/src/main/java/com/assu/server/domain/certification/handler/CertificationWebsocketHandler.java new file mode 100644 index 00000000..7009e674 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/handler/CertificationWebsocketHandler.java @@ -0,0 +1,41 @@ +// package com.assu.server.domain.certification.handler; +// +// import org.springframework.stereotype.Component; +// import org.springframework.web.socket.CloseStatus; +// import org.springframework.web.socket.TextMessage; +// import org.springframework.web.socket.WebSocketSession; +// import org.springframework.web.socket.handler.TextWebSocketHandler; +// +// @Component +// public class CertificationWebsocketHandler extends TextWebSocketHandler { +// +// @Override +// public void afterConnectionEstablished(WebSocketSession session) throws Exception { +// // 클라이언트 연결이 성공적으로 수립되었을 때 호출됩니다. +// System.out.println("Client connected: " + session.getId()); +// } +// +// @Override +// protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { +// // 클라이언트로부터 텍스트 메시지를 받았을 때 호출됩니다. +// String payload = message.getPayload(); +// System.out.println("Message received from " + session.getId() + ": " + payload); +// +// // 받은 메시지를 다시 클라이언트에게 보내거나 다른 로직을 처리합니다. +// session.sendMessage(new TextMessage("Echo: " + payload)); +// } +// +// @Override +// public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { +// // 클라이언트 연결이 종료되었을 때 호출됩니다. +// System.out.println("Client disconnected: " + session.getId() + " with status " + status.getCode()); +// } +// +// @Override +// public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { +// // 전송 오류가 발생했을 때 호출됩니다. +// System.err.println("Transport error for session " + session.getId() + ": " + exception.getMessage()); +// // 필요한 경우 연결을 종료하거나 오류를 처리합니다. +// session.close(CloseStatus.SERVER_ERROR); +// } +// } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/certification/service/CertificationService.java b/src/main/java/com/assu/server/domain/certification/service/CertificationService.java index 4e68dfed..cde9e47c 100644 --- a/src/main/java/com/assu/server/domain/certification/service/CertificationService.java +++ b/src/main/java/com/assu/server/domain/certification/service/CertificationService.java @@ -2,13 +2,14 @@ import com.assu.server.domain.certification.dto.CertificationRequestDTO; import com.assu.server.domain.certification.dto.CertificationResponseDTO; +import com.assu.server.domain.certification.dto.GroupSessionRequest; import com.assu.server.domain.member.entity.Member; public interface CertificationService { CertificationResponseDTO.getSessionIdResponse getSessionId(CertificationRequestDTO.groupRequest dto, Member member); - void handleCertification(CertificationRequestDTO.groupSessionRequest dto, Member member); + void handleCertification(GroupSessionRequest dto, Member member); void certificatePersonal(CertificationRequestDTO.personalRequest dto, Member member); } diff --git a/src/main/java/com/assu/server/domain/certification/service/CertificationServiceImpl.java b/src/main/java/com/assu/server/domain/certification/service/CertificationServiceImpl.java index 347299be..01946fc6 100644 --- a/src/main/java/com/assu/server/domain/certification/service/CertificationServiceImpl.java +++ b/src/main/java/com/assu/server/domain/certification/service/CertificationServiceImpl.java @@ -1,7 +1,6 @@ package com.assu.server.domain.certification.service; import java.time.Duration; -import java.time.LocalDateTime; import java.util.List; @@ -13,18 +12,17 @@ import com.assu.server.domain.certification.SessionTimeoutManager; import com.assu.server.domain.certification.component.CertificationSessionManager; import com.assu.server.domain.certification.converter.CertificationConverter; +import com.assu.server.domain.certification.dto.CertificationProgressResponseDTO; import com.assu.server.domain.certification.dto.CertificationRequestDTO; import com.assu.server.domain.certification.dto.CertificationResponseDTO; -import com.assu.server.domain.certification.dto.CurrentProgress; +import com.assu.server.domain.certification.dto.GroupSessionRequest; import com.assu.server.domain.certification.entity.AssociateCertification; -import com.assu.server.domain.certification.entity.QRCertification; import com.assu.server.domain.certification.entity.enums.SessionStatus; import com.assu.server.domain.certification.repository.AssociateCertificationRepository; import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.domain.user.entity.Student; -import com.assu.server.domain.user.repository.StudentRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.GeneralException; import jakarta.transaction.Transactional; @@ -39,7 +37,6 @@ public class CertificationServiceImpl implements CertificationService { private final AdminRepository adminRepository; private final StoreRepository storeRepository; private final AssociateCertificationRepository associateCertificationRepository; - private final StudentRepository studentRepository; // 세션 메니저 private final CertificationSessionManager sessionManager; @@ -74,7 +71,7 @@ public CertificationResponseDTO.getSessionIdResponse getSessionId( sessionManager.openSession(sessionId); // 세션 생성 직후 만료 시간을 5분으로 설정 - timeoutManager.scheduleTimeout(sessionId, Duration.ofMinutes(5)); + timeoutManager.scheduleTimeout(sessionId, Duration.ofMinutes(100));// TODO: 나중에 5분으로 변경 // 세션 여는 대표자는 제일 먼저 인증 sessionManager.addUserToSession(sessionId, userId); @@ -84,7 +81,7 @@ public CertificationResponseDTO.getSessionIdResponse getSessionId( } @Override - public void handleCertification(CertificationRequestDTO.groupSessionRequest dto, Member member) { + public void handleCertification(GroupSessionRequest dto, Member member) { Long userId = member.getId(); // 제휴 대상인지 확인하기 @@ -111,25 +108,15 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto, throw new GeneralException(ErrorStatus.SESSION_NOT_OPENED); boolean isDoubledUser= sessionManager.hasUser(sessionId, userId); - if(isDoubledUser) + if(isDoubledUser) { + messagingTemplate.convertAndSend("/certification/progress/"+sessionId, + new CertificationProgressResponseDTO("progress", 0,"doubled member", null)); throw new GeneralException(ErrorStatus.DOUBLE_CERTIFIED_USER); + } sessionManager.addUserToSession(sessionId, userId); int currentCertifiedNumber = sessionManager.getCurrentUserCount(sessionId); - // messagingTemplate.convertAndSend("/certification/progress/"+sessionId, - // new CurrentProgress.CertificationNumber(currentCertifiedNumber)); - // - // if(currentCertifiedNumber >= session.getPeopleNumber()){ - // session.setIsCertified(true); - // session.setStatus(SessionStatus.COMPLETED); - // associateCertificationRepository.save(session); - // - // - // messagingTemplate.convertAndSend("/certification/progress/"+sessionId, - // new CurrentProgress.CompletedNotification("인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId)) - // ); - // } if(currentCertifiedNumber >= session.getPeopleNumber()){ session.setIsCertified(true); session.setStatus(SessionStatus.COMPLETED); @@ -137,14 +124,10 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto, // 완료 알림에 현재 인원수도 포함 messagingTemplate.convertAndSend("/certification/progress/" + sessionId, - new CurrentProgress.CompletedNotification( - "인증이 완료되었습니다.", - sessionManager.snapshotUserIds(sessionId) - ) - ); + new CertificationProgressResponseDTO("completed", currentCertifiedNumber, "인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId))); } else { messagingTemplate.convertAndSend("/certification/progress/" + sessionId, - new CurrentProgress.CertificationNumber(currentCertifiedNumber)); + new CertificationProgressResponseDTO("progress", currentCertifiedNumber, null, null)); } diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java index 0f9920b7..cdb02cb4 100644 --- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java +++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java @@ -11,8 +11,17 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws/chat") // 클라이언트 WebSocket 연결 지점 - .setAllowedOriginPatterns("http://localhost:63342") + .setAllowedOriginPatterns( + "http://localhost:63342", + "http://localhost:5173", // Vite 기본 + "http://localhost:3000", // CRA/Next 기본 + "http://127.0.0.1:*", + "http://192.168.*.*:*") // 같은 LAN의 실제 기기 테스트용 .withSockJS(); // fallback for old browsers + + // ✅ 모바일/안드로이드용 (네이티브 WebSocket) + registry.addEndpoint("/ws/chat-native") + .setAllowedOriginPatterns("*"); // wss 사용 시 TLS 세팅 } @Override 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 52b677d7..d7893a9b 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 @@ -4,16 +4,20 @@ import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.service.ChatService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import com.assu.server.global.util.PrincipalDetails; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import com.assu.server.global.apiPayload.BaseResponse; import org.springframework.messaging.simp.SimpMessagingTemplate; import java.util.List; +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/chat") @@ -22,64 +26,89 @@ public class ChatController { private final SimpMessagingTemplate simpMessagingTemplate; @Operation( - summary = "채팅방 목록 조회 API", - description = "Request Header에 User id를 입력해 주세요." + summary = "채팅방을 생성하는 API", + description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed80c38871ec77deced713) 채팅방을 생성합니다.\n"+ + "- storeId: Request Body, Long\n" + + "- partnerId: Request Body, Long\n" ) - @GetMapping("/rooms") - public BaseResponse> getChatRoomList() { - return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList()); + @PostMapping("/rooms") + public BaseResponse createChatRoom( + @AuthenticationPrincipal PrincipalDetails pd, + @RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request, memberId)); } @Operation( - summary = "채팅방 생성 API", - description = "상대방의 id를 request body에 입력해 주세요" + summary = "채팅방 목록을 조회하는 API", + description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/API-1d71197c19ed819f8f70fb437e9ce62b?p=2241197c19ed816993c3c5ae17d6f099&pm=s) 채팅방 목록을 조회합니다.\n" ) - @PostMapping("/create/rooms") - public BaseResponse createChatRoom(@RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) { - return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request)); + @GetMapping("/rooms") + public BaseResponse> getChatRoomList( + @AuthenticationPrincipal PrincipalDetails pd + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList(memberId)); } @Operation( summary = "채팅 API", - description = "roomId, senderId, message를 입력해 주세요" + description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2371197c19ed80968342e2bc8fe88cee&pm=s) 메시지를 전송합니다.\n"+ + "- roomId: Request Body, Long\n" + + "- senderId: Request Body, Long\n"+ + "- receiverId: Request Body, Long\n" + + "- message: Request Body, String\n" ) @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); } @Operation( summary = "메시지 읽음 처리 API", - description = "roomId를 입력해 주세요." + description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2241197c19ed81ffa771cb18ab157b54&pm=s) 메시지를 읽음처리합니다.\n"+ + "- roomId: Path Variable, Long\n" ) @PatchMapping("rooms/{roomId}/read") public BaseResponse readMessage( - @PathVariable Long roomId) { - ChatResponseDTO.ReadMessageResponseDTO response = chatService.readMessage(roomId); + @AuthenticationPrincipal PrincipalDetails pd, + @PathVariable Long roomId + ) { + Long memberId = pd.getMember().getId(); + ChatResponseDTO.ReadMessageResponseDTO response = chatService.readMessage(roomId, memberId); return BaseResponse.onSuccess(SuccessStatus._OK, response); } @Operation( summary = "채팅방 상세 조회 API", - description = "roomId를 입력해 주세요." + description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2241197c19ed81399395fd66f73730af&pm=s) 채팅방을 클릭했을 때 메시지를 조회합니다.\n"+ + "- roomId: Path Variable, Long\n" ) @GetMapping("rooms/{roomId}/messages") - public BaseResponse getChatHistory(@PathVariable Long roomId) { - ChatResponseDTO.ChatHistoryResponseDTO response = chatService.readHistory(roomId); + public BaseResponse getChatHistory( + @AuthenticationPrincipal PrincipalDetails pd, + @PathVariable Long roomId + ) { + Long memberId = pd.getMember().getId(); + ChatResponseDTO.ChatHistoryResponseDTO response = chatService.readHistory(roomId, memberId); return BaseResponse.onSuccess(SuccessStatus._OK, response); } @Operation( - summary = "채팅방 나가기 API" + - " (참여자가 2명이면 채팅방이 살아있지만, 이미 한 명이 나갔다면 채팅방이 삭제됩니다.)", - description = "roomId를 입력해 주세요." + summary = "채팅방을 나가는 API" + + "참여자가 2명이면 채팅방이 살아있지만, 이미 한 명이 나갔다면 채팅방이 삭제됩니다.", + description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2371197c19ed8079a6e1c2331cb4f534&pm=s) 채팅방을 나갑니다.\n"+ + "- roomId: Path Variable, Long\n" ) @DeleteMapping("rooms/{roomId}/leave") public BaseResponse leaveChattingRoom( + @AuthenticationPrincipal PrincipalDetails pd, @PathVariable Long roomId ) { - return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId)); + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId, memberId)); } } 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 a84621fa..70fe10c2 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 @@ -44,7 +44,12 @@ public static ChattingRoom toCreateChattingRoom(Admin admin, Partner partner) { } public static ChatResponseDTO.CreateChatRoomResponseDTO toCreateChatRoomIdDTO(ChattingRoom room) { - return new ChatResponseDTO.CreateChatRoomResponseDTO(room.getId()); + return ChatResponseDTO.CreateChatRoomResponseDTO.builder() + .roomId(room.getId()) + .adminViewName(room.getPartner().getName()) + .partnerViewName(room.getAdmin().getName()) + .build(); + } public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender, Member receiver) { @@ -60,8 +65,10 @@ public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message me return ChatResponseDTO.SendMessageResponseDTO.builder() .roomId(message.getChattingRoom().getId()) .senderId(message.getSender().getId()) + .receiverId(message.getReceiver().getId()) .message(message.getMessage()) .sentAt(message.getCreatedAt()) + .messageType(message.getType()) .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 628af1db..ad20a6f1 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 @@ -1,6 +1,7 @@ package com.assu.server.domain.chat.dto; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -21,6 +22,9 @@ public class ChatMessageDTO { private String message; private LocalDateTime sendTime; + @JsonProperty("isRead") private boolean isRead; + + @JsonProperty("isMyMessage") private boolean isMyMessage; } 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..2123afc2 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 @@ -5,7 +5,7 @@ public class ChatRequestDTO { @Getter public static class CreateChatRoomRequestDTO { - private Long adminId; + private Long storeId; private Long partnerId; } 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 8c4790aa..29bbaa16 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 @@ -1,5 +1,8 @@ package com.assu.server.domain.chat.dto; +import com.assu.server.domain.chat.entity.enums.MessageType; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.google.protobuf.Enum; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -16,21 +19,30 @@ public class ChatResponseDTO { @Builder public static class CreateChatRoomResponseDTO { private Long roomId; + private String adminViewName; + private String partnerViewName; } // 메시지 전송 @Builder public record SendMessageResponseDTO( + Long messageId, Long roomId, Long senderId, + Long receiverId, String message, + MessageType messageType, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime sentAt ) {} // 메시지 읽음 처리 public record ReadMessageResponseDTO( Long roomId, - int readCount + Long readerId, + List readMessagesId, + int readCount, + boolean isRead ) {} // 채팅방 들어갔을 때 조회 diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index bc44c561..e11b3c13 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -6,10 +6,10 @@ import java.util.List; public interface ChatService { - List getChatRoomList(); - ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request); + List getChatRoomList(Long memberId); + ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId); ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); - ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId); - ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId); - ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId); + ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId); + ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId); + ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId, Long memberId); } 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 8b4e1e3c..e4ef31b3 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 @@ -16,13 +16,21 @@ import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.partner.repository.PartnerRepository; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; + +import java.util.ArrayList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +@Slf4j @Service @RequiredArgsConstructor public class ChatServiceImpl implements ChatService { @@ -31,29 +39,33 @@ public class ChatServiceImpl implements ChatService { private final PartnerRepository partnerRepository; private final AdminRepository adminRepository; private final MessageRepository messageRepository; + private final StoreRepository storeRepository; @Override - public List getChatRoomList() { -// Long memberId = SecurityUtil.getCurrentUserId; - Long memberId = 1L; + public List getChatRoomList(Long memberId) { List chatRoomList = chatRepository.findChattingRoomsByMemberId(memberId); return ChatConverter.toChatRoomListResultDTO(chatRoomList); } @Override - public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request) { -// Long memberId = SecurityUtil.getCurrentUserId; -// Long opponentId = request.getOpponentId(); + public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId) { - Long adminId = request.getAdminId(); + Long storeId = request.getStoreId(); Long partnerId = request.getPartnerId(); - Admin admin = adminRepository.findById(adminId) + Admin admin = adminRepository.findById(memberId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); Partner partner = partnerRepository.findById(partnerId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); + + + if (!store.getPartner().getMember().getId().equals(partner.getMember().getId())) { + throw new DatabaseException(ErrorStatus.NO_SUCH_STORE_WITH_THAT_PARTNER); + } ChattingRoom room = ChatConverter.toCreateChattingRoom(admin, partner); @@ -66,13 +78,11 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C admin.getName() ); ChattingRoom savedRoom = chatRepository.save(room); - - - return ChatConverter.toCreateChatRoomIdDTO(savedRoom); } @Override + @Transactional public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { // 유효성 검사 ChattingRoom room = chatRepository.findById(request.roomId()) @@ -83,43 +93,48 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); Message message = ChatConverter.toMessageEntity(request, room, sender, receiver); - messageRepository.save(message); - - return ChatConverter.toSendMessageDTO(message); +// 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); } @Transactional @Override - public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) { -// Long memberId = SecurityUtil.getCurrentUserId(); - Long memberId = 2L; + public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId) { List unreadMessages = messageRepository.findUnreadMessagesByRoomAndReceiver(roomId, memberId); + List readMessagesIdList = new ArrayList<>(); + for(Message unreadMessage : unreadMessages) { + readMessagesIdList.add(unreadMessage.getId()); + } unreadMessages.forEach(Message::markAsRead); - return new ChatResponseDTO.ReadMessageResponseDTO(roomId, unreadMessages.size()); + + return new ChatResponseDTO.ReadMessageResponseDTO(roomId, memberId,readMessagesIdList, unreadMessages.size(), true); } @Override - public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId) { -// Long memberId = SecurityUtil.getCurrentUserId(); - Long memberId = 1L; + public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId) { ChattingRoom room = chatRepository.findById(roomId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); - List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(roomId, memberId); + List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(room.getId(), memberId); - return ChatConverter.toChatHistoryDTO(roomId, allMessages); + return ChatConverter.toChatHistoryDTO(room.getId(), allMessages); } @Override - public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId) { -// Long memberId = SecurityUtil.getCurrentUserId(); - - Long memberId = 2L; - + public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId, Long memberId) { // 멤버 조회 Member member = memberRepository.findById(memberId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); diff --git a/src/main/java/com/assu/server/domain/map/converter/MapConverter.java b/src/main/java/com/assu/server/domain/map/converter/MapConverter.java new file mode 100644 index 00000000..1d1a9af6 --- /dev/null +++ b/src/main/java/com/assu/server/domain/map/converter/MapConverter.java @@ -0,0 +1,74 @@ +package com.assu.server.domain.map.converter; + +import java.util.List; + +import com.assu.server.domain.partnership.entity.Goods; +import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.entity.enums.CriterionType; +import com.assu.server.domain.partnership.entity.enums.OptionType; + +public class MapConverter { + + + + + private static List extractGoods(PaperContent content) { + if (content.getOptionType() == OptionType.SERVICE ) { + return content.getGoods().stream() + .map(Goods::getBelonging) + .toList(); + } + return null; + } + + private static Integer extractPeople(PaperContent content) { + if (content.getCriterionType() == CriterionType.HEADCOUNT) { + return content.getPeople(); + } + return null; + } + + private static String buildPaperContentText(PaperContent content, List goodsList, Integer peopleValue) { + String result = ""; + + boolean isGoodsSingle = goodsList != null && goodsList.size() == 1; + boolean isGoodsMultiple = goodsList != null && goodsList.size() > 1; + + // 1. HEADCOUNT + SERVICE + 여러 개 goods + if (content.getCriterionType() == CriterionType.HEADCOUNT && + content.getOptionType() == OptionType.SERVICE && + isGoodsMultiple) { + result = peopleValue + "명 이상 식사 시 " + content.getCategory() + " 제공"; + } + // 2. HEADCOUNT + SERVICE + 단일 goods + else if (content.getCriterionType() == CriterionType.HEADCOUNT && + content.getOptionType() == OptionType.SERVICE && + isGoodsSingle) { + result = peopleValue + "명 이상 식사 시 " + goodsList.get(0) + " 제공"; + } + // 3. HEADCOUNT + DISCOUNT + else if (content.getCriterionType() == CriterionType.HEADCOUNT && + content.getOptionType() == OptionType.DISCOUNT) { + result = peopleValue + "명 이상 식사 시 " + content.getDiscount() + "% 할인"; + } + // 4. PRICE + SERVICE + 여러 개 goods + else if (content.getCriterionType() == CriterionType.PRICE && + content.getOptionType() == OptionType.SERVICE && + isGoodsMultiple) { + result = content.getCost() + "원 이상 주문 시 " + content.getCategory() + " 제공"; + } + // 5. PRICE + SERVICE + 단일 goods + else if (content.getCriterionType() == CriterionType.PRICE && + content.getOptionType() == OptionType.SERVICE && + isGoodsSingle) { + result = content.getCost() + "원 이상 주문 시 " + goodsList.get(0) + " 제공"; + } + // 6. PRICE + DISCOUNT + else if (content.getCriterionType() == CriterionType.PRICE && + content.getOptionType() == OptionType.DISCOUNT) { + result = content.getCost() + "원 이상 주문 시 " + content.getDiscount() + "% 할인"; + } + + return result; + } +} diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java index 90d78f2a..195dacd1 100644 --- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java +++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java @@ -48,6 +48,7 @@ public static class AdminMapResponseDTO { public static class StoreMapResponseDTO { private Long storeId; private Long adminId; + private String adminName; private String name; private String address; private Integer rate; diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java index 58b0af3b..ae1bc8ed 100644 --- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java +++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java @@ -7,8 +7,11 @@ import com.assu.server.domain.map.dto.MapResponseDTO; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.partner.repository.PartnerRepository; +import com.assu.server.domain.partnership.entity.Goods; import com.assu.server.domain.partnership.entity.Paper; import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.entity.enums.OptionType; +import com.assu.server.domain.partnership.repository.GoodsRepository; import com.assu.server.domain.partnership.repository.PaperContentRepository; import com.assu.server.domain.partnership.repository.PaperRepository; import com.assu.server.domain.store.entity.Store; @@ -16,6 +19,8 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.config.KakaoLocalClient; import com.assu.server.global.exception.DatabaseException; +import com.assu.server.global.exception.GeneralException; + import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Coordinate; @@ -33,10 +38,10 @@ public class MapServiceImpl implements MapService { private final AdminRepository adminRepository; private final PartnerRepository partnerRepository; private final StoreRepository storeRepository; - private final KakaoLocalClient kakaoLocalClient; private final PaperContentRepository paperContentRepository; private final PaperRepository paperRepository; private final GeometryFactory geometryFactory; + private final GoodsRepository goodsRepository; @Override public List getPartners(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) { @@ -100,15 +105,19 @@ public List getStores(MapRequestDTO.ViewOnMa boolean hasPartner = (s.getPartner() != null); PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId()) - .orElse(null); + .orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) + ); Long adminId = paperRepository.findTopPaperByStoreId(s.getId()) .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null) .orElse(null); + Admin admin = adminRepository.findById(adminId).orElse(null); return MapResponseDTO.StoreMapResponseDTO.builder() .storeId(s.getId()) .adminId(adminId) + .adminName(admin.getName()) .name(s.getName()) .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress()) .rate(s.getRate()) @@ -132,14 +141,38 @@ public List searchStores(String keyword) { return stores.stream().map(s -> { boolean hasPartner = s.getPartner() != null; PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId()) - .orElse(null); + .orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) + ); Long adminId = paperRepository.findTopPaperByStoreId(s.getId()) .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null) .orElse(null); + Admin admin = adminRepository.findById(adminId).orElse(null); + + String finalCategory = null; + + if (content != null) { + // 2. content에 카테고리가 이미 존재하면 그 값을 사용합니다. + if (content.getCategory() != null) { + finalCategory = content.getCategory(); + } + // 3. 카테고리가 없고, 옵션 타입이 SERVICE인 경우 Goods를 조회합니다. + else if (content.getOptionType() == OptionType.SERVICE) { + List goods = goodsRepository.findByContentId(content.getId()); + + // 4. (가장 중요) goods 리스트가 비어있지 않은지 반드시 확인합니다. + if (!goods.isEmpty()) { + finalCategory = goods.get(0).getBelonging(); + } + // goods가 비어있으면 finalCategory는 그대로 null로 유지됩니다. + } + } + return MapResponseDTO.StoreMapResponseDTO.builder() .storeId(s.getId()) + .adminName(admin.getName()) .adminId(adminId) .name(s.getName()) .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress()) @@ -148,7 +181,7 @@ public List searchStores(String keyword) { .optionType(content != null ? content.getOptionType() : null) .people(content != null ? content.getPeople() : null) .cost(content != null ? content.getCost() : null) - .category(content != null ? content.getCategory() : null) + .category(finalCategory) .discountRate(content != null ? content.getDiscount() : null) .hasPartner(hasPartner) .latitude(s.getLatitude()) diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java index 4286805e..9487feb1 100644 --- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java +++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java @@ -14,7 +14,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/dashBoard") +@RequestMapping("/admin/dashBoard") public class StudentAdminController { private final StudentAdminService studentAdminService; @Operation( diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java index b3c35232..6ae860c1 100644 --- a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java +++ b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java @@ -26,7 +26,6 @@ public class PaperController { private final PaperQueryService paperQueryService; - private final MemberRepository memberRepository; @GetMapping("/store/{storeId}/papers") @Operation(summary = "유저에게 적용 가능한 제휴 컨텐츠 조회", description = "유저가 속한 단과대, 학부 admin_id과 store_id 를 가진 제휴 컨텐츠 제공") diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java b/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java index ff376346..0a1ebdd6 100644 --- a/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java +++ b/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java @@ -7,10 +7,13 @@ import org.springframework.web.bind.annotation.RestController; import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.notification.service.NotificationCommandService; import com.assu.server.domain.partnership.dto.PaperResponseDTO; import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; import com.assu.server.domain.partnership.service.PartnershipService; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.global.apiPayload.BaseResponse; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import com.assu.server.global.util.PrincipalDetails; @@ -32,18 +35,19 @@ public class PartnershipController { private final PartnershipService partnershipService; + private final NotificationCommandService notificationCommandService; + private final StoreRepository storeRepository; @PostMapping("/usage") @Operation(summary= "유저의 인증 후 최종적으로 호출", description = "인증완료 화면 전에 바로 호출되어 유저의 제휴 내역에 데이터가 들어가게 됩니다. (개인 인증인 경우도 포함됩니다.)") - public ResponseEntity> finalPartnershipRequest( + public ResponseEntity> finalPartnershipRequest( @AuthenticationPrincipal PrincipalDetails userDetails,@RequestBody PartnershipRequestDTO.finalRequest dto ) { Member member = userDetails.getMember(); - partnershipService.recordPartnershipUsage(dto, member); - return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus.USER_PAPER_REQUEST_SUCCESS)); + return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.USER_PAPER_REQUEST_SUCCESS, null)); } diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java index c8b8fb14..b80e9a41 100644 --- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -26,12 +26,13 @@ public class PartnershipConverter { - public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Student student) { + public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Student student, Long paperId) { return PartnershipUsage.builder() .adminName(dto.getAdminName()) .date(LocalDate.now()) .place(dto.getPlaceName()) .student(student) + .paperId(paperId) .isReviewed(false) .contentId(dto.getContentId()) .partnershipContent(dto.getPartnershipContent()) @@ -116,6 +117,7 @@ public static PaperContentResponseDTO.storePaperContentResponse toContentRespons String paperContentText = buildPaperContentText(content, goodsList, peopleValue); return PaperContentResponseDTO.storePaperContentResponse.builder() + .adminId(content.getPaper().getAdmin().getId()) .adminName(content.getPaper().getAdmin().getName()) .cost(content.getCost()) .paperContent(paperContentText) @@ -127,7 +129,7 @@ public static PaperContentResponseDTO.storePaperContentResponse toContentRespons private static List extractGoods(PaperContent content) { - if (content.getOptionType() == OptionType.SERVICE && content.getCategory() != null) { + if (content.getOptionType() == OptionType.SERVICE ) { return content.getGoods().stream() .map(Goods::getBelonging) .toList(); diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java index 3aff7d4c..46dd3255 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java @@ -13,6 +13,7 @@ public class PaperContentResponseDTO { @NoArgsConstructor @AllArgsConstructor public static class storePaperContentResponse{ + Long adminId; String adminName; String paperContent; Long contentId; diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java index 1d9344cb..94706485 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java @@ -14,6 +14,8 @@ public class PartnershipRequestDTO { @Getter public static class finalRequest{ + Long storeId; + String tableNumber; String adminName; String placeName; String partnershipContent; diff --git a/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java index bede0456..31edb844 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java @@ -1,7 +1,14 @@ package com.assu.server.domain.partnership.repository; +import java.util.List; +import java.util.Optional; + import com.assu.server.domain.partnership.entity.Goods; import org.springframework.data.jpa.repository.JpaRepository; public interface GoodsRepository extends JpaRepository { + + List findByContentId(Long contentId); + + List findByContentIdIn(List contentIds); } 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 8a18d2fa..f70a5f86 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 @@ -8,6 +8,8 @@ import org.springframework.stereotype.Service; import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.notification.repository.NotificationRepository; +import com.assu.server.domain.notification.service.NotificationCommandService; import com.assu.server.domain.partnership.converter.PartnershipConverter; import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; import com.assu.server.domain.user.entity.PartnershipUsage; @@ -33,6 +35,7 @@ import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; +import com.assu.server.global.exception.GeneralException; import com.assu.server.infra.s3.AmazonS3Manager; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -50,14 +53,21 @@ public class PartnershipServiceImpl implements PartnershipService { private final PartnershipUsageRepository partnershipUsageRepository; private final StudentRepository studentRepository; + private final PaperContentRepository contentRepository; + private final NotificationCommandService notificationService; + public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ List usages = new ArrayList<>(); + PaperContent content = contentRepository.findById(dto.getContentId()).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) + ); + Long paperId = content.getPaper().getId(); // 1) 요청한 member 본인 - usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile())); + usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile(), paperId)); member.getStudentProfile().setStamp(); List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList()); @@ -65,14 +75,23 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe for (Long userId : userIds) { if(userId != member.getId()){ Student student = studentRepository.getReferenceById(userId); - usages.add(PartnershipConverter.toPartnershipUsage(dto, student)); + usages.add(PartnershipConverter.toPartnershipUsage(dto, student, paperId)); student.setStamp(); } } - partnershipUsageRepository.saveAll(usages); - + Store store = storeRepository.findById(dto.getStoreId()).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) + ); + Partner partner = store.getPartner(); + if (partner != null) { + Long partnerId = partner.getId(); + notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); + partnershipUsageRepository.saveAll(usages); + } else { + throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER); + } } 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 e756de02..7c460ba2 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 @@ -3,15 +3,7 @@ import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.partner.entity.Partner; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; @@ -28,7 +20,7 @@ public class Store extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "partner_id") private Partner partner; diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java index d50db89d..d1c7517d 100644 --- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java +++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java @@ -1,5 +1,7 @@ package com.assu.server.domain.user.controller; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; @@ -32,14 +34,12 @@ public class StudentController { summary = "월별 제휴 사용내역 조회 API", description = "# [v1.0 (2025-09-09)](https://www.notion.so/_-2241197c19ed8134bd49d8841e841634?source=copy_link)\n" + "- `multipart/form-data`로 호출합니다.\n" + - "- 처리: 정보 바탕으로 sessionManager에 session생성\n" + - "- 성공 시 201(Created)과 생성된 memberId 반환.\n" + "\n**Request Parts:**\n" + - " - `storeId` (Long, required): 스토어 id\n" + " - `year` (Integer, required): 년도\n" + " - `month` (Long, required): 월\n"+ "\n**Response:**\n" + " - 성공 시 partnership Usage 내역 반환 \n"+ + " - 해당 storeId, storeName 반환"+ " - 해당 월에 사용한 제휴 수 반환" ) public ResponseEntity> getMyPartnership( @@ -50,14 +50,37 @@ public ResponseEntity> getMyPartn return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.PARTNERSHIP_HISTORY_SUCCESS, result)); } + @GetMapping("/usage") + @Operation( + summary = "월별 제휴 사용내역 조회 API", + description = "# [v1.0 (2025-09-10)](https://www.notion.so/_-24c1197c19ed809a9d81e8f928e8355f?source=copy_link)\n" + + "- `multipart/form-data`로 호출합니다.\n" + + "\n**Request:**\n" + + " - page : (Int, required) 이상의 정수 \n" + + " - size : (Int, required) 기본 값 10 \n" + + " - sort : (String, required) createdAt,desc 문자열로 입력\n" + + "\n**Response:**\n" + + " - 성공 시 리뷰 되지 않은 partnership Usage 내역 반환 \n"+ + " - StudentResponseTO.UsageDetailDTO 객체 반환 \n" + + ) + public ResponseEntity>> getUnreviewedUsage( + @AuthenticationPrincipal PrincipalDetails pd, + Pageable pageable + ){ + return ResponseEntity.ok(BaseResponse + .onSuccess(SuccessStatus.UNREVIEWED_HISTORY_SUCCESS, + studentService.getUnreviewedUsage(pd.getId(), pageable))); + } + @Operation( summary = "사용자 stamp 개수 조회 API", - description = "# [v1.0 (2025-09-09)](https://www.notion.so/_-2241197c19ed8134bd49d8841e841634?source=copy_link)\n" + + description = "# [v1.0 (2025-09-09)](https://www.notion.so/2691197c19ed805c980dd546adee9301?source=copy_link)\n" + "- `multipart/form-data`로 호출합니다.\n" + - "- 처리: 정보 바탕으로 sessionManager에 session생성\n" + + "- login 필요 "+ "\n**Response:**\n" + " - stamp 개수 반환 \n" ) diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java index b8c1cb43..e7ee2706 100644 --- a/src/main/java/com/assu/server/domain/user/entity/Student.java +++ b/src/main/java/com/assu/server/domain/user/entity/Student.java @@ -49,10 +49,7 @@ public void setMember(Member member) { } public void setStamp() { - if (this.stamp == 10) - this.stamp = 1; - else - this.stamp++; + this.stamp++; } /** diff --git a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java index 75534364..d07c2160 100644 --- a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java @@ -3,6 +3,8 @@ import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -34,4 +36,10 @@ List findByYearAndMonth( ); Optional findById(Long id); + + + @Query("SELECT pu FROM PartnershipUsage pu " + + "WHERE pu.student.id = :studentId " + + "AND (pu.isReviewed = false)") + Page findByUnreviewedUsage(Long studentId, Pageable pageable); } diff --git a/src/main/java/com/assu/server/domain/user/service/StudentService.java b/src/main/java/com/assu/server/domain/user/service/StudentService.java index 9c7a214c..595613f0 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentService.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentService.java @@ -1,8 +1,13 @@ package com.assu.server.domain.user.service; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + import com.assu.server.domain.user.dto.StudentResponseDTO; public interface StudentService { StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month); StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId);//조회 + + Page getUnreviewedUsage(Long memberId, Pageable pageable); } diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java index 30232f3e..64f7b5e1 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java @@ -17,6 +17,10 @@ import com.assu.server.global.exception.DatabaseException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @Service @@ -68,4 +72,44 @@ public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int yea ) .build(); } + + + @Override + @Transactional + public Page getUnreviewedUsage(Long memberId, Pageable pageable) { + // 프론트에서 1-based 페이지를 보낸 경우 0-based 로 보정 + pageable = PageRequest.of( + Math.max(pageable.getPageNumber() - 1, 0), + pageable.getPageSize(), + pageable.getSort() + ); + + Page contentList = + partnershipUsageRepository.findByUnreviewedUsage(memberId, pageable); + + return contentList.map(u -> { + // 1. partnershipUsage의 paperContentId 로 paperContent 조회 + PaperContent paperContent = paperContentRepository.findById(u.getContentId()) + .orElse(null); + + // 2. store 추출 + Store store = (paperContent != null) ? paperContent.getPaper().getStore() : null; + + // 3. 날짜 포맷팅 + LocalDateTime ld = u.getCreatedAt(); + String formatDate = ld.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + + return StudentResponseDTO.UsageDetailDTO.builder() + .partnershipUsageId(u.getId()) + .adminName(u.getAdminName()) + .storeName(u.getPlace()) + .usedAt(formatDate) + .benefitDescription(u.getPartnershipContent()) + .isReviewed(u.getIsReviewed()) + .storeId((store != null) ? store.getId() : null) // store null 체크 + .partnerId((store != null && store.getPartner() != null) ? store.getPartner().getId() : null) + .build(); + }); + } + } 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 af3be54c..f0d331a3 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 @@ -51,10 +51,12 @@ public enum ErrorStatus implements BaseErrorCode { NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_4006", "존재하지 않는 스토어 ID입니다."), NO_SUCH_USAGE(HttpStatus.NOT_FOUND, "USAGE4001", "존재하지 않는 제휴 사용 내역입니다."), NO_PAPER_FOR_STORE(HttpStatus.NOT_FOUND, "ADMIN_4005", "존재하지 않는 paper ID입니다."), - EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4005","이미 존재하는 전화번호입니다."), - EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4006","이미 존재하는 이메일입니다."), - EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 학번입니다."), 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","이미 존재하는 학번입니다."), + MEMBER_ALREADY_WITHDRAWN(HttpStatus.BAD_REQUEST, "MEMBER_4010", "이미 탈퇴된 회원입니다."), // 제휴 에러 diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java index 4e185c4b..0c41f665 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java @@ -37,6 +37,7 @@ public enum SuccessStatus implements BaseCode { USER_PAPER_REQUEST_SUCCESS(HttpStatus.OK, "PAPER202", "제휴 요청이 성공적으로 처리되었습니다."), PARTNERSHIP_HISTORY_SUCCESS(HttpStatus.OK, "PARTNERSHIP202", "월 별 제휴 사용내역이 성공적으로 조회되었습니다."), + UNREVIEWED_HISTORY_SUCCESS(HttpStatus.OK, "PARTNERSHIP203", "리뷰 되지 않은 제휴 사용내역이 성공적으로 조회되었습니다."), // 그룹 인증 GROUP_SESSION_CREATE(HttpStatus.OK, "GROUP201", "인증 세션 생성 및 대표자 구독이 완료되었습니다."), diff --git a/src/main/java/com/assu/server/global/config/SecurityConfig.java b/src/main/java/com/assu/server/global/config/SecurityConfig.java index 62263533..8d7f72b8 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -21,6 +21,9 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + // ✅ WebSocket 핸드셰이크 허용 (네이티브 + SockJS 모두 포함) + .requestMatchers("/ws/**").permitAll() + // Swagger 등 공개 리소스 .requestMatchers( "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", @@ -40,8 +43,9 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF "/auth/students/login", "/auth/students/ssu-verify" ).permitAll() + .requestMatchers("/ws/**").permitAll() + // 나머지는 인증 필요 - // 나머지 요청은 JwtAuthFilter가 화이트리스트/보호자원 판별 .anyRequest().authenticated() ) .formLogin(form -> form.disable()) diff --git a/src/main/java/com/assu/server/global/util/PrincipalDetails.java b/src/main/java/com/assu/server/global/util/PrincipalDetails.java index 1e21ab1b..dc7582be 100644 --- a/src/main/java/com/assu/server/global/util/PrincipalDetails.java +++ b/src/main/java/com/assu/server/global/util/PrincipalDetails.java @@ -13,9 +13,11 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; +import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; @Getter @Builder diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b6722ba5..61918f57 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,19 +19,23 @@ spring: highlight_sql : true lifecycle: timeout-per-shutdown-phase: 30s - rabbitmq: - listener: - simple: - acknowledge-mode: manual - prefetch: 20 - concurrency: 1 - max-concurrency: 4 - default-requeue-rejected: false + rabbitmq: + listener: + simple: + acknowledge-mode: manual + prefetch: 20 + concurrency: 1 + max-concurrency: 4 + default-requeue-rejected: false logging: level: org.springframework.web: DEBUG org.springframework.web.client.DefaultRestClient: OFF + org.springframework.messaging.simp: DEBUG + org.springframework.messaging.handler: DEBUG + org.springframework.messaging: DEBUG + org.springframework.web.socket: DEBUG server: shutdown: graceful \ No newline at end of file diff --git a/src/test/java/com/assu/server/ServerApplicationTests.java b/src/test/java/com/assu/server/ServerApplicationTests.java index 64bd3ef7..da762c7d 100644 --- a/src/test/java/com/assu/server/ServerApplicationTests.java +++ b/src/test/java/com/assu/server/ServerApplicationTests.java @@ -1,6 +1,7 @@ package com.assu.server; import com.assu.server.domain.auth.security.jwt.JwtUtil; +//import com.google.api.client.http.javanet.ConnectionFactory; import com.google.firebase.messaging.FirebaseMessaging; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -22,8 +23,8 @@ @ActiveProfiles("test") class ServerApplicationTests { - @Mock - private FirebaseMessaging firebaseMessaging; + @Mock + private FirebaseMessaging firebaseMessaging; @MockitoBean private ConnectionFactory connectionFactory; @@ -37,40 +38,40 @@ FirebaseMessaging firebaseMessaging() { return Mockito.mock(FirebaseMessaging.class); } - @Bean - RedisConnectionFactory redisConnectionFactory() { - return Mockito.mock(RedisConnectionFactory.class); - } + @Bean + RedisConnectionFactory redisConnectionFactory() { + return Mockito.mock(RedisConnectionFactory.class); + } - @Bean + @Bean @SuppressWarnings("unchecked") - RedisTemplate redisTemplate() { - return Mockito.mock(RedisTemplate.class); - } - - @Bean - StringRedisTemplate stringRedisTemplate() { - return Mockito.mock(StringRedisTemplate.class); - } - - @Bean - JwtUtil jwtUtil() { - return Mockito.mock(JwtUtil.class); - } - - @Bean(name = "rabbitListenerContainerFactory") - RabbitListenerContainerFactory rabbitListenerContainerFactory() { - var factory = Mockito.mock(RabbitListenerContainerFactory.class); - var container = Mockito.mock(org.springframework.amqp.rabbit.listener.MessageListenerContainer.class); - Mockito.when(factory.createListenerContainer(Mockito.any())) - .thenReturn(container); - return factory; - } - - } - - @Test - void contextLoads() { - } + RedisTemplate redisTemplate() { + return Mockito.mock(RedisTemplate.class); + } + + @Bean + StringRedisTemplate stringRedisTemplate() { + return Mockito.mock(StringRedisTemplate.class); + } + + @Bean + JwtUtil jwtUtil() { + return Mockito.mock(JwtUtil.class); + } + + @Bean(name = "rabbitListenerContainerFactory") + RabbitListenerContainerFactory rabbitListenerContainerFactory() { + var factory = Mockito.mock(RabbitListenerContainerFactory.class); + var container = Mockito.mock(org.springframework.amqp.rabbit.listener.MessageListenerContainer.class); + Mockito.when(factory.createListenerContainer(Mockito.any())) + .thenReturn(container); + return factory; + } + + } + + @Test + void contextLoads() { + } } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index ac859d1f..8a3197b2 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -21,6 +21,11 @@ jwt: access-valid-seconds: 3600 refresh-valid-seconds: 1209600 +assu: + security: + school-crypto: + base64-key: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=" #"dummy-base64-key"를 Base64로 인코딩한 값 + messaging: rabbit: enabled: false