Skip to content

Commit bd67b1d

Browse files
authored
Merge pull request #288 from prgrms-web-devcourse-final-project/refactor#287
[refactor] 로그아웃 시 세션 정보 삭제 기능 추가
2 parents e825197 + 4c7a04e commit bd67b1d

File tree

14 files changed

+211
-55
lines changed

14 files changed

+211
-55
lines changed

cookies.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/main/java/com/back/domain/myhistory/dto/MyHistoryPostItemDto.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,49 @@
22

33
import com.back.domain.post.post.entity.Post;
44
import com.back.domain.post.post.entity.PostImage;
5-
import java.util.List;
5+
import com.back.domain.post.post.enums.PostStatus;
66
import lombok.Builder;
77
import lombok.Getter;
88

99
import java.time.LocalDateTime;
10+
import java.util.List;
1011

1112
@Getter
1213
@Builder
1314
public class MyHistoryPostItemDto {
1415
private Long id;
16+
private Long postId;
17+
private String categoryName;
18+
private String userNickName;
1519
private String title;
20+
private String content;
1621
private List<String> imageUrls;
1722
private LocalDateTime createdAt;
23+
private LocalDateTime updatedAt;
24+
private PostStatus status;
1825
private Integer likeCount;
1926
private Integer commentCount;
27+
private Integer viewCount;
2028

2129
public static MyHistoryPostItemDto from(Post p) {
30+
String categoryName = p.getCategory() != null ? p.getCategory().getName() : null;
31+
String userNickName = p.getUser() != null ? p.getUser().getNickname() : null;
2232
return MyHistoryPostItemDto.builder()
2333
.id(p.getId())
34+
.postId(p.getId())
35+
.categoryName(categoryName)
36+
.userNickName(userNickName)
2437
.title(p.getTitle())
38+
.content(p.getContent())
2539
.imageUrls(p.getImages().stream()
26-
.map(PostImage::getUrl)
27-
.toList())
40+
.map(PostImage::getUrl)
41+
.toList())
2842
.createdAt(p.getCreatedAt())
43+
.updatedAt(p.getUpdatedAt())
44+
.status(p.getStatus())
2945
.likeCount(p.getLikeCount())
3046
.commentCount(p.getCommentCount())
47+
.viewCount(p.getViewCount())
3148
.build();
3249
}
3350
}
34-

src/main/java/com/back/domain/post/comment/repository/CommentRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import com.back.domain.post.comment.entity.Comment;
44
import com.back.domain.post.comment.enums.CommentStatus;
5-
import java.util.List;
65
import org.springframework.data.jpa.repository.JpaRepository;
76
import org.springframework.stereotype.Repository;
87

