Skip to content

Commit 9dee264

Browse files
authored
merge: pull request #47 from feat/request/2
Feat/request/2
2 parents 7bdd9b4 + 2a132ad commit 9dee264

File tree

8 files changed

+128
-27
lines changed

8 files changed

+128
-27
lines changed

src/main/java/org/dfbf/soundlink/domain/chat/controller/ChatController.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
11
package org.dfbf.soundlink.domain.chat.controller;
22

3+
import io.lettuce.core.dynamic.annotation.Param;
34
import io.swagger.v3.oas.annotations.Operation;
45
import io.swagger.v3.oas.annotations.tags.Tag;
56
import lombok.RequiredArgsConstructor;
67
import org.dfbf.soundlink.domain.chat.service.ChatRoomService;
78
import org.dfbf.soundlink.global.exception.ResponseResult;
89
import org.springframework.security.core.annotation.AuthenticationPrincipal;
9-
import org.springframework.web.bind.annotation.PostMapping;
10-
import org.springframework.web.bind.annotation.RequestMapping;
11-
import org.springframework.web.bind.annotation.RequestParam;
12-
import org.springframework.web.bind.annotation.RestController;
10+
import org.springframework.web.bind.annotation.*;
1311

1412
@RestController
13+
@RequestMapping("/api/chat")
1514
@RequiredArgsConstructor
16-
@RequestMapping("/api/chat/chatRoom")
17-
@Tag(name = "ChatRoom API", description = "채팅방 관련 API")
15+
@Tag(name = "Chat API", description = "채팅 관련 API")
1816
public class ChatController {
17+
1918
private final ChatRoomService chatRoomService;
2019

20+
@PostMapping("/request")
21+
@Operation(summary = "채팅 요청 API", description = "채팅 요청 (상세 정보는 노션 API 명세 확인)")
22+
public ResponseResult requestChat(@AuthenticationPrincipal Long id, @RequestBody Long emotionRecordId) {
23+
return chatRoomService.saveRequestToRedis(id, emotionRecordId);
24+
}
25+
26+
@DeleteMapping("/request")
27+
@Operation(summary = "채팅 요청 취소 API", description = "채팅 요청 취소")
28+
public ResponseResult cancelChatRequest(@AuthenticationPrincipal Long id, @RequestBody Long emotionRecordId) {
29+
return chatRoomService.deleteRequestFromRedis(id, emotionRecordId);
30+
}
31+
2132
@PostMapping("/create")
2233
@Operation(summary = "채팅요청 시 채팅방 생성", description = "requestId, responseId 값 확인")
2334
public ResponseResult create(@AuthenticationPrincipal Long userId, @RequestParam Long recordId) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.dfbf.soundlink.domain.chat.entity.redis;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
6+
import java.io.Serializable;
7+
8+
@Data
9+
@AllArgsConstructor
10+
public class ChatRequest implements Serializable {
11+
Long requestId;
12+
Long responseId;
13+
Long emotionRecordId;
14+
}
15+

src/main/java/org/dfbf/soundlink/domain/chat/service/ChatRoomService.java

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.dfbf.soundlink.domain.chat.service;
22

33
import lombok.RequiredArgsConstructor;
4+
import org.dfbf.soundlink.domain.chat.entity.redis.ChatRequest;
45
import org.dfbf.soundlink.domain.chat.dto.ChatReqDto;
56
import org.dfbf.soundlink.domain.chat.entity.ChatRoom;
67
import org.dfbf.soundlink.domain.chat.exception.ChatRoomNotFoundException;
@@ -21,29 +22,88 @@
2122
import org.springframework.stereotype.Service;
2223
import org.springframework.transaction.annotation.Transactional;
2324

25+
import java.time.Duration;
2426
import java.sql.Timestamp;
2527

26-
2728
@Service
2829
@RequiredArgsConstructor
2930
public class ChatRoomService {
31+
32+
private final RedisTemplate<String, Object> redisTemplate;
33+
private final EmotionRecordRepository emotionRecordRepository;
3034
private final ChatRoomRepository chatRoomRepository;
3135
private final UserRepository userRepository;
32-
private final EmotionRecordRepository emotionRecordRepository;
33-
private final RedisTemplate<String, String> redisTemplate;
3436

37+
private static final String CHAT_REQUEST_KEY = "chatRequest";
38+
39+
// 요청을 Redis에 저장 (TTL: 60초)
40+
public ResponseResult saveRequestToRedis(Long requestUserId, Long emotionRecordId) {
41+
try {
42+
// 응답자의 ID를 EmotionRecord에서 가져옴
43+
Long responseUserId = emotionRecordRepository.findById(emotionRecordId)
44+
.orElseThrow(EmotionRecordNotFoundException::new)
45+
.getUser()
46+
.getUserId();
47+
48+
// 요청자와 응답자가 같은 경우
49+
if (requestUserId.equals(responseUserId)) {
50+
return new ResponseResult(400, "You can't chat with yourself.");
51+
}
52+
53+
// Redis에 이미 requestUserId가 포함되어 있는 경우
54+
if (!redisTemplate.keys(CHAT_REQUEST_KEY + requestUserId + "to*").isEmpty()) {
55+
String firstKey = redisTemplate.keys(CHAT_REQUEST_KEY + requestUserId + "to*").iterator().next(); // 첫 번째 키 가져오기
56+
Long ttl = redisTemplate.getExpire(firstKey);
57+
return new ResponseResult(400, ttl + "초 후에 다시 시도해주세요.");
58+
}
59+
60+
// Key & Request 객체 생성
61+
String key = CHAT_REQUEST_KEY + requestUserId + "to" + emotionRecordId;
62+
ChatRequest chatRequest = new ChatRequest(requestUserId, responseUserId, emotionRecordId);
63+
64+
// Redis 저장
65+
redisTemplate.opsForValue().set(key, chatRequest, Duration.ofSeconds(61));
66+
67+
return new ResponseResult(ErrorCode.SUCCESS);
68+
} catch (EmotionRecordNotFoundException e) {
69+
return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD);
70+
} catch (Exception e) {
71+
return new ResponseResult(400, "Chat request failed.");
72+
}
73+
}
74+
75+
// 요청을 삭제
76+
public ResponseResult deleteRequestFromRedis(Long requestUserId, Long emotionRecordId) {
77+
try {
78+
// Key 생성
79+
String key = CHAT_REQUEST_KEY + requestUserId + "to" + emotionRecordId;
80+
81+
// Redis에 Key가 존재하는 경우 삭제 (KEY가 없는 경우 400)
82+
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
83+
redisTemplate.delete(key);
84+
return new ResponseResult(ErrorCode.SUCCESS);
85+
} else {
86+
return new ResponseResult(400, "ChatRequest not found or expired.");
87+
}
88+
89+
} catch (EmotionRecordNotFoundException e) {
90+
return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD);
91+
} catch (Exception e) {
92+
return new ResponseResult(400, "Chat request failed.");
93+
}
3594

