Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Spring Security
// implementation 'org.springframework.boot:spring-boot-starter-security'
// testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
Comment on lines +36 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest JJWT version on Maven Central
curl -s "https://search.maven.org/solrsearch/select?q=g:io.jsonwebtoken+AND+a:jjwt-api&rows=1&wt=json" | jq '.response.docs[0].latestVersion'

Repository: TripTalk/BE_SpringBoot

Length of output: 74


JJWT 의존성을 최신 버전으로 업데이트하세요.

현재 0.12.3 버전이 사용 중이나, Maven Central의 최신 안정 버전은 0.12.6입니다. 보안 패치 및 버그 수정을 포함한 최신 버전으로 업데이트하는 것을 권장합니다.

🤖 Prompt for AI Agents
In build.gradle around lines 36 to 38, the JJWT dependencies are pinned to
0.12.3; update all three coordinates (io.jsonwebtoken:jjwt-api, jjwt-impl,
jjwt-jackson) to the latest stable 0.12.6 to pick up security and bug fixes,
then refresh/resolve Gradle dependencies (e.g., gradle --refresh-dependencies or
IDE sync) and run tests to verify no regressions.

implementation 'org.springframework.boot:spring-boot-configuration-processor'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

spring-boot-configuration-processorannotationProcessor로 선언해야 합니다.

spring-boot-configuration-processor는 컴파일 시점에 @ConfigurationProperties 클래스에 대한 메타데이터를 생성하는 어노테이션 프로세서입니다. implementation 대신 annotationProcessor로 선언해야 올바르게 동작합니다.

-	implementation 'org.springframework.boot:spring-boot-configuration-processor'
+	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
implementation 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
🤖 Prompt for AI Agents
In build.gradle around line 39, the dependency declaration uses implementation
'org.springframework.boot:spring-boot-configuration-processor' but this library
is an annotation processor and must be declared as annotationProcessor; replace
the implementation configuration with annotationProcessor
'org.springframework.boot:spring-boot-configuration-processor' (optionally also
add a compileOnly entry if IDEs need the classes at compile time).


// Spring Web
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/example/triptalk/TriptalkApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class TriptalkApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.triptalk.domain.user.controller;

import com.example.triptalk.domain.user.dto.AuthRequest;
import com.example.triptalk.domain.user.dto.AuthResponse;
import com.example.triptalk.domain.user.service.AuthService;
import com.example.triptalk.global.apiPayload.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Auth", description = "인증 관련 API")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@Operation(summary = "회원가입", description = "이메일, 비밀번호, 닉네임으로 회원가입합니다.")
@PostMapping("/signup")
public ApiResponse<AuthResponse.SignUpDTO> signUp(@RequestBody AuthRequest.SignUpDTO request) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

요청 DTO 유효성 검증을 추가하세요.

요청 본문에 대한 입력 검증이 없습니다. @Valid 어노테이션을 추가하여 DTO 필드의 유효성을 검증해야 합니다.

다음 diff를 적용하세요:

