Skip to content

Commit a3f246a

Browse files
authored
Merge pull request #60 from team-fontory/feature/add-logging-and-documentation
Feature/add logging and documentation
2 parents d3aef60 + 213fce5 commit a3f246a

33 files changed

+894
-341
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,9 @@ out/
4141

4242
### yml ###
4343
application-dev.properties
44-
application-infrastructure.properties
44+
application-infrastructure.properties
45+
46+
### Claude Code ###
47+
CLAUDE.md
48+
.claude/
49+
.clinerules

src/main/java/org/fontory/fontorybe/authentication/adapter/inbound/security/JwtAuthenticationFilter.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
import lombok.RequiredArgsConstructor;
2727
import lombok.extern.slf4j.Slf4j;
2828

29+
/**
30+
* JWT 기반 인증을 처리하는 Spring Security 필터
31+
* 모든 HTTP 요청에 대해 JWT 토큰을 검증하고 인증 처리
32+
* Access Token 만료 시 Refresh Token을 사용한 자동 갱신 기능 포함
33+
*/
2934
@Slf4j
3035
@RequiredArgsConstructor
3136
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@@ -34,13 +39,22 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
3439
private final AuthService authService;
3540
private final CookieUtils cookieUtils;
3641

42+
/**
43+
* JWT 토큰 검증 및 인증 처리
44+
* 쿠키 또는 Authorization 헤더에서 토큰을 추출하고 검증
45+
*
46+
* @param req HTTP 요청
47+
* @param res HTTP 응답
48+
* @param chain 필터 체인
49+
*/
3750
@Override
3851
protected void doFilterInternal(HttpServletRequest req,
3952
@NonNull HttpServletResponse res,
4053
@NonNull FilterChain chain)
4154
throws ServletException, IOException {
4255
log.info("JwtAuthenticationFilter: {} {}", req.getMethod(), req.getRequestURI());
4356

57+
// 쿠키 또는 Authorization 헤더에서 토큰 추출
4458
String accessToken = cookieUtils
4559
.extractTokenFromCookieInRequest(req, ACCESS_TOKEN_COOKIE_NAME)
4660
.or(() -> Optional.ofNullable(req.getHeader("Authorization"))
@@ -51,6 +65,7 @@ protected void doFilterInternal(HttpServletRequest req,
5165
.orElse(null);
5266
log.info("Access token: {}, Refresh token: {}", accessToken, refreshToken);
5367

68+
// 토큰 존재 여부에 따른 처리
5469
if (accessToken != null) {
5570
log.debug("Access token found - attempting authentication");
5671
authenticateOrRefresh(accessToken, refreshToken, res);
@@ -67,6 +82,14 @@ protected void doFilterInternal(HttpServletRequest req,
6782
log.debug("JwtAuthenticationFilter completed");
6883
}
6984

85+
/**
86+
* Access Token 검증 및 만료 시 Refresh Token으로 갱신
87+
*
88+
* @param accessToken 검증할 Access Token
89+
* @param refreshToken 갱신에 사용할 Refresh Token
90+
* @param res HTTP 응답 (새 쿠키 설정용)
91+
* @throws JwtAuthenticationException JWT 인증 실패 시
92+
*/
7093
private void authenticateOrRefresh(String accessToken,
7194
String refreshToken,
7295
HttpServletResponse res) throws JwtAuthenticationException {
@@ -76,6 +99,7 @@ private void authenticateOrRefresh(String accessToken,
7699
SecurityContextHolder.getContext().setAuthentication(auth);
77100
log.info("Authentication successful: user={}, authorities={}", auth.getName(), auth.getAuthorities());
78101
} catch (ExpiredJwtException e) {
102+
// Access Token 만료 시 Refresh Token으로 갱신 시도
79103
log.warn("Access token expired: {}", e.getMessage());
80104
if (refreshToken == null) {
81105
throw new JwtAuthenticationException("Refresh token missing");

src/main/java/org/fontory/fontorybe/authentication/adapter/outbound/JwtTokenProviderImpl.java

Lines changed: 32 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,10 @@ public class JwtTokenProviderImpl implements JwtTokenProvider {
3131
private final JwtParser provideJwtParser;
3232
private final JwtParser fontCreateJwtParser;
3333

34-
private SecretKey getSigningKey(String key) {
35-
byte[] keyBytes = Decoders.BASE64.decode(key);
36-
return Keys.hmacShaKeyFor(keyBytes);
37-
}
38-
39-
public JwtTokenProviderImpl(
40-
JwtProperties props) {
34+
public JwtTokenProviderImpl(JwtProperties props) {
4135
log.info("Initializing JWT token provider");
4236

37+
this.props = props;
4338
this.accessSecretKey = getSigningKey(props.getAccessSecretKey());
4439
this.refreshSecretKey = getSigningKey(props.getRefreshSecretKey());
4540
this.provideSecretKey = getSigningKey(props.getProvideSecretKey());
@@ -49,25 +44,20 @@ public JwtTokenProviderImpl(
4944
this.refreshJwtParser = Jwts.parserBuilder().setSigningKey(refreshSecretKey).build();
5045
this.provideJwtParser = Jwts.parserBuilder().setSigningKey(provideSecretKey).build();
5146
this.fontCreateJwtParser = Jwts.parserBuilder().setSigningKey(fontCreateSecretKey).build();
52-
this.props = props;
5347

5448
log.debug("JWT token provider initialized with token validities - access: {}ms, refresh: {}ms",
5549
props.getAccessTokenValidityMs(), props.getRefreshTokenValidityMs());
5650
}
51+
52+
private SecretKey getSigningKey(String key) {
53+
byte[] keyBytes = Decoders.BASE64.decode(key);
54+
return Keys.hmacShaKeyFor(keyBytes);
55+
}
5756

5857
public String generateTemporalProvideToken(String id) {
5958
log.debug("Generating temporal provide token for id: {}", id);
60-
61-
Date now = new Date();
62-
Date expiryDate = new Date(now.getTime() + props.getTempTokenValidityMs());
63-
String token = Jwts.builder()
64-
.setSubject(String.valueOf(id))
65-
.setIssuedAt(now)
66-
.setExpiration(expiryDate)
67-
.signWith(this.provideSecretKey)
68-
.compact();
69-
70-
log.info("Temporal provide token generated: id={}, expiresAt={}", id, expiryDate);
59+
String token = generateToken(id, props.getTempTokenValidityMs(), provideSecretKey);
60+
log.info("Temporal provide token generated for id: {}", id);
7161
return token;
7262
}
7363

@@ -85,56 +75,28 @@ public Long getProvideId(String token) {
8575

8676
public String generateAccessToken(UserPrincipal user) {
8777
log.debug("Generating access token for user: {}", user.getId());
88-
89-
Date now = new Date();
90-
Date expiryDate = new Date(now.getTime() + props.getAccessTokenValidityMs());
91-
String token = Jwts.builder()
92-
.setSubject(String.valueOf(user.getId()))
93-
.setIssuedAt(now)
94-
.setExpiration(expiryDate)
95-
.signWith(this.accessSecretKey)
96-
.compact();
97-
98-
log.info("Access token generated: userId={}, expiresAt={}", user.getId(), expiryDate);
78+
String token = generateToken(String.valueOf(user.getId()), props.getAccessTokenValidityMs(), accessSecretKey);
79+
log.info("Access token generated for user: {}", user.getId());
9980
return token;
10081
}
10182

10283
public String generateRefreshToken(UserPrincipal user) {
10384
log.debug("Generating refresh token for user: {}", user.getId());
104-
105-
Date now = new Date();
106-
Date expiryDate = new Date(now.getTime() + props.getRefreshTokenValidityMs());
107-
String token = Jwts.builder()
108-
.setSubject(String.valueOf(user.getId()))
109-
.setIssuedAt(now)
110-
.setExpiration(expiryDate)
111-
.signWith(refreshSecretKey)
112-
.compact();
113-
114-
log.info("Refresh token generated: userId={}, expiresAt={}", user.getId(), expiryDate);
85+
String token = generateToken(String.valueOf(user.getId()), props.getRefreshTokenValidityMs(), refreshSecretKey);
86+
log.info("Refresh token generated for user: {}", user.getId());
11587
return token;
11688
}
11789

11890
public Long getMemberIdFromAccessToken(String token) {
11991
log.debug("Extracting member ID from access token");
120-
121-
Claims claims = accessJwtParser
122-
.parseClaimsJws(token)
123-
.getBody();
124-
Long memberId = Long.valueOf(claims.getSubject());
125-
92+
Long memberId = extractMemberIdFromToken(token, accessJwtParser);
12693
log.debug("Member ID extracted from access token: {}", memberId);
12794
return memberId;
12895
}
12996

13097
public Long getMemberIdFromRefreshToken(String token) {
13198
log.debug("Extracting member ID from refresh token");
132-
133-
Claims claims = refreshJwtParser
134-
.parseClaimsJws(token)
135-
.getBody();
136-
Long memberId = Long.valueOf(claims.getSubject());
137-
99+
Long memberId = extractMemberIdFromToken(token, refreshJwtParser);
138100
log.debug("Member ID extracted from refresh token: {}", memberId);
139101
return memberId;
140102
}
@@ -156,13 +118,25 @@ public Authentication getAuthenticationFromAccessToken(String token) {
156118

157119
public String getFontCreateServer(String token) {
158120
log.debug("Validating font create server token");
159-
160-
Claims claims = fontCreateJwtParser
161-
.parseClaimsJws(token)
162-
.getBody();
121+
Claims claims = fontCreateJwtParser.parseClaimsJws(token).getBody();
163122
String subject = claims.getSubject();
164-
165123
log.debug("Font create server token validated: subject={}", subject);
166124
return subject;
167125
}
126+
127+
private String generateToken(String subject, long validityMs, SecretKey key) {
128+
Date now = new Date();
129+
Date expiryDate = new Date(now.getTime() + validityMs);
130+
return Jwts.builder()
131+
.setSubject(subject)
132+
.setIssuedAt(now)
133+
.setExpiration(expiryDate)
134+
.signWith(key)
135+
.compact();
136+
}
137+
138+
private Long extractMemberIdFromToken(String token, JwtParser parser) {
139+
Claims claims = parser.parseClaimsJws(token).getBody();
140+
return Long.valueOf(claims.getSubject());
141+
}
168142
}

src/main/java/org/fontory/fontorybe/authentication/application/AuthService.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ public class AuthService {
2727
private final JwtTokenProvider jwtTokenProvider;
2828

2929
/**
30-
* 새롭게 토큰 발급
31-
* 기존에 토큰이 존재한다면 제거, 기존 토큰이 존재할 필요 X
30+
* 새로운 Access/Refresh 토큰 쌍을 발급
31+
* Refresh Token은 Redis에 저장하여 검증에 사용
32+
*
33+
* @param member 토큰을 발급할 회원 정보
34+
* @return 발급된 토큰 쌍
3235
*/
3336
private TokenResponse issueNewTokens(Member member) {
3437
log.info("Issuing new token pair for member: memberId={}, provideId={}",

src/main/java/org/fontory/fontorybe/bookmark/controller/BookmarkController.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,21 @@
2525
import lombok.extern.slf4j.Slf4j;
2626

2727
@Slf4j
28-
@Tag(name = "북마크 관리", description = "북마크 API")
28+
@Tag(name = "북마크 관리", description = "폰트 북마크 추가, 삭제, 조회 API")
2929
@RestController
3030
@RequestMapping("/bookmarks")
3131
@RequiredArgsConstructor
3232
public class BookmarkController {
3333
private final BookmarkService bookmarkService;
3434

35-
@Operation(summary = "북마크 추가")
35+
@Operation(
36+
summary = "북마크 추가",
37+
description = "특정 폰트를 북마크에 추가합니다."
38+
)
3639
@PostMapping("/{fontId}")
37-
public ResponseEntity<?> addBookmark(@Login UserPrincipal userPrincipal, @PathVariable Long fontId) {
40+
public ResponseEntity<?> addBookmark(
41+
@Login UserPrincipal userPrincipal,
42+
@PathVariable @Parameter(description = "북마크할 폰트 ID") Long fontId) {
3843
Long memberId = userPrincipal.getId();
3944
log.info("Request received: Add bookmark for font ID: {} by member ID: {}", fontId, memberId);
4045

@@ -47,9 +52,14 @@ public ResponseEntity<?> addBookmark(@Login UserPrincipal userPrincipal, @PathVa
4752
.body(BookmarkCreateResponse.from(createdBookmark));
4853
}
4954

50-
@Operation(summary = "북마크 삭제")
55+
@Operation(
56+
summary = "북마크 삭제",
57+
description = "특정 폰트를 북마크에서 제거합니다."
58+
)
5159
@DeleteMapping("/{fontId}")
52-
public ResponseEntity<?> deleteBookmark(@Login UserPrincipal userPrincipal, @PathVariable Long fontId) {
60+
public ResponseEntity<?> deleteBookmark(
61+
@Login UserPrincipal userPrincipal,
62+
@PathVariable @Parameter(description = "북마크 삭제할 폰트 ID") Long fontId) {
5363
Long memberId = userPrincipal.getId();
5464
log.info("Request received: Delete bookmark for font ID: {} by member ID: {}", fontId, memberId);
5565

@@ -62,7 +72,10 @@ public ResponseEntity<?> deleteBookmark(@Login UserPrincipal userPrincipal, @Pat
6272
.body(deletedBookmark);
6373
}
6474

65-
@Operation(summary = "북마크한 폰트 보기")
75+
@Operation(
76+
summary = "북마크한 폰트 보기",
77+
description = "로그인한 사용자가 북마크한 폰트 목록을 조회합니다. 페이지네이션 및 검색을 지원합니다."
78+
)
6679
@GetMapping
6780
public ResponseEntity<?> getBookmarks(
6881
@Parameter(description = "페이지 시작 오프셋 (기본값: 0)", example = "0") @RequestParam(defaultValue = "0") int page,
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
package org.fontory.fontorybe.bookmark.infrastructure;
22

3+
import java.util.List;
34
import java.util.Optional;
45
import org.fontory.fontorybe.bookmark.infrastructure.entity.BookmarkEntity;
56
import org.springframework.data.domain.Page;
67
import org.springframework.data.domain.Pageable;
78
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
811

912
public interface BookmarkJpaRepository extends JpaRepository<BookmarkEntity, Long> {
1013
boolean existsByMemberIdAndFontId(Long memberId, Long fontId);
1114
Optional<BookmarkEntity> findByMemberIdAndFontId(Long memberId, Long fontId);
1215
Page<BookmarkEntity> findAllByMemberId(Long memberId, Pageable pageable);
13-
16+
17+
// 성능 최적화를 위한 추가 쿼리
18+
@Query(value = "SELECT COUNT(*) FROM bookmark WHERE member_id = :memberId", nativeQuery = true)
19+
long countByMemberId(@Param("memberId") Long memberId);
20+
21+
@Query(value = "SELECT font_id FROM bookmark WHERE member_id = :memberId ORDER BY created_at DESC LIMIT :limit", nativeQuery = true)
22+
List<Long> findRecentBookmarkedFontIds(@Param("memberId") Long memberId, @Param("limit") int limit);
23+
24+
@Query("SELECT b FROM BookmarkEntity b WHERE b.memberId = :memberId ORDER BY b.createdAt DESC")
25+
List<BookmarkEntity> findAllByMemberIdOrderByCreatedAtDesc(@Param("memberId") Long memberId, Pageable pageable);
1426
}

0 commit comments

Comments
 (0)