-
Notifications
You must be signed in to change notification settings - Fork 0
feat : login 추가 #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThis change introduces a comprehensive user authentication and management system using JWT and OAuth2, specifically integrating Kakao login. It adds new entities, DTOs, repositories, services, controllers, and security configurations. The build system is updated for QueryDSL and JWT dependencies, and Swagger/OpenAPI configuration is enhanced for JWT security documentation. Project structure and IDE configuration files are also updated. Changes
Sequence Diagram(s)User Sign-up and Kakao Login FlowsequenceDiagram
participant Client
participant UserController
participant UserService
participant KakaoService
participant SignService
participant UserRepository
participant JwtTokenProvider
participant RefreshTokenRepository
Client->>UserController: POST /api/v1/user/signup (SignUpRequest)
UserController->>UserService: signupUser(request)
UserService->>UserRepository: existsByEmail(email)
UserRepository-->>UserService: boolean
alt Email not exists
UserService->>UserRepository: save(new User)
UserRepository-->>UserService: User
UserService-->>UserController: BaseResponse<User>
else Email exists
UserService-->>UserController: Error
end
UserController-->>Client: Response
Client->>UserController: POST /api/v1/user/kakao (KakaoLoginRequest)
UserController->>KakaoService: loginWithKakao(accessToken)
KakaoService->>Kakao API: GET /v2/user/me
Kakao API-->>KakaoService: KakaoUserResponse
KakaoService->>SignService: saveUserKakao(userInfo, "kakao")
SignService->>UserRepository: save(new User)
UserRepository-->>SignService: User
SignService-->>KakaoService: User
KakaoService->>JwtTokenProvider: createToken(userId)
JwtTokenProvider->>RefreshTokenRepository: save(refreshToken)
JwtTokenProvider-->>KakaoService: TokenResponse
KakaoService-->>UserController: TokenResponse
UserController-->>Client: BaseResponse<TokenResponse>
JWT Authentication Filter FlowsequenceDiagram
participant Client
participant JwtAuthenticationFilter
participant JwtTokenProvider
participant SecurityContext
Client->>JwtAuthenticationFilter: HTTP request with Authorization header
JwtAuthenticationFilter->>JwtTokenProvider: validateToken(token)
JwtTokenProvider-->>JwtAuthenticationFilter: boolean
alt Token valid
JwtAuthenticationFilter->>JwtTokenProvider: getAuthentication(token)
JwtTokenProvider-->>JwtAuthenticationFilter: Authentication
JwtAuthenticationFilter->>SecurityContext: setAuthentication(auth)
end
JwtAuthenticationFilter-->>Client: Continue filter chain
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 30
🔭 Outside diff range comments (1)
greatjourney/build.gradle (1)
65-71: Remove duplicate CSV and POI dependencies.The CSV and POI dependencies are duplicated (lines 65-66 and 70-71). Keep only one declaration of each.
Apply this diff to remove the duplicates:
// CSV 파싱 implementation 'org.apache.commons:commons-csv:1.10.0' // Excel(XLSX) 파싱 implementation 'org.apache.poi:poi-ooxml:5.2.5' - implementation 'org.apache.commons:commons-csv:1.10.0' - implementation 'org.apache.poi:poi-ooxml:5.2.5'
🧹 Nitpick comments (20)
greatjourney/.gitignore (1)
31-33: Consolidate duplicate ignore patterns and align wildcard usageThere is already a wildcard rule (
**/application.properties) at line 28.
Keeping the root-levelapplication.properties(line 31) duplicates this, andapplication.yml(line 32) is the only new entry but lacks the same wildcard coverage.
Recommend pruning the duplicate and applying the wildcard for consistency:-application.properties -application.yml +**/application.ymlThis continues to ignore
application.propertieseverywhere (via line 28) and now does the same forapplication.yml.greatjourney/src/main/java/backend/greatjourney/config/SwaggerConfig.java (1)
16-16: Consider using a constant for the security scheme name.To improve maintainability and avoid potential typos, consider defining the security scheme name as a constant.
Add a constant at the class level:
@Configuration public class SwaggerConfig { + private static final String BEARER_AUTH_SCHEME = "BearerAuth"; + @Bean public OpenAPI openAPI() { - SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth"); + SecurityRequirement securityRequirement = new SecurityRequirement().addList(BEARER_AUTH_SCHEME);Then update the components registration to use the same constant.
greatjourney/src/main/java/backend/greatjourney/domain/token/dto/TokenResponse.java (1)
6-11: Prefer immutable DTO – declare fieldsfinaland remove settersThe DTO is only ever populated through the all-args constructor, so declaring the two fields
finalclarifies immutability and prevents accidental mutation through reflection libraries.-@Getter -@AllArgsConstructor -public class TokenResponse { - private String accessToken; - private String refreshToken; -} +@Getter +@AllArgsConstructor +public class TokenResponse { + private final String accessToken; + private final String refreshToken; +}If you need Jackson deserialization, add
@JsonCreatoror a no-args constructor instead of sacrificing immutability.greatjourney/src/main/java/backend/greatjourney/domain/user/dto/response/KakaoUserResponse.java (1)
16-21: Consider making DTOsrecords for concise, immutable representationSince this class is a pure data holder, Java 17+
recordsyntax can cut boilerplate and enforce immutability:public record KakaoUserResponse( Long id, KakaoAccount kakaoAccount, KakaoProfile profile) { }greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Status.java (2)
3-6: Clarify enum semantics & consider more conventional naming
SUCCESSis somewhat vague for a long-lived user state. In most auth domains the active state is namedACTIVEorENABLED, which better contrasts withPENDINGandDELETED. If the enum is persisted, changing names later becomes a breaking DB migration, so it’s worth double-checking the vocabulary now.No code change required if you are confident, but please verify the terminology across the service layer and API docs.
3-6: Add minimal JavadocEven for small enums, a one-liner Javadoc describing when each status is assigned greatly improves maintainability for newcomers.
greatjourney/src/main/java/backend/greatjourney/domain/user/entity/UserRole.java (1)
3-5: Integrate with Spring-Security’sGrantedAuthorityonce, not everywhereYou’ll soon need a
Collection<? extends GrantedAuthority>representation. Consider adding a helper inside the enum:public GrantedAuthority asAuthority() { return new SimpleGrantedAuthority(name()); }This eliminates scattered string concatenations in security code.
.idea/modules/1984157471/greatjourney.main.iml (1)
1-7: IDE-specific files should be excluded from VCS
.imlfiles are machine-generated and environment-specific; committing them clutters the repo and causes merge noise. Add.idea/**(or at least*.iml) to.gitignoreunless the team has agreed otherwise..idea/gradle.xml (1)
6-11: Same concern: project-local IDE metadata in VCSThe change merely updates an IntelliJ Gradle linkage path. Unless you’re intentionally version-controlling workspace configs, consider ignoring
.idea/to avoid accidental breakage for other developers or CI images.greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/SignUpRequest.java (1)
1-5: Add JavaDoc documentation for the record.Consider adding JavaDoc to document the purpose and usage of this DTO.
+/** + * DTO for user sign-up requests containing email and authentication domain. + * + * @param email user's email address + * @param domain authentication domain (e.g., KAKAO, GOOGLE, NAVER) + */ public record SignUpRequest(greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/KakaoLoginRequest.java (1)
1-4: Add JavaDoc documentation for the record.Consider adding JavaDoc to document the purpose of this DTO in the Kakao OAuth flow.
+/** + * DTO for Kakao login requests containing the OAuth access token. + * + * @param accessToken OAuth access token obtained from Kakao + */ public record KakaoLoginRequest(String accessToken) {greatjourney/src/main/java/backend/greatjourney/global/exception/CustomException.java (2)
6-9: Consider adding constructor overload for custom messages.While the current constructor uses the ErrorCode's message, you might want to allow custom messages while preserving the ErrorCode.
public CustomException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } + + public CustomException(ErrorCode errorCode, String customMessage) { + super(customMessage); + this.errorCode = errorCode; + } + + public CustomException(ErrorCode errorCode, String customMessage, Throwable cause) { + super(customMessage, cause); + this.errorCode = errorCode; + }
1-14: Add JavaDoc documentation for the exception class.Consider adding comprehensive JavaDoc to document the purpose and usage of this custom exception.
+/** + * Custom runtime exception that wraps an ErrorCode for centralized error handling. + * This exception is used throughout the application to provide consistent error + * reporting with standardized error codes and messages. + * + * @see ErrorCode + */ public class CustomException extends RuntimeException{greatjourney/src/main/java/backend/greatjourney/global/security/config/SecurityConfig.java (1)
34-36: Consider code consistency for comments.The inline comments are in Korean while the rest of the codebase appears to use English. Consider maintaining consistent language throughout the codebase.
greatjourney/src/main/java/backend/greatjourney/domain/user/repository/impl/UserRepositoryImplement.java (1)
15-15: Fix variable naming typo.The variable name
quesrappears to be a typo.- QUser quesr = QUser.user; + QUser qUser = QUser.user;And update all references to use the corrected name.
greatjourney/src/main/java/backend/greatjourney/domain/user/service/SignService.java (1)
3-3: Remove unused import.The
DomImplimport is not used in this class and should be removed.-import org.apache.xmlbeans.impl.store.DomImpl;greatjourney/src/main/java/backend/greatjourney/global/security/entitiy/CustomOAuth2User.java (1)
1-1: Fix package name typo.The package name contains a typo:
entitiyshould beentity.-package backend.greatjourney.global.security.entitiy; +package backend.greatjourney.global.security.entity;This will also require updating all import statements that reference this class.
greatjourney/src/main/java/backend/greatjourney/domain/user/service/KakaoService.java (1)
39-39: Use Spring-managed ObjectMapperObjectMapper should be injected as a Spring bean to ensure consistent JSON serialization/deserialization settings across the application.
-private final ObjectMapper objectMapper = new ObjectMapper(); +private final ObjectMapper objectMapper;greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtTokenProvider.java (2)
118-129: Consider returning Long for user ID consistencyThe method returns a String but the subject contains a Long userId. Consider returning Long directly to maintain type consistency.
-public String getUserIdFromToken(String token) { +public Long getUserIdFromToken(String token) { try { - return Jwts.parser() + String subject = Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload() .getSubject(); + return Long.parseLong(subject); } catch (JwtException e) { throw new CustomException(ErrorCode.JWT_PARSE_FAILED); + } catch (NumberFormatException e) { + throw new CustomException(ErrorCode.INVALID_TOKEN_FORMAT); } }
110-110: Fix formatting: add space after comma-return new TokenResponse(accessToken,refreshToken); +return new TokenResponse(accessToken, refreshToken);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (37)
.idea/compiler.xml(1 hunks).idea/gradle.xml(1 hunks).idea/modules.xml(1 hunks).idea/modules/1984157471/greatjourney.main.iml(1 hunks).idea/modules/backend.greatjourney.main.iml(1 hunks)greatjourney/.gitignore(1 hunks)greatjourney/build.gradle(4 hunks)greatjourney/src/main/java/backend/greatjourney/GreatjourneyApplication.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/config/QueryDslConfig.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/config/SwaggerConfig.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/token/dto/TokenResponse.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/token/entity/RefreshToken.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/token/repository/RefreshTokenRepository.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationFilter.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationManager.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtTokenProvider.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/controller/UserController.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/dto/properties/KakaoOAuthProperties.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/ChangeUserRequest.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/KakaoLoginRequest.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/SignUpRequest.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/dto/response/KakaoUserResponse.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Domain.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Status.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/entity/User.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/entity/UserRole.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepository.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepositoryCustom.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/repository/impl/UserRepositoryImplement.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/service/KakaoService.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/service/SignService.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/domain/user/service/UserService.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/global/exception/BaseResponse.java(0 hunks)greatjourney/src/main/java/backend/greatjourney/global/exception/CustomException.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/global/security/config/SecurityConfig.java(1 hunks)greatjourney/src/main/java/backend/greatjourney/global/security/entitiy/CustomOAuth2User.java(1 hunks)
💤 Files with no reviewable changes (1)
- greatjourney/src/main/java/backend/greatjourney/global/exception/BaseResponse.java
🧰 Additional context used
🧬 Code Graph Analysis (3)
greatjourney/src/main/java/backend/greatjourney/domain/token/entity/RefreshToken.java (1)
greatjourney/src/main/java/backend/greatjourney/domain/token/dto/TokenResponse.java (1)
Getter(6-11)
greatjourney/src/main/java/backend/greatjourney/domain/user/controller/UserController.java (2)
greatjourney/src/main/java/backend/greatjourney/global/security/entitiy/CustomOAuth2User.java (1)
CustomOAuth2User(13-47)greatjourney/src/main/java/backend/greatjourney/domain/user/repository/impl/UserRepositoryImplement.java (1)
RequiredArgsConstructor(12-40)
greatjourney/src/main/java/backend/greatjourney/domain/user/service/SignService.java (3)
greatjourney/src/main/java/backend/greatjourney/domain/user/service/UserService.java (1)
Service(21-93)greatjourney/src/main/java/backend/greatjourney/domain/user/repository/impl/UserRepositoryImplement.java (1)
RequiredArgsConstructor(12-40)greatjourney/src/main/java/backend/greatjourney/domain/user/service/KakaoService.java (1)
Slf4j(30-68)
🔇 Additional comments (19)
greatjourney/src/main/java/backend/greatjourney/config/SwaggerConfig.java (3)
3-11: LGTM! Clean import organization.The imports are well-organized and include all necessary OpenAPI and Spring components for JWT authentication configuration.
25-30: LGTM! Well-structured API metadata.The API information is properly configured with appropriate title, description, and version for the "어디로" (Where to go) application.
32-39: LGTM! Correct JWT security scheme configuration.The security scheme is properly configured for JWT bearer token authentication with correct type, scheme, format, and header location.
.idea/modules.xml (1)
5-7: IDE-specific files should stay out of VCSCommitting
.idea/**risks merge noise and developer-specific state. Add the directory to.gitignoreunless the team explicitly version-controls IDEA settings.greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Domain.java (1)
3-7: LGTM – simple, self-explanatory enumNo issues found; naming is clear and future-proof for additional providers.
.idea/compiler.xml (2)
9-18: QueryDSL annotation processor configuration looks correct.The added annotation processor entries for QueryDSL, Jakarta persistence, and related libraries are properly configured to support code generation.
20-20: Module name update aligns with project structure.The module name change to
backend.greatjourney.mainis consistent with the project's package structure..idea/modules/backend.greatjourney.main.iml (1)
4-5: Module configuration for generated sources is correct.The content root and source folder configuration for annotation processor generated sources is properly set up to support QueryDSL code generation.
greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepositoryCustom.java (1)
7-11: LGTM! Well-designed repository interface.The interface follows good practices with clear method names, proper use of Optional for null safety, and follows Spring Data conventions.
greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepository.java (1)
7-8: LGTM! Clean repository interface design.The interface correctly extends both JpaRepository and UserRepositoryCustom, following Spring Data best practices for combining standard and custom repository operations.
greatjourney/src/main/java/backend/greatjourney/GreatjourneyApplication.java (1)
9-9: LGTM! Correctly enables Spring Security.Removing the SecurityAutoConfiguration exclusion is the right approach to enable the comprehensive security features being added in this PR, including JWT authentication and Kakao OAuth integration.
greatjourney/src/main/java/backend/greatjourney/config/QueryDslConfig.java (1)
11-21: LGTM! Standard QueryDSL configuration implementation.The QueryDSL configuration follows Spring best practices with proper dependency injection and bean creation. The implementation is clean and minimal.
greatjourney/build.gradle (2)
35-37: JWT dependency upgrade looks correct.The JWT dependency upgrade from 0.11.5 to 0.12.3 and changing
jjwt-implfromruntimeOnlytoimplementationis correct for the newer version requirements.
73-77: QueryDSL configuration is properly set up.The QueryDSL dependencies and annotation processors are correctly configured for Jakarta EE compatibility.
greatjourney/src/main/java/backend/greatjourney/domain/token/entity/RefreshToken.java (1)
24-30: Builder implementation is well-structured.The builder pattern implementation properly initializes all fields and uses appropriate parameter names. The use of
Instantfor expiry dates is a good choice for precise timestamp handling.greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationFilter.java (1)
14-20: Well-structured JWT authentication filter.The class properly extends
OncePerRequestFilterand uses constructor injection for dependencies. The filter will be executed once per request, which is appropriate for JWT authentication.greatjourney/src/main/java/backend/greatjourney/domain/user/repository/impl/UserRepositoryImplement.java (2)
17-26: Correct implementation for existence check.The method properly uses
selectOne()withfetchFirst()to efficiently check for email existence without fetching the entire entity.
28-36: Proper optional result handling.The method correctly uses
Optional.ofNullable()withfetchOne()to handle the case where no user is found.greatjourney/src/main/java/backend/greatjourney/global/security/entitiy/CustomOAuth2User.java (1)
42-45: Simple but effective authorities implementation.The method correctly creates a
GrantedAuthorityfrom the role string using a lambda expression.Consider adding role validation if needed:
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(() -> role != null ? role : "ROLE_USER"); }
| @Bean | ||
| public OpenAPI openAPI() { | ||
| SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth"); | ||
|
|
||
| return new OpenAPI() | ||
| .components(new Components()) | ||
| .info(apiInfo()) | ||
| .addSecurityItem(securityRequirement) | ||
| .schemaRequirement("BearerAuth", securityScheme()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the security scheme registration method.
The method schemaRequirement on line 22 should be components() to properly register the security scheme. The current implementation won't correctly associate the security scheme with the OpenAPI specification.
Apply this diff to fix the security scheme registration:
return new OpenAPI()
- .components(new Components())
+ .components(new Components().addSecuritySchemes("BearerAuth", securityScheme()))
.info(apiInfo())
.addSecurityItem(securityRequirement)
- .schemaRequirement("BearerAuth", securityScheme());Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In greatjourney/src/main/java/backend/greatjourney/config/SwaggerConfig.java
lines 14 to 23, replace the call to schemaRequirement("BearerAuth",
securityScheme()) with a proper registration of the security scheme inside the
components() method. Specifically, add the security scheme to the Components
object and set it via components() on the OpenAPI instance to correctly
associate the security scheme with the OpenAPI specification.
| public class KakaoUserResponse { | ||
| private Long id; | ||
| private KakaoAccount kakao_account; | ||
| private Properties properties; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| public static class KakaoAccount { | ||
| private String email; | ||
| } | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| public static class Properties { | ||
| private String nickname; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix naming & type-shadowing – use Java-style field names with @JsonProperty
kakao_account and properties violate Java naming conventions and the nested class Properties shadows java.util.Properties, which is confusing.
public class KakaoUserResponse {
- private Long id;
- private KakaoAccount kakao_account;
- private Properties properties;
+ private Long id;
+
+ @JsonProperty("kakao_account")
+ private KakaoAccount kakaoAccount;
+
+ @JsonProperty("properties")
+ private KakaoProfile profile;
@@
- public static class Properties {
- private String nickname;
- }
+ public static class KakaoProfile {
+ private String nickname;
+ }
}This keeps JSON binding intact while restoring idiomatic Java naming and avoiding class-name clashes.
📝 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.
| public class KakaoUserResponse { | |
| private Long id; | |
| private KakaoAccount kakao_account; | |
| private Properties properties; | |
| @Getter | |
| @Setter | |
| @JsonIgnoreProperties(ignoreUnknown = true) | |
| public static class KakaoAccount { | |
| private String email; | |
| } | |
| @Getter | |
| @Setter | |
| @JsonIgnoreProperties(ignoreUnknown = true) | |
| public static class Properties { | |
| private String nickname; | |
| } | |
| } | |
| public class KakaoUserResponse { | |
| private Long id; | |
| @JsonProperty("kakao_account") | |
| private KakaoAccount kakaoAccount; | |
| @JsonProperty("properties") | |
| private KakaoProfile profile; | |
| @Getter | |
| @Setter | |
| @JsonIgnoreProperties(ignoreUnknown = true) | |
| public static class KakaoAccount { | |
| private String email; | |
| } | |
| @Getter | |
| @Setter | |
| @JsonIgnoreProperties(ignoreUnknown = true) | |
| public static class KakaoProfile { | |
| private String nickname; | |
| } | |
| } |
🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/domain/user/dto/response/KakaoUserResponse.java
between lines 11 and 29, rename the fields kakao_account and properties to
follow Java camelCase conventions (e.g., kakaoAccount and userProperties), and
annotate them with @JsonProperty to map to the original JSON names. Also, rename
the nested Properties class to avoid shadowing java.util.Properties, such as
renaming it to UserProperties, and update all references accordingly to maintain
clarity and proper JSON binding.
| private final HttpStatus status; | ||
| private final int code; | ||
| private final String message; | ||
|
|
||
|
|
||
| ErrorCode( HttpStatus status,int code, String message){ | ||
| this.code = code; | ||
| this.status = status; | ||
| this.message = message; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Derive code from status to remove duplication
You can eliminate the integer parameter entirely:
@Getter
public enum ErrorCode {
LOGIN_FAIL(BAD_REQUEST,"로그인에 오류가 발생하였습니다."),
…
USER_NOT_FOUND(BAD_REQUEST,"존재하지 않는 유저입니다.");
private final HttpStatus status;
private final int code;
private final String message;
ErrorCode(HttpStatus status,String message){
this.status = status;
this.code = status.value();
this.message= message;
}
}This prevents future inconsistencies at zero runtime cost.
🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java
around lines 28 to 37, the integer code field is redundantly passed as a
constructor parameter despite being derivable from the HttpStatus. Remove the
int code parameter from the constructor and instead assign this.code =
status.value() inside the constructor. Update the constructor signature to
accept only HttpStatus and message, and adjust all enum constants accordingly to
pass only HttpStatus and message. This eliminates duplication and ensures code
consistency.
| LOGIN_FAIL(HttpStatus.BAD_REQUEST,400,"로그인에 오류가 발생하였습니다."), | ||
| INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,500,"서버에 오류가 발생하였습니다."), | ||
| JWT_KEY_GENERATION_FAILED(HttpStatus.BAD_REQUEST,400,"JWT 키 생성에 실패하였습니다."), | ||
| NO_REFRESH_TOKEN(UNAUTHORIZED,400, "리프레시 토큰이 없습니다."), | ||
| LOGOUT_ERROR(BAD_REQUEST,400,"로그아웃에 실패하였습니다."), | ||
| SIGNUP_ERROR(BAD_REQUEST,400,"회원가입에러입니다."), | ||
| EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, 400,"만료된 토큰입니다."), | ||
| JWT_PARSE_FAILED(BAD_REQUEST,404,"토큰 파싱이 잘못되었습니다."), | ||
|
|
||
| KAKAO_USER_ERROR(BAD_REQUEST,404,"카카오 유저 정보를 가져오지 못하였습니다."), | ||
| TOKEN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,500, "토큰을 제대로 생성하지 못하였습니다."), | ||
|
|
||
|
|
||
| USER_NOT_FOUND(BAD_REQUEST,404,"존재하지 않는 유저입니다."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTTP status ↔ numeric code mismatch – will cause inconsistent API responses
Several constants pair HttpStatus.UNAUTHORIZED (401) with numeric code 400, and BAD_REQUEST (400) with numeric 404, e.g.:
NO_REFRESH_TOKEN(UNAUTHORIZED,400,…)
EXPIRED_REFRESH_TOKEN(UNAUTHORIZED,400,…)
JWT_PARSE_FAILED(BAD_REQUEST,404,…)
Clients relying on the numeric field will receive conflicting information.
-NO_REFRESH_TOKEN(UNAUTHORIZED,400, "리프레시 토큰이 없습니다."),
-EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, 400,"만료된 토큰입니다."),
-JWT_PARSE_FAILED(BAD_REQUEST,404,"토큰 파싱이 잘못되었습니다."),
+NO_REFRESH_TOKEN(UNAUTHORIZED,401, "리프레시 토큰이 없습니다."),
+EXPIRED_REFRESH_TOKEN(UNAUTHORIZED,401,"만료된 토큰입니다."),
+JWT_PARSE_FAILED(BAD_REQUEST,400,"토큰 파싱이 잘못되었습니다."),Audit the entire enum to keep code == status.value(), or derive code automatically from status to prevent drift.
📝 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.
| LOGIN_FAIL(HttpStatus.BAD_REQUEST,400,"로그인에 오류가 발생하였습니다."), | |
| INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,500,"서버에 오류가 발생하였습니다."), | |
| JWT_KEY_GENERATION_FAILED(HttpStatus.BAD_REQUEST,400,"JWT 키 생성에 실패하였습니다."), | |
| NO_REFRESH_TOKEN(UNAUTHORIZED,400, "리프레시 토큰이 없습니다."), | |
| LOGOUT_ERROR(BAD_REQUEST,400,"로그아웃에 실패하였습니다."), | |
| SIGNUP_ERROR(BAD_REQUEST,400,"회원가입에러입니다."), | |
| EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, 400,"만료된 토큰입니다."), | |
| JWT_PARSE_FAILED(BAD_REQUEST,404,"토큰 파싱이 잘못되었습니다."), | |
| KAKAO_USER_ERROR(BAD_REQUEST,404,"카카오 유저 정보를 가져오지 못하였습니다."), | |
| TOKEN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,500, "토큰을 제대로 생성하지 못하였습니다."), | |
| USER_NOT_FOUND(BAD_REQUEST,404,"존재하지 않는 유저입니다."); | |
| JWT_KEY_GENERATION_FAILED(HttpStatus.BAD_REQUEST, 400, "JWT 키 생성에 실패하였습니다."), | |
| NO_REFRESH_TOKEN(UNAUTHORIZED, 401, "리프레시 토큰이 없습니다."), | |
| LOGOUT_ERROR(BAD_REQUEST, 400, "로그아웃에 실패하였습니다."), | |
| SIGNUP_ERROR(BAD_REQUEST, 400, "회원가입에러입니다."), | |
| EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, 401, "만료된 토큰입니다."), | |
| JWT_PARSE_FAILED(BAD_REQUEST, 400, "토큰 파싱이 잘못되었습니다."), | |
| KAKAO_USER_ERROR(BAD_REQUEST, 404, "카카오 유저 정보를 가져오지 못하였습니다."), |
🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java
between lines 11 and 24, the numeric codes assigned to error constants do not
match the HTTP status codes, causing inconsistent API responses. Fix this by
ensuring the numeric code matches the value of the HttpStatus enum used for each
constant, either by setting code to status.value() directly or by removing the
manual code parameter and deriving it automatically from the HttpStatus to keep
them consistent.
| package backend.greatjourney.domain.user.dto.request; | ||
|
|
||
| public record ChangeUserRequest(String name) { | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add validation annotations to prevent empty names
Incoming payloads can currently pass an empty string or null, leading to silent DB corruption or constraint violations down-stream. Add Bean-Validation:
package backend.greatjourney.domain.user.dto.request;
+import jakarta.validation.constraints.NotBlank;
+
-public record ChangeUserRequest(String name) {
-}
+public record ChangeUserRequest(
+ @NotBlank(message = "name must not be blank")
+ String name
+) {}📝 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.
| package backend.greatjourney.domain.user.dto.request; | |
| public record ChangeUserRequest(String name) { | |
| } | |
| package backend.greatjourney.domain.user.dto.request; | |
| import jakarta.validation.constraints.NotBlank; | |
| public record ChangeUserRequest( | |
| @NotBlank(message = "name must not be blank") | |
| String name | |
| ) {} |
🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/ChangeUserRequest.java
at lines 1 to 4, the ChangeUserRequest record lacks validation annotations on
the name field, allowing empty or null values that can cause database issues.
Add appropriate Bean Validation annotations such as @NotBlank or @NotEmpty and
@NotNull to the name parameter to ensure it cannot be null or empty when
received.
| //회원정보수정 | ||
| @Operation(summary = "회원정보 수정 API") | ||
| @PatchMapping("/change") | ||
| public BaseResponse<?> chageUserInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User,@RequestBody |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix typo in method name
-public BaseResponse<?> chageUserInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User,@RequestBody
+public BaseResponse<?> changeUserInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User, @RequestBody📝 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.
| public BaseResponse<?> chageUserInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User,@RequestBody | |
| public BaseResponse<?> changeUserInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User, @RequestBody |
🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/domain/user/controller/UserController.java
at line 69, there is a typo in the method name "chageUserInfo". Rename the
method to "changeUserInfo" to correct the spelling.
| public BaseResponse<?> loginKakao(@RequestBody KakaoLoginRequest request){ | ||
| return BaseResponse.builder() | ||
| .isSuccess(true) | ||
| .code(200) | ||
| .message("로그인이 완료되었습니다.") | ||
| .data(kakaoService.loginWithKakao(request.accessToken())) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Maintain consistent response handling pattern
The Kakao login endpoint manually builds the response while other endpoints delegate this to the service layer. Consider maintaining consistency by moving response building to the service.
public BaseResponse<?> loginKakao(@RequestBody KakaoLoginRequest request){
- return BaseResponse.builder()
- .isSuccess(true)
- .code(200)
- .message("로그인이 완료되었습니다.")
- .data(kakaoService.loginWithKakao(request.accessToken()))
- .build();
+ return kakaoService.loginWithKakao(request.accessToken());
}Then modify KakaoService to return BaseResponse instead of TokenResponse.
📝 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.
| public BaseResponse<?> loginKakao(@RequestBody KakaoLoginRequest request){ | |
| return BaseResponse.builder() | |
| .isSuccess(true) | |
| .code(200) | |
| .message("로그인이 완료되었습니다.") | |
| .data(kakaoService.loginWithKakao(request.accessToken())) | |
| .build(); | |
| } | |
| public BaseResponse<?> loginKakao(@RequestBody KakaoLoginRequest request){ | |
| return kakaoService.loginWithKakao(request.accessToken()); | |
| } |
🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/domain/user/controller/UserController.java
around lines 49 to 56, the loginKakao method manually builds the BaseResponse
while other endpoints delegate this to the service layer. To fix this, refactor
the loginKakao method to directly return the response from
kakaoService.loginWithKakao without wrapping it again. Then update
KakaoService.loginWithKakao to return BaseResponse<TokenResponse> instead of
just TokenResponse, so the service handles the full response construction
consistently.
| Long userId = Long.parseLong(customOAuth2User.getUserId()); | ||
| RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId); | ||
|
|
||
| return BaseResponse.<Void>builder() | ||
| .code(200) | ||
| .message("로그아웃이 완료되었습니다.") | ||
| .data(refreshTokenRepository.deleteByToken(refreshToken.getToken())) | ||
| .isSuccess(true) | ||
| .build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null check for refresh token
The code doesn't handle the case where no refresh token exists for the user, which could cause a NullPointerException.
Long userId = Long.parseLong(customOAuth2User.getUserId());
RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId);
+if (refreshToken == null) {
+ return BaseResponse.<Void>builder()
+ .code(200)
+ .message("이미 로그아웃된 상태입니다.")
+ .isSuccess(true)
+ .build();
+}
return BaseResponse.<Void>builder()
.code(200)
.message("로그아웃이 완료되었습니다.")
- .data(refreshTokenRepository.deleteByToken(refreshToken.getToken()))
+ .data(null)
.isSuccess(true)
.build();
+refreshTokenRepository.deleteByToken(refreshToken.getToken());Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/domain/user/service/UserService.java
around lines 44 to 52, add a null check for the refreshToken object after
retrieving it from the repository. If refreshToken is null, handle this case
appropriately (e.g., return a response indicating no token found or proceed
without attempting to delete). This prevents a NullPointerException when calling
refreshToken.getToken().
| Long userId = Long.parseLong(customOAuth2User.getUserId()); | ||
| RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId); | ||
|
|
||
| refreshTokenRepository.deleteByToken(refreshToken.getToken()); | ||
| User user = userRepository.findByUserId(userId) | ||
| .orElseThrow(()->new CustomException(ErrorCode.USER_NOT_FOUND)); | ||
|
|
||
| user.setStatus(Status.DELETED); | ||
| userRepository.save(user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null check and consider using builder pattern
Similar to the logout method, this needs null checking for the refresh token. Also, using setters on entities is not ideal for immutability.
Long userId = Long.parseLong(customOAuth2User.getUserId());
RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId);
+if (refreshToken != null) {
+ refreshTokenRepository.deleteByToken(refreshToken.getToken());
+}
-refreshTokenRepository.deleteByToken(refreshToken.getToken());Consider using a builder pattern or dedicated update method instead of setters for better encapsulation.
📝 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.
| Long userId = Long.parseLong(customOAuth2User.getUserId()); | |
| RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId); | |
| refreshTokenRepository.deleteByToken(refreshToken.getToken()); | |
| User user = userRepository.findByUserId(userId) | |
| .orElseThrow(()->new CustomException(ErrorCode.USER_NOT_FOUND)); | |
| user.setStatus(Status.DELETED); | |
| userRepository.save(user); | |
| Long userId = Long.parseLong(customOAuth2User.getUserId()); | |
| RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId); | |
| if (refreshToken != null) { | |
| refreshTokenRepository.deleteByToken(refreshToken.getToken()); | |
| } | |
| User user = userRepository.findByUserId(userId) | |
| .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); | |
| user.setStatus(Status.DELETED); | |
| userRepository.save(user); |
🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/domain/user/service/UserService.java
around lines 58 to 66, add a null check for the refreshToken before calling
deleteByToken to avoid NullPointerException. Replace the direct setter call on
the user entity with a builder pattern or a dedicated update method to change
the user's status to DELETED, ensuring better encapsulation and immutability.
| throw new IllegalArgumentException("이미 존재하는 회원입니다."); | ||
| } | ||
| return new BaseResponse<>(true,"회원가입이 완료되었습니다",201,userRepository.save(User.builder() | ||
| .domain(Domain.valueOf(request.domain())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add validation for domain string
Domain.valueOf() will throw IllegalArgumentException if the domain string is invalid. Consider adding validation or using a safer parsing method.
+Domain domain;
+try {
+ domain = Domain.valueOf(request.domain());
+} catch (IllegalArgumentException e) {
+ throw new CustomException(ErrorCode.INVALID_DOMAIN);
+}
return new BaseResponse<>(true,"회원가입이 완료되었습니다",201,userRepository.save(User.builder()
- .domain(Domain.valueOf(request.domain()))
+ .domain(domain)
.email(request.email())
.status(Status.SUCCESS)
.build()));🤖 Prompt for AI Agents
In
greatjourney/src/main/java/backend/greatjourney/domain/user/service/UserService.java
at line 35, the use of Domain.valueOf(request.domain()) can throw an
IllegalArgumentException if the domain string is invalid. To fix this, add
validation before calling valueOf by checking if the domain string matches any
valid enum constants or use a safer parsing method that returns an Optional or
null if invalid. Handle the invalid case gracefully, such as throwing a custom
exception or returning an error response.
#️⃣ Issue Number
📝 요약(Summary)
로그인 구조 및 카카오 로그인을 구현하였습니다.
Spring security와 jwt 토큰을 사용해서 구현하였습니다.
📸 기능 스크린 샷 공유
💬 공유사항 to 리뷰어
✅ PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
closes : #1
Summary by CodeRabbit
New Features
Chores
.gitignoreto exclude sensitive configuration files.