3695
@Transactional
3796
public ResponseResult createChatRoom(@AuthenticationPrincipal Long userId, Long recordId){
3897
try {
39-
//요청 보내는사람
98+
// 요청 보내는사람
4099
User requestUserId = userRepository.findById(userId)
41100
.orElseThrow(UserNotFoundException::new);
42101

43-
//감정기록 조회
102+
// 감정기록 조회
44103
EmotionRecord emotionRecord = emotionRecordRepository.findById(recordId)
45104
.orElseThrow(EmotionRecordNotFoundException::new);
46-
//이미 존재하는 채팅방인지 확인
105+
106+
// 이미 존재하는 채팅방인지 확인
47107
if(chatRoomRepository.existsByRequestUserIdAndRecordId(requestUserId,emotionRecord)){
48108
return new ResponseResult(ErrorCode.CHAT_FAILED, "이미 존재하는 채팅방입니다.");
49109
}
@@ -58,41 +118,40 @@ public ResponseResult createChatRoom(@AuthenticationPrincipal Long userId, Long
58118
.endTime(null)
59119
.build();
60120

61-
//DB에 저장
121+
// DB에 저장
62122
chatRoomRepository.save(chatRoom);
63123

64124
ChatReqDto chatReqDto = new ChatReqDto(userId, responseUserId);
65-
//레디스에 저장
125+
126+
// 레디스에 저장
66127
redisTemplate.opsForValue().set("Room::"+chatRoom.getChatRoomId(), String.valueOf(chatReqDto));
67128

68129
return new ResponseResult(ErrorCode.SUCCESS, chatRoom);
69130
} catch (Exception e) {
70131
return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage());
71132
}
72-
73133
}
74134

