Skip to content

Commit b5b62af

Browse files
authored
[Fix]:WebSocket 개인 알림 수정
BidService: 입찰 밀림 알림 추가 WebSocketAuthInterceptor: JWT토큰 검증 및 Websocket 세션에 사용자 정보 설정 WebSocketConfig: 인터셉터 등록 및 prefix 설정 bid-test.html: 테스트용 html
2 parents bd2c319 + 6ef5bed commit b5b62af

File tree

7 files changed

+269
-92
lines changed

7 files changed

+269
-92
lines changed

src/main/java/com/backend/domain/bid/service/BidService.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ public RsData<BidResponseDto> createBidInternal(Long productId, Long bidderId, B
5757
// 유효성 검증
5858
validateBid(product, member, request.price());
5959

60+
Long previousHighestPrice = bidRepository.findHighestBidPrice(productId).orElse(null);
61+
62+
// 이전 최고 입찰자 확인 (입찰 밀림 알림용)
63+
Bid previousHighestBid = null;
64+
if (previousHighestPrice != null) {
65+
List<Bid> recentBids = bidRepository.findNBids(productId, 10);
66+
previousHighestBid = recentBids.stream()
67+
.filter(bid -> bid.getBidPrice().equals(previousHighestPrice))
68+
.findFirst()
69+
.orElse(null);
70+
}
71+
6072
// 입찰 생성 및 저장
6173
Bid savedBid = saveBid(product, member, request.price());
6274

@@ -69,9 +81,19 @@ public RsData<BidResponseDto> createBidInternal(Long productId, Long bidderId, B
6981
// 실시간 브로드캐스트
7082
webSocketService.broadcastBidUpdate(productId, bidResponse);
7183

72-
// 입찰 성공 알림
84+
// 입찰 성공 알림 (현재 입찰자에게)
7385
bidNotificationService.notifyBidSuccess(bidderId, product, request.price());
7486

87+
// 입찰 밀림 알림 (이전 최고 입찰자에게)
88+
if (previousHighestBid != null && !previousHighestBid.getMember().getId().equals(bidderId)) {
89+
bidNotificationService.notifyBidOutbid(
90+
previousHighestBid.getMember().getId(),
91+
product,
92+
previousHighestBid.getBidPrice(),
93+
request.price()
94+
);
95+
}
96+
7597
return RsData.created("입찰이 완료되었습니다.", bidResponse);
7698
}
7799

src/main/java/com/backend/domain/notification/service/AuctionNotificationService.java

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,17 @@ public void notifyAuctionStart(Long userId, Product product) {
3333
"endTime", product.getEndTime().toString()
3434
);
3535

36-
webSocketService.sendNotificationToUser(userId.toString(), message, data);
37-
38-
// DB 큐에도 저장
36+
// userId로 Member 조회 후 email로 전송
3937
Member member = memberRepository.findById(userId).orElse(null);
4038
if (member != null) {
39+
webSocketService.sendNotificationToUser(member.getEmail(), message, data);
40+
41+
// DB 큐에도 저장
4142
notificationQueueService.enqueueNotification(member, message, "AUCTION_START", product);
43+
44+
log.info("경매 시작 알림 전송 - 사용자: {} ({}), 상품: {}",
45+
userId, member.getEmail(), product.getId());
4246
}
43-
44-
log.info("경매 시작 알림 전송 - 사용자: {}, 상품: {}", userId, product.getId());
4547
}
4648

4749
// 경매 곧 종료 알림 (10분 전)
@@ -58,15 +60,17 @@ public void notifyAuctionEndingSoon(Long userId, Product product, long remaining
5860
"endTime", product.getEndTime().toString()
5961
);
6062

61-
webSocketService.sendNotificationToUser(userId.toString(), message, data);
62-
63-
// DB 큐에도 저장
63+
// userId로 Member 조회 후 email로 전송
6464
Member member = memberRepository.findById(userId).orElse(null);
6565
if (member != null) {
66+
webSocketService.sendNotificationToUser(member.getEmail(), message, data);
67+
68+
// DB 큐에도 저장
6669
notificationQueueService.enqueueNotification(member, message, "AUCTION_ENDING_SOON", product);
70+
71+
log.info("경매 종료 임박 알림 전송 - 사용자: {} ({}), 상품: {}, 남은 시간: {}분",
72+
userId, member.getEmail(), product.getId(), remainingMinutes);
6773
}
68-
69-
log.info("경매 종료 임박 알림 전송 - 사용자: {}, 상품: {}, 남은 시간: {}분", userId, product.getId(), remainingMinutes);
7074
}
7175

7276
// 경매 종료 알림
@@ -84,14 +88,16 @@ public void notifyAuctionEnd(Long userId, Product product, boolean hasWinner, Lo
8488
"status", hasWinner ? "낙찰" : "유찰"
8589
);
8690