8+
import java.util.List;
9+
910
@Repository
1011
public interface CommentRepository extends JpaRepository<Comment, Long> {
1112

src/main/java/com/back/domain/post/post/controller/PostController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.back.domain.post.post.dto.request.PostCreateRequestDto;
44
import com.back.domain.post.post.dto.request.PostSortScrollRequestDto;
55
import com.back.domain.post.post.dto.request.PostUpdateRequestDto;
6+
import com.back.domain.post.post.dto.response.PostLikeResponseDto;
67
import com.back.domain.post.post.dto.response.PostResponseDto;
78
import com.back.domain.post.post.service.PostService;
89
import com.back.global.rsData.RsData;
@@ -114,10 +115,9 @@ public RsData<Void> deletePost(
114115
*/
115116
@PostMapping("/{postId}/like")
116117
@Operation(summary = "게시글 추천")
117-
public RsData<Void> toggleLike(
118+
public RsData<PostLikeResponseDto> toggleLike(
118119
@PathVariable Long postId
119120
) {
120-
postService.toggleLike(postId);
121-
return RsData.successOf(null); // code=200, message="success"
121+
return RsData.successOf(postService.toggleLike(postId)); // code=200, message="success"
122122
}
123123
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.back.domain.post.post.dto.response;
2+
3+
import com.back.domain.post.post.entity.PostLike;
4+
import com.back.domain.post.post.enums.PostLikeStatus;
5+
6+
public record PostLikeResponseDto(
7+
PostLikeStatus status
8+
) {
9+
10+
public PostLikeResponseDto(PostLike postLike) {
11+
this(
12+
postLike.getStatus()
13+
);
14+
}
15+
}

src/main/java/com/back/domain/post/post/service/PostService.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.back.domain.post.post.dto.request.PostCreateRequestDto;
88
import com.back.domain.post.post.dto.request.PostSortScrollRequestDto;
99
import com.back.domain.post.post.dto.request.PostUpdateRequestDto;
10+
import com.back.domain.post.post.dto.response.PostLikeResponseDto;
1011
import com.back.domain.post.post.dto.response.PostResponseDto;
1112
import com.back.domain.post.post.entity.Post;
1213
import com.back.domain.post.post.entity.PostImage;
@@ -233,7 +234,7 @@ public void deletePost(Long postId) {
233234

234235
// 게시글 추천(좋아요) 토글 로직
235236
@Transactional
236-
public void toggleLike(Long postId) {
237+
public PostLikeResponseDto toggleLike(Long postId) {
237238
User user = rq.getActor(); // 현재 로그인한 사용자
238239

239240
Post post = postRepository.findById(postId)
@@ -248,6 +249,8 @@ public void toggleLike(Long postId) {
248249
post.decreaseLikeCount();
249250
// 활동 점수: 추천 취소 시 -0.1
250251
abvScoreService.revokeForLike(user.getId());
252+
253+
return new PostLikeResponseDto(existingLike.get().getStatus());
251254
} else {
252255
// 추천 추가
253256
PostLike postLike = PostLike.builder()
@@ -259,15 +262,17 @@ public void toggleLike(Long postId) {
259262
post.increaseLikeCount();
260263
// 활동 점수: 추천 추가 시 +0.1
261264
abvScoreService.awardForLike(user.getId());
262-
}
263265

264-
// 게시글 작성자에게 알림 전송
265-
notificationService.sendNotification(
266-
post.getUser(),
267-
post,
268-
NotificationType.LIKE,
269-
user.getNickname() + " 님이 추천을 남겼습니다."
270-
);
266+
// 게시글 작성자에게 알림 전송
267+
notificationService.sendNotification(
268+
post.getUser(),
269+
post,
270+
NotificationType.LIKE,
271+
user.getNickname() + " 님이 추천을 남겼습니다."
272+
);
273+
274+
return new PostLikeResponseDto(postLike.getStatus());
275+
}
271276
}
272277

273278
// 태그 추가 메서드

src/main/java/com/back/domain/user/controller/UserAuthController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.back.domain.user.controller;
22

33
import com.back.domain.user.dto.RefreshTokenResDto;
4+
import com.back.domain.user.dto.UserMeResDto;
45
import com.back.domain.user.service.UserAuthService;
56
import com.back.global.rsData.RsData;
67
import io.swagger.v3.oas.annotations.Operation;
@@ -11,6 +12,7 @@
1112
import jakarta.servlet.http.HttpServletResponse;
1213
import lombok.RequiredArgsConstructor;
1314
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.web.bind.annotation.GetMapping;
1416
import org.springframework.web.bind.annotation.PostMapping;
1517
import org.springframework.web.bind.annotation.RequestMapping;
1618
import org.springframework.web.bind.annotation.RestController;
@@ -50,4 +52,16 @@ public RsData<Void> logout(HttpServletRequest request, HttpServletResponse respo
5052
userAuthService.logout(request, response);
5153
return RsData.of(200, "로그아웃되었습니다.");
5254
}
55+
56+
@Operation(summary = "현재 로그인한 유저 정보 조회", description = "세션 유효성 검증 및 사용자 정보 반환")
57+
@ApiResponses(value = {
58+
@ApiResponse(responseCode = "200", description = "인증된 유저 정보 반환 성공"),
59+
@ApiResponse(responseCode = "401", description = "인증되지 않은 사용자"),
60+
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
61+
})
62+
@GetMapping("/me")
63+
public RsData<UserMeResDto> getCurrentUser() {
64+
UserMeResDto userInfo = userAuthService.getCurrentUser();
65+
return RsData.of(200, "인증된 유저 정보 반환 성공", userInfo);
66+
}
5367
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.back.domain.user.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Builder
9+
public class UserMeResDto {
10+
@JsonProperty("user")
11+
private final UserInfo user;
12+
13+
@Getter
14+
@Builder
15+
public static class UserInfo {
16+
private final String id;
17+
private final String email;
18+
private final String nickname;
19+
20+
@JsonProperty("is_first_login")
21+
private final Boolean isFirstLogin;
22+
23+
@JsonProperty("abv_degree")
24+
private final Double abvDegree;
25+
26+
private final String provider;
27+
}
28+
}

src/main/java/com/back/domain/user/service/UserAuthService.java

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.back.domain.user.service;
22

33
import com.back.domain.user.dto.RefreshTokenResDto;
4+
import com.back.domain.user.dto.UserMeResDto;
45
import com.back.domain.user.entity.User;
56
import com.back.domain.user.repository.UserRepository;
67
import com.back.global.exception.ServiceException;
78
import com.back.global.jwt.JwtUtil;
89
import com.back.global.jwt.refreshToken.entity.RefreshToken;
910
import com.back.global.jwt.refreshToken.repository.RefreshTokenRepository;
1011
import com.back.global.jwt.refreshToken.service.RefreshTokenService;
12+
import com.back.global.rq.Rq;
1113
import com.back.global.rsData.RsData;
1214
import jakarta.servlet.http.HttpServletRequest;
1315
import jakarta.servlet.http.HttpServletResponse;
@@ -20,6 +22,8 @@
2022
import java.util.Optional;
2123
import java.util.Set;
2224

25+
import static org.springframework.security.core.context.SecurityContextHolder.*;
26+
2327
@Slf4j
2428
@Service
2529
@RequiredArgsConstructor
@@ -76,6 +80,7 @@ public class UserAuthService {
7680
private final UserRepository userRepository;
7781
private final RefreshTokenService refreshTokenService;
7882
private final RefreshTokenRepository refreshTokenRepository;
83+
private final Rq rq;
7984

8085
//OAuth 관련
8186

@@ -196,19 +201,81 @@ public RefreshTokenResDto refreshTokens(HttpServletRequest request, HttpServletR
196201

197202
//토큰 끊기면서 OAuth 자동 로그아웃
198203
public void logout(HttpServletRequest request, HttpServletResponse response) {
204+
// 1. RefreshToken DB에서 삭제
199205
String refreshToken = jwtUtil.getRefreshTokenFromCookie(request);
200-
201206
if (refreshToken != null) {
202207
refreshTokenService.revokeToken(refreshToken);
203208
}
204209

210+
// 2. JWT 쿠키 삭제
205211
jwtUtil.removeAccessTokenCookie(response);
206212
jwtUtil.removeRefreshTokenCookie(response);
213+
214+
// 3. Spring Security 세션 무효화 (Redis 포함)
215+
try {
216+
if (request.getSession(false) != null) {
217+
request.getSession().invalidate();
218+
log.debug("세션 무효화");
219+
}
220+
} catch (IllegalStateException e) {
221+
log.debug("세션이 이미 무효화되어 있음");
222+
}
223+
224+
// 4. SecurityContext 클리어
225+
clearContext();
226+
227+
log.info("로그아웃 완료 - JWT, 세션, SecurityContext 모두 정리됨");
207228
}
208229

209230
@Transactional
210231
public void setFirstLoginFalse(Long id) {
211232
Optional<User> userOpt = userRepository.findById(id);
212233
userOpt.ifPresent(user -> user.setFirstLogin(false));
213234
}
235+
236+
// 현재 로그인한 사용자 정보 조회 (세션 검증용)
237+
public UserMeResDto getCurrentUser() {
238+
try {
239+
User actor = rq.getActor();
240+
241+
if (actor == null) {
242+
log.debug("인증되지 않은 사용자");
243+
throw new ServiceException(401, "인증되지 않은 사용자");
244+
}
245+
246+
Optional<User> userOpt = userRepository.findById(actor.getId());
247+
if (userOpt.isEmpty()) {
248+
log.warn("사용자 ID {}를 DB에서 찾을 수 없음 (토큰은 유효하나 사용자 삭제됨)", actor.getId());
249+
throw new ServiceException(401, "인증되지 않은 사용자");
250+
}
251+
252+
User user = userOpt.get();
253+
String provider = extractProvider(user.getOauthId());
254+
255+
return UserMeResDto.builder()
256+
.user(UserMeResDto.UserInfo.builder()
257+
.id(user.getId().toString())
258+
.email(user.getEmail())
259+
.nickname(user.getNickname())
260+
.isFirstLogin(user.isFirstLogin())
261+
.abvDegree(user.getAbvDegree())
262+
.provider(provider)
263+
.build())
264+
.build();
265+
266+
} catch (ServiceException e) {
267+
throw e;
268+
} catch (Exception e) {
269+
log.error("사용자 정보 조회 중 서버 오류 발생: {}", e.getMessage(), e);
270+
throw new ServiceException(500, "서버 내부 오류");
271+
}
272+
}
273+
274+
private String extractProvider(String oauthId) {
275+
if (oauthId == null || oauthId.isBlank()) {
276+
return "unknown";
277+
}
278+
String[] parts = oauthId.split("_", 2);
279+
return parts.length > 0 ? parts[0] : "unknown";
280+
}
214281
}

src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import jakarta.persistence.*;
44
import lombok.*;
5+
import org.springframework.data.annotation.CreatedDate;
6+
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
57

68
import java.time.LocalDateTime;
79

810
@Entity
11+
@EntityListeners(AuditingEntityListener.class)
912
@Table(name = "refresh_tokens")
1013
@Getter
1114
@Setter
@@ -20,24 +23,33 @@ public class RefreshToken {
2023
@Column(nullable = false)
2124
private Long userId;
2225

23-
@Column(nullable = false)
26+
@CreatedDate
27+
@Column(nullable = false, updatable = false)
2428
private LocalDateTime createdAt;
2529

2630
@Column(nullable = false)
2731
private LocalDateTime expiresAt;
2832

33+
@Column(nullable = false)
34+
private LocalDateTime lastUsedAt;
2935

3036
public static RefreshToken create(String token, Long userId, long ttlSeconds) {
3137
LocalDateTime now = LocalDateTime.now();
3238
return RefreshToken.builder()
3339
.token(token)
3440
.userId(userId)
35-
.createdAt(now)
41+
.lastUsedAt(now)
3642
.expiresAt(now.plusSeconds(ttlSeconds))
3743
.build();
3844
}
3945

4046
public boolean isExpired() {
4147
return LocalDateTime.now().isAfter(this.expiresAt);
4248
}
49+
50+
public boolean isIdleExpired(long idleTimeoutHours) {
51+
return LocalDateTime.now().isAfter(this.lastUsedAt.plusMinutes(idleTimeoutHours));
52+
53+
}
54+
4355
}

0 commit comments

Comments
 (0)