75-
//채팅방 닫기
135+
// 채팅방 닫기
76136
@Transactional
77137
public ResponseResult closeChatRoom(@AuthenticationPrincipal Long userId, Long chatRoomId) {
78138
try {
79139
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
80140
.orElseThrow(ChatRoomNotFoundException::new);
81141

82-
//요청자 또는 응답자가 아니면 예외 처리
142+
// 요청자 또는 응답자가 아니면 예외 처리
83143
if(!chatRoom.getRequestUserId().getUserId().equals(userId) &&
84144
!chatRoom.getRecordId().getUser().getUserId().equals(userId)) {
85-
throw new UnauthorizedAccessException();//권한이 없을 경우 예외 발생
145+
throw new UnauthorizedAccessException(); // 권한이 없을 경우 예외 발생
86146
}
87147

88-
chatRoom.updateChatRoomStatus(RoomStatus.CLOSED); //삳태 '닫기'로 변경
89-
chatRoomRepository.save(chatRoom);//DB에 저장
148+
chatRoom.updateChatRoomStatus(RoomStatus.CLOSED); // 삳태 '닫기'로 변경
149+
chatRoomRepository.save(chatRoom); // DB에 저장
90150

91-
redisTemplate.delete("Room::"+chatRoomId);//레디스에서 삭제
151+
redisTemplate.delete("Room::"+chatRoomId); // 레디스에서 삭제
92152
return new ResponseResult(ErrorCode.SUCCESS);
93153
} catch (Exception e) {
94154
return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage());
95155
}
96156
}
97-
98157
}