87-
webSocketService.sendNotificationToUser(userId.toString(), message, data);
88-
89-
// DB 큐에도 저장
91+
// userId로 Member 조회 후 email로 전송
9092
Member member = memberRepository.findById(userId).orElse(null);
9193
if (member != null) {
94+
webSocketService.sendNotificationToUser(member.getEmail(), message, data);
95+
96+
// DB 큐에도 저장
9297
notificationQueueService.enqueueNotification(member, message, "AUCTION_END", product);
98+
99+
log.info("경매 종료 알림 전송 - 사용자: {} ({}), 상품: {}, 결과: {}",
100+
userId, member.getEmail(), product.getId(), hasWinner ? "낙찰" : "유찰");
93101
}
94-
95-
log.info("경매 종료 알림 전송 - 사용자: {}, 상품: {}, 결과: {}", userId, product.getId(), hasWinner ? "낙찰" : "유찰");
96102
}
97103
}

src/main/java/com/backend/domain/notification/service/BidNotificationService.java

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,16 @@ public void notifyBidSuccess(Long userId, Product product, Long bidAmount) {
3131
"bidAmount", bidAmount
3232
);
3333

34-
webSocketService.sendNotificationToUser(userId.toString(), message, data);
35-
36-
// DB 큐에도 저장
34+
// userId로 Member 조회 후 email로 전송
3735
Member member = memberRepository.findById(userId).orElse(null);
3836
if (member != null) {
37+
webSocketService.sendNotificationToUser(member.getEmail(), message, data);
38+
3939
notificationQueueService.enqueueNotification(member, message, "BID_SUCCESS", product);
40+
41+
log.info("입찰 성공 알림 전송 - 사용자: {} ({}), 상품: {}, 금액: {}",
42+
userId, member.getEmail(), product.getId(), bidAmount);
4043
}
41-
42-
log.info("입찰 성공 알림 전송 - 사용자: {}, 상품: {}, 금액: {}", userId, product.getId(), bidAmount);
4344
}
4445

4546
// 입찰 밀림 알림 (더 높은 입찰이 들어왔을 때)
@@ -55,15 +56,17 @@ public void notifyBidOutbid(Long userId, Product product, Long myBidAmount, Long
5556
"newHighestBid", newHighestBid
5657
);
5758

58-
webSocketService.sendNotificationToUser(userId.toString(), message, data);
59-
60-
// DB 큐에도 저장
59+
// userId로 Member 조회 후 email로 전송
6160
Member member = memberRepository.findById(userId).orElse(null);
6261
if (member != null) {
62+
webSocketService.sendNotificationToUser(member.getEmail(), message, data);
63+
64+
// DB 큐에도 저장
6365
notificationQueueService.enqueueNotification(member, message, "BID_OUTBID", product);
66+
67+
log.info("입찰 밀림 알림 전송 - 사용자: {} ({}), 상품: {}",
68+
userId, member.getEmail(), product.getId());
6469
}
65-
66-
log.info("입찰 밀림 알림 전송 - 사용자: {}, 상품: {}", userId, product.getId());
6770
}
6871

6972
// 경매 종료 - 낙찰 알림
@@ -78,15 +81,17 @@ public void notifyAuctionWon(Long winnerId, Product product, Long finalPrice) {
7881
"finalPrice", finalPrice
7982
);
8083

81-
webSocketService.sendNotificationToUser(winnerId.toString(), message, data);
82-
83-
// DB 큐에도 저장
84+
// winnerId로 Member 조회 후 email로 전송
8485
Member member = memberRepository.findById(winnerId).orElse(null);
8586
if (member != null) {
87+
webSocketService.sendNotificationToUser(member.getEmail(), message, data);
88+
89+
// DB 큐에도 저장
8690
notificationQueueService.enqueueNotification(member, message, "AUCTION_WON", product);
91+
92+
log.info("낙찰 알림 전송 - 사용자: {} ({}), 상품: {}, 낙찰가: {}",
93+
winnerId, member.getEmail(), product.getId(), finalPrice);
8794
}
88-
89-
log.info("낙찰 알림 전송 - 사용자: {}, 상품: {}, 낙찰가: {}", winnerId, product.getId(), finalPrice);
9095
}
9196

9297
// 경매 종료 - 유찰 알림
@@ -102,14 +107,16 @@ public void notifyAuctionLost(Long userId, Product product, Long finalPrice, Lon
102107
"myBidAmount", myBidAmount
103108
);
104109