-    public ApiResponse<AuthResponse.SignUpDTO> signUp(@RequestBody AuthRequest.SignUpDTO request) {
+    public ApiResponse<AuthResponse.SignUpDTO> signUp(@Valid @RequestBody AuthRequest.SignUpDTO request) {

또한 AuthRequest.SignUpDTO 클래스에 적절한 검증 어노테이션(예: @NotBlank, @Email, @Size 등)을 추가해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ApiResponse<AuthResponse.SignUpDTO> signUp(@RequestBody AuthRequest.SignUpDTO request) {
public ApiResponse<AuthResponse.SignUpDTO> signUp(@Valid @RequestBody AuthRequest.SignUpDTO request) {
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/controller/AuthController.java
around line 24, the signUp endpoint is missing request body validation; add the
@Valid annotation to the method parameter (e.g., public
ApiResponse<AuthResponse.SignUpDTO> signUp(@Valid @RequestBody
AuthRequest.SignUpDTO request)) so Spring triggers validation, and then update
AuthRequest.SignUpDTO to include appropriate Jakarta/javax validation
annotations on its fields (e.g., @NotBlank, @Email, @Size, @NotNull as
applicable) to enforce input constraints and enable automatic 400 responses on
invalid input.

return ApiResponse.onSuccess(authService.signUp(request));
}

@Operation(summary = "로그인", description = "이메일과 비밀번호로 로그인하여 토큰을 발급받습니다.")
@PostMapping("/login")
public ApiResponse<AuthResponse.TokenDTO> login(@RequestBody AuthRequest.LoginDTO request) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

요청 DTO 유효성 검증을 추가하세요.

로그인 요청에 대한 입력 검증이 없습니다.

다음 diff를 적용하세요:

-    public ApiResponse<AuthResponse.TokenDTO> login(@RequestBody AuthRequest.LoginDTO request) {
+    public ApiResponse<AuthResponse.TokenDTO> login(@Valid @RequestBody AuthRequest.LoginDTO request) {
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/controller/AuthController.java
around line 30, the login endpoint accepts the DTO without any validation; add
request validation by annotating the controller parameter with @Valid (and
import javax/jakarta.validation.Valid) and ensure the AuthRequest.LoginDTO
fields have proper javax/jakarta.validation annotations (e.g., @NotBlank,
@Email, @Size) in its class; either add a BindingResult parameter to return a
formatted ApiResponse on validation failure or rely on an existing global
MethodArgumentNotValidException handler to convert validation errors into
ApiResponse; also add @Validated at the controller class level if needed.

return ApiResponse.onSuccess(authService.login(request));
}

@Operation(summary = "토큰 재발급", description = "Refresh Token으로 Access Token을 재발급받습니다.")
@PostMapping("/refresh")
public ApiResponse<AuthResponse.TokenDTO> reissue(@RequestBody AuthRequest.ReissueDTO request) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

요청 DTO 유효성 검증을 추가하세요.

토큰 재발급 요청에 대한 입력 검증이 없습니다.

다음 diff를 적용하세요:

-    public ApiResponse<AuthResponse.TokenDTO> reissue(@RequestBody AuthRequest.ReissueDTO request) {
+    public ApiResponse<AuthResponse.TokenDTO> reissue(@Valid @RequestBody AuthRequest.ReissueDTO request) {
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/controller/AuthController.java
around line 36, the reissue endpoint accepts the ReissueDTO without validation;
add request validation by annotating the parameter with @Valid (i.e., public
ApiResponse<AuthResponse.TokenDTO> reissue(@Valid @RequestBody
AuthRequest.ReissueDTO request)), import javax.validation.Valid, and annotate
the controller class with @Validated if not present. Update
AuthRequest.ReissueDTO to declare explicit validation constraints on its fields
(e.g., @NotBlank for token strings, @NotNull where appropriate) and import the
corresponding javax.validation annotations. Rely on Spring’s
MethodArgumentNotValidException to return 400 or add an exception handler if you
want custom error responses.

return ApiResponse.onSuccess(authService.reissue(request));
}

@Operation(summary = "로그아웃", description = "로그아웃하여 Refresh Token을 삭제합니다.")
@PostMapping("/logout")
public ApiResponse<Void> logout(@AuthenticationPrincipal UserDetails userDetails) {
authService.logout(userDetails.getUsername());
return ApiResponse.onSuccess(null);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.triptalk.domain.user.converter;

import com.example.triptalk.domain.user.dto.AuthRequest;
import com.example.triptalk.domain.user.dto.AuthResponse;
import com.example.triptalk.domain.user.entity.User;
import org.springframework.stereotype.Component;

@Component
public class AuthConverter {

public User toUser(AuthRequest.SignUpDTO request, String encodedPassword) {
return User.builder()
.email(request.getEmail())
.password(encodedPassword)
.nickName(request.getNickName())
.completedTravelCount(0)
.plannedTravelCount(0)
.build();
}

public AuthResponse.SignUpDTO toSignUpResponse(User user) {
return AuthResponse.SignUpDTO.builder()
.userId(user.getId())
.email(user.getEmail())
.nickName(user.getNickName())
.build();
}

public AuthResponse.TokenDTO toTokenResponse(String accessToken, String refreshToken) {
return AuthResponse.TokenDTO.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.triptalk.domain.user.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class AuthRequest {

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "회원가입 요청")
public static class SignUpDTO {
@Schema(description = "이메일", example = "[email protected]")
private String email;

@Schema(description = "비밀번호", example = "password123")
private String password;

@Schema(description = "닉네임", example = "여행자")
private String nickName;
}
Comment on lines +14 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

입력 검증 어노테이션이 누락되었습니다.

회원가입 요청 DTO에 필수 필드에 대한 검증 어노테이션이 없어 빈 값이나 잘못된 형식의 데이터가 전달될 수 있습니다. 이는 보안 및 데이터 무결성에 영향을 줄 수 있습니다.

다음 diff를 적용하여 검증 로직을 추가하세요:

+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
 public class AuthRequest {

     @Getter
     @NoArgsConstructor
     @AllArgsConstructor
     @Schema(description = "회원가입 요청")
     public static class SignUpDTO {
+        @NotBlank(message = "이메일은 필수입니다")
+        @Email(message = "유효한 이메일 형식이어야 합니다")
         @Schema(description = "이메일", example = "[email protected]")
         private String email;

+        @NotBlank(message = "비밀번호는 필수입니다")
+        @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다")
         @Schema(description = "비밀번호", example = "password123")
         private String password;

+        @NotBlank(message = "닉네임은 필수입니다")
         @Schema(description = "닉네임", example = "여행자")
         private String nickName;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static class SignUpDTO {
@Schema(description = "이메일", example = "[email protected]")
private String email;
@Schema(description = "비밀번호", example = "password123")
private String password;
@Schema(description = "닉네임", example = "여행자")
private String nickName;
}
public static class SignUpDTO {
@NotBlank(message = "이메일은 필수입니다")
@Email(message = "유효한 이메일 형식이어야 합니다")
@Schema(description = "이메일", example = "[email protected]")
private String email;
@NotBlank(message = "비밀번호는 필수입니다")
@Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다")
@Schema(description = "비밀번호", example = "password123")
private String password;
@NotBlank(message = "닉네임은 필수입니다")
@Schema(description = "닉네임", example = "여행자")
private String nickName;
}
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/dto/AuthRequest.java around
lines 14-23, the SignUpDTO lacks validation annotations allowing empty or
malformed input; add javax.validation constraints: annotate email with @NotBlank
and @Email, password with @NotBlank and a suitable @Size(min=8, max=128) (or
your project's password policy), and nickName with @NotBlank and optional
@Size(max=30); add the corresponding imports (javax.validation.constraints.*)
and ensure controllers accept @Valid for this DTO so validation is triggered.


@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "로그인 요청")
public static class LoginDTO {
@Schema(description = "이메일", example = "[email protected]")
private String email;

@Schema(description = "비밀번호", example = "password123")
private String password;
}
Comment on lines +29 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

로그인 DTO에도 입력 검증이 필요합니다.

     @Getter
     @NoArgsConstructor
     @AllArgsConstructor
     @Schema(description = "로그인 요청")
     public static class LoginDTO {
+        @NotBlank(message = "이메일은 필수입니다")
+        @Email(message = "유효한 이메일 형식이어야 합니다")
         @Schema(description = "이메일", example = "[email protected]")
         private String email;

+        @NotBlank(message = "비밀번호는 필수입니다")
         @Schema(description = "비밀번호", example = "password123")
         private String password;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static class LoginDTO {
@Schema(description = "이메일", example = "[email protected]")
private String email;
@Schema(description = "비밀번호", example = "password123")
private String password;
}
public static class LoginDTO {
@NotBlank(message = "이메일은 필수입니다")
@Email(message = "유효한 이메일 형식이어야 합니다")
@Schema(description = "이메일", example = "[email protected]")
private String email;
@NotBlank(message = "비밀번호는 필수입니다")
@Schema(description = "비밀번호", example = "password123")
private String password;
}
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/dto/AuthRequest.java around
lines 29-35, the LoginDTO has no input validation; add validation annotations to
the fields (e.g., annotate email with @NotBlank and @Email, annotate password
with @NotBlank and @Size(min=8) or appropriate length constraints) and import
the constraints from jakarta.validation.constraints (or
javax.validation.constraints depending on project), keep or add Lombok
annotations (or generate getters/setters) so validation can run, and ensure
controllers that consume this DTO use @Valid on the request parameter.


@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "토큰 재발급 요청")
public static class ReissueDTO {
@Schema(description = "리프레시 토큰")
private String refreshToken;
}
Comment on lines +41 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

토큰 재발급 DTO에도 검증이 필요합니다.

     @Getter
     @NoArgsConstructor
     @AllArgsConstructor
     @Schema(description = "토큰 재발급 요청")
     public static class ReissueDTO {
+        @NotBlank(message = "리프레시 토큰은 필수입니다")
         @Schema(description = "리프레시 토큰")
         private String refreshToken;
     }
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/dto/AuthRequest.java around
lines 41-44, the ReissueDTO lacks validation on its refreshToken field; annotate
the field with a javax.validation constraint such as @NotBlank (with an
appropriate message) to enforce non-empty tokens, import the constraint, and
ensure any controller consuming this DTO uses @Valid on the request body so
validation is triggered.

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.triptalk.domain.user.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

public class AuthResponse {

@Getter
@Builder
@AllArgsConstructor
@Schema(description = "토큰 응답")
public static class TokenDTO {
@Schema(description = "액세스 토큰")
private String accessToken;

@Schema(description = "리프레시 토큰")
private String refreshToken;
}

@Getter
@Builder
@AllArgsConstructor
@Schema(description = "회원가입 응답")
public static class SignUpDTO {
@Schema(description = "사용자 ID", example = "1")
private Long userId;

@Schema(description = "이메일", example = "[email protected]")
private String email;

@Schema(description = "닉네임", example = "여행자")
private String nickName;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.triptalk.domain.user.entity;

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class RefreshToken {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String token;

@Column(nullable = false)
private Long userId;

@Column(nullable = false)
private LocalDateTime expiryDate;

public boolean isExpired() {
return LocalDateTime.now().isAfter(expiryDate);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.triptalk.domain.user.repository;

import com.example.triptalk.domain.user.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
Optional<RefreshToken> findByUserId(Long userId);
void deleteByUserId(Long userId);
}
Comment on lines +8 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

삭제 메서드에 필수 어노테이션이 누락되었습니다.

deleteByUserId 메서드는 커스텀 삭제 쿼리이므로 @Modifying@Transactional 어노테이션이 필요합니다. 이 어노테이션들이 없으면 삭제 작업이 제대로 실행되지 않을 수 있습니다.

다음 diff를 적용하세요:

 package com.example.triptalk.domain.user.repository;

 import com.example.triptalk.domain.user.entity.RefreshToken;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.transaction.annotation.Transactional;

 import java.util.Optional;

 public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
     Optional<RefreshToken> findByToken(String token);
     Optional<RefreshToken> findByUserId(Long userId);
+    
+    @Modifying
+    @Transactional
     void deleteByUserId(Long userId);
 }
🤖 Prompt for AI Agents
In
src/main/java/com/example/triptalk/domain/user/repository/RefreshTokenRepository.java
around lines 8 to 12, the deleteByUserId method is a modifying query but is
missing the required @Modifying and @Transactional annotations; update the
repository by annotating the deleteByUserId method with @Modifying and
@Transactional (or mark the repository/class transactional) and add the
corresponding imports (org.springframework.data.jpa.repository.Modifying and
org.springframework.transaction.annotation.Transactional) so the delete
operation executes correctly.


Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public interface UserRepository extends JpaRepository<User, Long> {
// email로 유저 조회
Optional<User> findByEmail(String email);

// email 중복 체크
boolean existsByEmail(String email);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.triptalk.domain.user.service;

import com.example.triptalk.domain.user.dto.AuthRequest;
import com.example.triptalk.domain.user.dto.AuthResponse;

public interface AuthService {

AuthResponse.SignUpDTO signUp(AuthRequest.SignUpDTO request);

AuthResponse.TokenDTO login(AuthRequest.LoginDTO request);

AuthResponse.TokenDTO reissue(AuthRequest.ReissueDTO request);

void logout(String email);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.example.triptalk.domain.user.service;

import com.example.triptalk.domain.user.converter.AuthConverter;
import com.example.triptalk.domain.user.dto.AuthRequest;
import com.example.triptalk.domain.user.dto.AuthResponse;
import com.example.triptalk.domain.user.entity.RefreshToken;
import com.example.triptalk.domain.user.entity.User;
import com.example.triptalk.domain.user.repository.RefreshTokenRepository;
import com.example.triptalk.domain.user.repository.UserRepository;
import com.example.triptalk.global.apiPayload.code.status.ErrorStatus;
import com.example.triptalk.global.apiPayload.exception.handler.ErrorHandler;
import com.example.triptalk.global.security.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
@Transactional
public class AuthServiceImpl implements AuthService {

private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;
private final AuthConverter authConverter;

@Override
public AuthResponse.SignUpDTO signUp(AuthRequest.SignUpDTO request) {
// 이메일 중복 체크
if (userRepository.existsByEmail(request.getEmail())) {
throw new ErrorHandler(ErrorStatus.AUTH_DUPLICATE_EMAIL);
}

// 비밀번호 인코딩 후 유저 생성
String encodedPassword = passwordEncoder.encode(request.getPassword());
User user = authConverter.toUser(request, encodedPassword);
User savedUser = userRepository.save(user);

return authConverter.toSignUpResponse(savedUser);
}

@Override
public AuthResponse.TokenDTO login(AuthRequest.LoginDTO request) {
// 유저 조회
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new ErrorHandler(ErrorStatus.USER_NOT_FOUND));

// 비밀번호 검증
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new ErrorHandler(ErrorStatus.AUTH_INVALID_PASSWORD);
}

// 토큰 생성
String accessToken = jwtTokenProvider.createAccessToken(user.getEmail());
String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail());

// RefreshToken 저장
saveRefreshToken(user.getId(), refreshToken);

return authConverter.toTokenResponse(accessToken, refreshToken);
}

@Override
public AuthResponse.TokenDTO reissue(AuthRequest.ReissueDTO request) {
// RefreshToken 유효성 검증
if (!jwtTokenProvider.validateToken(request.getRefreshToken())) {
throw new ErrorHandler(ErrorStatus.AUTH_INVALID_TOKEN);
}

// DB에서 RefreshToken 조회
RefreshToken refreshToken = refreshTokenRepository.findByToken(request.getRefreshToken())
.orElseThrow(() -> new ErrorHandler(ErrorStatus.AUTH_TOKEN_NOT_FOUND));

// 만료 여부 확인
if (refreshToken.isExpired()) {
refreshTokenRepository.delete(refreshToken);
throw new ErrorHandler(ErrorStatus.AUTH_EXPIRED_TOKEN);
}

// 유저 조회
User user = userRepository.findById(refreshToken.getUserId())
.orElseThrow(() -> new ErrorHandler(ErrorStatus.USER_NOT_FOUND));

// 새로운 토큰 생성
String newAccessToken = jwtTokenProvider.createAccessToken(user.getEmail());
String newRefreshToken = jwtTokenProvider.createRefreshToken(user.getEmail());

// 기존 RefreshToken 삭제 후 새로운 토큰 저장
refreshTokenRepository.delete(refreshToken);
saveRefreshToken(user.getId(), newRefreshToken);
Comment on lines +92 to +94
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

중복된 삭제 로직을 제거하세요.

Line 93에서 기존 RefreshToken을 삭제한 후, Line 94의 saveRefreshToken() 메서드 내부(Line 108)에서 다시 동일한 삭제 작업을 수행합니다. 이는 불필요한 중복입니다.

다음 중 하나를 선택하여 수정하세요:

옵션 1: reissue()에서 삭제 제거 (권장)

         // 새로운 토큰 생성
         String newAccessToken = jwtTokenProvider.createAccessToken(user.getEmail());
         String newRefreshToken = jwtTokenProvider.createRefreshToken(user.getEmail());
 
-        // 기존 RefreshToken 삭제 후 새로운 토큰 저장
-        refreshTokenRepository.delete(refreshToken);
+        // 새로운 토큰 저장 (saveRefreshToken이 기존 토큰 삭제 처리)
         saveRefreshToken(user.getId(), newRefreshToken);

옵션 2: saveRefreshToken()을 upsert 방식으로 변경

현재 deleteByUserIdsave하는 패턴보다는, 기존 토큰이 있으면 업데이트하고 없으면 생성하는 방식이 더 효율적일 수 있습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/service/AuthServiceImpl.java
around lines 92-94 (and note saveRefreshToken's internal deletion at line ~108),
the refresh token is being deleted twice: once in reissue() (line 93) and again
inside saveRefreshToken(). Remove the redundant delete from reissue() so
saveRefreshToken() remains the single place handling replacement, or
alternatively implement an upsert in saveRefreshToken() (replace
deleteByUserId+save with findByUserId and update if present, otherwise create) —
preferred: delete the explicit refreshTokenRepository.delete(refreshToken) call
in reissue(), leaving saveRefreshToken() to handle persistence consistently.


return authConverter.toTokenResponse(newAccessToken, newRefreshToken);
}

@Override
public void logout(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new ErrorHandler(ErrorStatus.USER_NOT_FOUND));
refreshTokenRepository.deleteByUserId(user.getId());
}

private void saveRefreshToken(Long userId, String token) {
// 기존 토큰 삭제
refreshTokenRepository.deleteByUserId(userId);

// 새 토큰 저장
RefreshToken refreshToken = RefreshToken.builder()
.token(token)
.userId(userId)
.expiryDate(LocalDateTime.now().plusSeconds(
jwtTokenProvider.getRefreshTokenExpiration() / 1000))
Comment on lines +114 to +115
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for getRefreshTokenExpiration method definition and refreshTokenExpiration property
rg -n -A5 -B5 "getRefreshTokenExpiration|refreshTokenExpiration" --type java

Repository: TripTalk/BE_SpringBoot

Length of output: 4415


🏁 Script executed:

#!/bin/bash
# Read JwtProperties to see if there are any comments
cat -n src/main/java/com/example/triptalk/global/security/jwt/JwtProperties.java

echo "---"

# Search for JWT configuration in properties/yaml files
find . -type f \( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \) -exec cat {} \; | grep -A2 -B2 "jwt:" || echo "No JWT config found in standard locations"

Repository: TripTalk/BE_SpringBoot

Length of output: 667


확인된 문제: 만료 시간 계산 시 단위 변환이 명확하지 않음

getRefreshTokenExpiration()은 밀리초를 반환합니다 (JwtTokenProvider의 Date 생성자 사용법으로 확인). AuthServiceImpl에서 /1000으로 나누어 초 단위로 변환하는 것은 LocalDateTime.plusSeconds()의 요구사항에 맞게 올바르게 동작합니다.

다만 다음과 같이 개선하면 코드 의도가 더욱 명확해집니다:

  • JwtProperties의 refreshTokenExpiration 필드에 /** 밀리초(ms) 단위 */ 주석 추가
  • AuthServiceImpl 115줄의 /1000 위에 // 밀리초를 초 단위로 변환 주석 추가
  • 또는 명시적인 단위 변환 메서드(예: Duration.ofMillis(ms).toSeconds()) 사용 검토
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/service/AuthServiceImpl.java
around lines 114-115, the expiry calculation uses
jwtTokenProvider.getRefreshTokenExpiration()/1000 which is correct but unclear
about units; update to make intent explicit by either adding a comment above the
division: "// 밀리초를 초 단위로 변환", or better replace the expression with an explicit
conversion using Duration (e.g.,
Duration.ofMillis(jwtTokenProvider.getRefreshTokenExpiration()).toSeconds()),
and also add a Javadoc/comment on JwtProperties.refreshTokenExpiration
indicating it is in milliseconds (/** 밀리초(ms) 단위 */) so the unit is clear across
the codebase.

.build();

refreshTokenRepository.save(refreshToken);
}
}

Loading
Loading