src/main/java/org/dfbf/soundlink/domain/emotionRecord/controller/EmotionRecordController.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public class EmotionRecordController {
2727
summary = "감정 기록 작성/저장 API",
2828
description = "작성한 감정 기록을 저장합니다."
2929
)
30-
3130
public ResponseResult saveEmotionWithMusic(
3231
@AuthenticationPrincipal Long userId,
3332
@Valid @RequestBody EmotionRecordRequestDTO request) {

src/main/java/org/dfbf/soundlink/domain/user/config/RedisConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public CacheManager contentCacheManager(RedisConnectionFactory cf) {
4444
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
4545
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
4646
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경
47-
.entryTtl(Duration.ofMinutes(60L)); // 캐시 수명 10분
47+
.entryTtl(Duration.ofMinutes(30L)); // 캐시 수명 30분
4848

4949
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
5050
}

src/main/java/org/dfbf/soundlink/domain/user/repository/dsl/UserRepositoryImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public String findPasswordByLoginId(String loginId) {
6868
}
6969

7070
@Override
71-
@Cacheable(value = "user", key = "#userId", unless = "#result == null")
71+
@Cacheable(value = "user", key = "#p0")
7272
public Optional<User> findByUserIdWithCache(Long userId) {
7373
return Optional.ofNullable(
7474
jpaQueryFactory
@@ -78,7 +78,7 @@ public Optional<User> findByUserIdWithCache(Long userId) {
7878
}
7979

8080
@Override
81-
@CachePut(value = "user", key = "#user.userId")
81+
@CachePut(value = "user", key = "#p0.userId")
8282
public User saveWithCache(User user) {
8383
jpaQueryFactory
8484
.update(QUser.user)

src/main/java/org/dfbf/soundlink/domain/user/service/UserService.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.HashMap;
3535
import java.util.Map;
3636
import java.util.Optional;
37+
import java.util.concurrent.TimeUnit;
3738

3839
@Slf4j
3940
@Service
@@ -51,7 +52,7 @@ public class UserService {
5152
private final JwtProvider jwtProvider;
5253
private final TokenProperties tokenProperties;
5354

54-
private RedisTemplate<String, String> redisTemplate;
55+
private final RedisTemplate<String, String> redisTemplate;
5556
private final TokenService tokenService;
5657

5758
private static final String domain = "";
@@ -70,6 +71,7 @@ public ResponseResult signUp(UserSignUpDto userSignUpDto) {
7071
@Transactional
7172
public ResponseResult getUser(Long userId) {
7273
try {
74+
log.info("getUser" + "userId: " + userId);
7375
User user = userRepository.findByUserIdWithCache(userId)
7476
.orElseThrow(NoUserDataException::new);
7577
UserGetDto result = new UserGetDto(user);
@@ -240,6 +242,9 @@ public ResponseResult login(LoginReqDto loginReqDto, HttpServletResponse respons
240242
User user = userRepository.findByLoginId(loginReqDto.loginId())
241243
.orElseThrow(NoUserDataException::new);
242244

245+
// 로그인하면 Redis에 유저데이터 캐싱
246+
userRepository.findByUserIdWithCache(user.getUserId());
247+
243248
String accessToken = jwtProvider.createAccessToken(user.getUserId());
244249
String refreshToken = jwtProvider.createRefreshToken(user.getUserId());
245250

@@ -258,6 +263,12 @@ public ResponseResult login(LoginReqDto loginReqDto, HttpServletResponse respons
258263
}
259264
}
260265

266+
// Redis에서 유저 캐시 삭제
267+
private void evictUserCache(Long userId) {
268+
String key = "user::" + userId;
269+
redisTemplate.delete(key); // 직접 삭제
270+
}
271+
261272
// 로그아웃
262273
public ResponseResult logout(HttpServletResponse response, HttpServletRequest request) {
263274
try {
@@ -276,10 +287,12 @@ public ResponseResult logout(HttpServletResponse response, HttpServletRequest re
276287
Long userId = jwtProvider.getUserId(accessToken); // 액세스 토큰을 넘겨서 userId 추출
277288

278289
tokenService.deleteRefreshToken(userId);
290+
this.evictUserCache(userId);
279291

280292
return new ResponseResult(ErrorCode.SUCCESS,"로그아웃 되었습니다.");
281293

282294
} catch (Exception e) {
295+
log.info("[ERROR] " + e.getMessage());
283296
return new ResponseResult(ErrorCode. INTERNAL_SERVER_ERROR,"로그아웃 중 오류가 발생했습니다.");
284297
}
285298
}
@@ -318,6 +331,7 @@ public ResponseResult getProfile(String tag) {
318331
public ResponseResult reissueToken(HttpServletRequest request, HttpServletResponse response) {
319332

320333
String refreshToken = jwtProvider.resolveRefreshToken(request);
334+
log.info("[REFRESH_TOKEN] " + refreshToken);
321335

322336
if (refreshToken == null) {
323337
logout(response,request);
@@ -345,6 +359,8 @@ public ResponseResult reissueToken(HttpServletRequest request, HttpServletRespon
345359
Map<String, String> responseBody = new HashMap<>();
346360
responseBody.put("accessToken", newAccessToken);
347361

362+
// Redis에서 user::userID TTL을 30분으로 다시 갱신
363+
redisTemplate.expire("user::" + userId, 30L * 60, TimeUnit.SECONDS);
348364

349365
return new ResponseResult(ErrorCode.SUCCESS, responseBody);
350366
} else {

src/main/java/org/dfbf/soundlink/global/auth/JwtAuthenticationFilter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
4545
handleException(response, ErrorCode.TOKEN_INVALID);
4646

4747
} catch (Exception ex) {
48+
logger.error(ex.getMessage(), ex);
4849
handleException(response, ErrorCode.INTERNAL_SERVER_ERROR);
4950
}
5051
}

0 commit comments

Comments
 (0)