105-
webSocketService.sendNotificationToUser(userId.toString(), message, data);
106-
107-
// DB 큐에도 저장
110+
// userId로 Member 조회 후 email로 전송
108111
Member member = memberRepository.findById(userId).orElse(null);
109112
if (member != null) {
113+
webSocketService.sendNotificationToUser(member.getEmail(), message, data);
114+
115+
// DB 큐에도 저장
110116
notificationQueueService.enqueueNotification(member, message, "AUCTION_LOST", product);
117+
118+
log.info("낙찰 실패 알림 전송 - 사용자: {} ({}), 상품: {}",
119+
userId, member.getEmail(), product.getId());
111120
}
112-
113-
log.info("낙찰 실패 알림 전송 - 사용자: {}, 상품: {}", userId, product.getId());
114121
}
115122
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.backend.global.websocket.config;
2+
3+
import com.backend.global.security.JwtUtil;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.messaging.Message;
7+
import org.springframework.messaging.MessageChannel;
8+
import org.springframework.messaging.simp.stomp.StompCommand;
9+
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
10+
import org.springframework.messaging.support.ChannelInterceptor;
11+
import org.springframework.messaging.support.MessageHeaderAccessor;
12+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
13+
import org.springframework.security.core.Authentication;
14+
import org.springframework.security.core.userdetails.UserDetails;
15+
import org.springframework.security.core.userdetails.UserDetailsService;
16+
import org.springframework.stereotype.Component;
17+
18+
@Component
19+
@RequiredArgsConstructor
20+
@Slf4j
21+
public class WebSocketAuthInterceptor implements ChannelInterceptor {
22+
23+
private final JwtUtil jwtUtil;
24+
private final UserDetailsService userDetailsService;
25+
26+
@Override
27+
public Message<?> preSend(Message<?> message, MessageChannel channel) {
28+
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
29+
30+
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
31+
// STOMP CONNECT 시 Authorization 헤더에서 JWT 토큰 추출
32+
String authHeader = accessor.getFirstNativeHeader("Authorization");
33+
34+
if (authHeader != null && authHeader.startsWith("Bearer ")) {
35+
String token = authHeader.substring(7);
36+
37+
try {
38+
if (jwtUtil.validateToken(token)) {
39+
String email = jwtUtil.getEmailFromToken(token);
40+
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
41+
42+
Authentication authentication = new UsernamePasswordAuthenticationToken(
43+
userDetails, null, userDetails.getAuthorities()
44+
);
45+
46+
// WebSocket 세션에 사용자 정보 설정
47+
accessor.setUser(authentication);
48+
49+
log.info("WebSocket 인증 성공: {}", email);
50+
}
51+
} catch (Exception e) {
52+
log.error("WebSocket 인증 실패: {}", e.getMessage());
53+
}
54+
}
55+
}
56+
57+
return message;
58+
}
59+
}

src/main/java/com/backend/global/websocket/config/WebSocketConfig.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package com.backend.global.websocket.config;
22

3+
import lombok.RequiredArgsConstructor;
34
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.messaging.simp.config.ChannelRegistration;
46
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
57
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
68
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
79
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
810

911
@Configuration
1012
@EnableWebSocketMessageBroker
13+
@RequiredArgsConstructor
1114
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
1215

16+
private final WebSocketAuthInterceptor webSocketAuthInterceptor;
17+
1318
@Override
1419
public void registerStompEndpoints(StompEndpointRegistry registry) {
1520
// WebSocket 연결 엔드포인트 설정
@@ -21,9 +26,16 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {
2126
@Override
2227
public void configureMessageBroker(MessageBrokerRegistry registry) {
2328
// 클라이언트가 구독할 경로
24-
registry.enableSimpleBroker("/topic");
29+
registry.enableSimpleBroker("/topic", "/queue");
2530
// 클라이언트에서 서버로 메시지를 보낼 때 사용할 경로
2631
registry.setApplicationDestinationPrefixes("/app");
32+
// 개인 메시지를 위한 사용자별 prefix 설정
33+
registry.setUserDestinationPrefix("/user");
34+
}
2735

36+
@Override
37+
public void configureClientInboundChannel(ChannelRegistration registration) {
38+
// WebSocket 인증 인터셉터 등록
39+
registration.interceptors(webSocketAuthInterceptor);
2840
}
2941
}

src/main/java/com/backend/global/websocket/service/WebSocketService.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@ public void broadcastAuctionEndingSoon(Long productId, String productName) {
5151
sendToTopic("bid/" + productId, message);
5252
}
5353

54-
// 개인 알림 전송 (특정 사용자)
55-
public void sendNotificationToUser(String userId, String message, Object data) {
54+
// 개인 알림 전송 (특정 사용자) - 이메일 기반
55+
public void sendNotificationToUser(String userEmail, String message, Object data) {
5656
WebSocketMessage webSocketMessage = WebSocketMessage.of(
5757
WebSocketMessage.MessageType.NOTIFICATION,
5858
"system",
5959
message,
6060
data
6161
);
62-
messagingTemplate.convertAndSendToUser(userId, "/queue/notifications", webSocketMessage);
63-
log.info("개인 알림 전송 - 사용자: {}, 메시지: {}", userId, message);
62+
messagingTemplate.convertAndSendToUser(userEmail, "/queue/notifications", webSocketMessage);
63+
log.info("개인 알림 전송 - 사용자: {}, 메시지: {}", userEmail, message);
6464
}
6565

6666
// 경매 종료 알림 브로드캐스트

0 commit comments

Comments
 (0)