Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
//REDIS
implementation 'org.redisson:redisson-spring-boot-starter:3.19.0'

testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.awaitility:awaitility:4.3.0'
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/example/gtable/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.gtable.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
Expand Down Expand Up @@ -51,7 +53,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers(
"/oauth2/authorization/kakao", // 카카오 로그인 요청
"/login/oauth2/code/**", // 카카오 인증 콜백
"/api/refresh-token") // refresh token (토큰 갱신)
"/api/refresh-token", // refresh token (토큰 갱신)
"/api/users/signup")
.permitAll()
.anyRequest().authenticated() // 그외 요청은 허가된 사람만 인가
)
Expand All @@ -60,5 +63,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
@RequiredArgsConstructor
public enum ErrorMessage {
// global
INVALID_INPUT_VALUE("입력값이 올바르지 않습니다.", "g001"),
INVALID_INPUT_VALUE("입력값이 올바르지 않습니다.", "global001"),

// auth
UNAUTHORIZED("권한이 없습니다", "a001"),
UNAUTHORIZED("권한이 없습니다", "auth001"),

// token
REFRESH_TOKEN_NOT_FOUND("기존 리프레시 토큰을 찾을 수 없습니다.", "t001"),
DOES_NOT_MATCH_REFRESH_TOKEN("기존 리프레시 토큰이 일치하지 않습니다.", "t002");
REFRESH_TOKEN_NOT_FOUND("기존 리프레시 토큰을 찾을 수 없습니다.", "token001"),
DOES_NOT_MATCH_REFRESH_TOKEN("기존 리프레시 토큰이 일치하지 않습니다.", "token002");

private final String message;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package com.example.gtable.user.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.gtable.global.api.ApiUtils;
import com.example.gtable.global.security.oauth2.dto.CustomOAuth2User;
import com.example.gtable.user.dto.ManagerSignupRequestDto;
import com.example.gtable.user.dto.UserResponseDto;
import com.example.gtable.user.entity.User;
import com.example.gtable.user.service.UserService;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -19,6 +26,19 @@
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;

// 관리자 회원가입
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody @Valid ManagerSignupRequestDto managerSignupRequestDto) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(
ApiUtils.success(
userService.signup(managerSignupRequestDto)
)
);
}

// 로그인된 유저 정보를 확인하는 api
@GetMapping("/me")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.gtable.user.dto;

import com.example.gtable.user.entity.Role;
import com.example.gtable.user.entity.SocialType;
import com.example.gtable.user.entity.User;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ManagerSignupRequestDto {

@NotBlank
@Email(regexp = "^(?=.{1,64}@)[A-Za-z0-9_-]+(\\.[A-Za-z0-9_-]+)*@"
+ "[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$")
@Schema(description = "이메일", example = "abc@gmail.com")
private String email;

@NotBlank
@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}")
@Schema(description = "비밀번호", example = "1234568!@")
private String password;

@NotBlank
@Pattern(regexp = "^[a-zA-Z가-힣]{2,12}$")
@Schema(description = "닉네임", example = "가십이")
private String nickname;

public User toEntity() {
return User.builder()
.email(email)
.password(password)
.nickname(nickname)
.socialType(SocialType.KAKAO)
.role(Role.MANAGER)
.build();

}
Comment on lines +34 to +43
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

보안과 설계 관점에서 몇 가지 중요한 개선사항이 있습니다!

현재 구현에서 발견된 주요 이슈들:

  1. 보안 이슈: 비밀번호가 평문으로 전달되고 있습니다
  2. 설계 이슈: 관리자 회원가입에서 SocialType.KAKAO가 하드코딩되어 있는 것이 적절하지 않아 보입니다
  3. 책임 분리: DTO에서 비즈니스 로직(역할 할당)을 결정하고 있습니다

다음과 같은 개선을 제안드립니다:

 public User toEntity() {
     return User.builder()
         .email(email)
-        .password(password)
+        .password(password) // 서비스 레이어에서 인코딩 처리
         .nickname(nickname)
-        .socialType(SocialType.KAKAO)
+        .socialType(SocialType.NONE) // 또는 적절한 관리자용 타입
         .role(Role.MANAGER)
         .build();
 }

비밀번호 인코딩은 서비스 레이어에서 처리하는 것이 더 안전하고 적절합니다.

📝 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 User toEntity() {
return User.builder()
.email(email)
.password(password)
.nickname(nickname)
.socialType(SocialType.KAKAO)
.role(Role.MANAGER)
.build();
}
public User toEntity() {
return User.builder()
.email(email)
.password(password) // 서비스 레이어에서 인코딩 처리
.nickname(nickname)
.socialType(SocialType.NONE) // 또는 적절한 관리자용 타입
.role(Role.MANAGER)
.build();
}
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/user/dto/ManagerSignupRequestDto.java around
lines 34 to 43, the toEntity method currently passes the password in plain text,
hardcodes SocialType.KAKAO, and assigns the role within the DTO, mixing business
logic with data transfer. To fix this, remove password encoding and role
assignment from the DTO; instead, have the service layer handle password
encoding and role setting. Also, avoid hardcoding SocialType in the DTO by
passing it as a parameter or setting it in the service layer to maintain
separation of concerns and improve security.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.gtable.user.dto;

import com.example.gtable.user.entity.Role;
import com.example.gtable.user.entity.User;

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

@Getter
@NoArgsConstructor
@Builder
@AllArgsConstructor
public class ManagerSignupResponseDto {
@Schema(description = "id", example = "1")
private Long id;
@Schema(description = "이메일", example = "abc@gmail.com")
private String email;
@Schema(description = "닉네임", example = "무한이")
private String nickname;
@Schema(description = "역할", example = "MANAGER")
private Role role;

public static ManagerSignupResponseDto fromEntity(User user) {
return ManagerSignupResponseDto.builder()
.id(user.getId())
.email(user.getEmail())
.nickname(user.getNickname())
.role(user.getRole())
.build();

}
}
2 changes: 1 addition & 1 deletion src/main/java/com/example/gtable/user/entity/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@RequiredArgsConstructor
public enum Role {
USER("USER"),
ADMIN("ADMIN");
MANAGER("MANAGER");

private final String name;

Expand Down
10 changes: 9 additions & 1 deletion src/main/java/com/example/gtable/user/entity/User.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.example.gtable.user.entity;

import org.springframework.security.crypto.password.PasswordEncoder;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand All @@ -24,6 +26,8 @@ public class User {
@Column(nullable = false, unique = true)
private String email; // 카카오 이메일

private String password; // 관리자 패스워드
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

password 필드에 대한 데이터베이스 제약조건 고려가 필요합니다

관리자 계정의 password 필드는 보안상 중요한 데이터입니다. 현재 어떤 제약조건도 없이 선언되어 있는데, 다음 사항들을 고려해보시는 것이 좋겠습니다:

  • @Column(nullable = false) 어노테이션 추가로 NULL 값 방지
  • 소셜 로그인 사용자와 관리자 계정을 구분하기 위한 설계 검토
-    private String password; // 관리자 패스워드
+    @Column(nullable = false)
+    private String password; // 관리자 패스워드
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/user/entity/User.java at line 29, the
password field lacks database constraints. Add the @Column(nullable = false)
annotation to prevent null values for security. Additionally, review the design
to distinguish between social login users and admin accounts, possibly by adding
a field or mechanism to differentiate user types.


@Column(nullable = false)
private String nickname;

Expand All @@ -37,8 +41,9 @@ public class User {
private Role role;

@Builder
public User(String email, String nickname, String profileImage, SocialType socialType, Role role){
public User(String email,String password, String nickname, String profileImage, SocialType socialType, Role role){
this.email = email;
this.password = password;
this.nickname = nickname;
this.profileImage = profileImage;
this.socialType = socialType;
Expand All @@ -62,4 +67,7 @@ public static User createUserWithId(Long userId, String email, String nickname,
public void updateNickname(String nickname){
this.nickname = nickname;
}
public void encodePassword(PasswordEncoder passwordEncoder) {
password = passwordEncoder.encode(password);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByNickname(String nickName);
}
30 changes: 30 additions & 0 deletions src/main/java/com/example/gtable/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
package com.example.gtable.user.service;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.gtable.user.dto.ManagerSignupRequestDto;
import com.example.gtable.user.dto.ManagerSignupResponseDto;
import com.example.gtable.user.entity.User;
import com.example.gtable.user.repository.UserRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Transactional
public ManagerSignupResponseDto signup(ManagerSignupRequestDto managerSignupRequestDto) {
validateEmailDuplicated(managerSignupRequestDto);
validateNickNameDuplicated(managerSignupRequestDto.getNickname());
User user = managerSignupRequestDto.toEntity();
user.encodePassword(passwordEncoder);

return ManagerSignupResponseDto.fromEntity(userRepository.save(user));

}
private void validateEmailDuplicated(ManagerSignupRequestDto managerSignupRequestDto) {
userRepository.findByEmail(managerSignupRequestDto.getEmail()).ifPresent(member -> {
throw new IllegalArgumentException();
}
);
}
private void validateNickNameDuplicated(String nickName) {
userRepository.findByNickname(nickName).ifPresent(member -> {
throw new IllegalArgumentException();
}
);
}
Comment on lines +32 to +43
Copy link

@coderabbitai coderabbitai bot Jun 6, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

중복 검증 로직에서 예외 메시지 개선이 필요합니다

현재 IllegalArgumentException을 메시지 없이 던지고 있어서, 클라이언트에서 구체적인 오류 원인을 파악하기 어렵습니다. GlobalExceptionHandler에서 처리하더라도 명확한 메시지가 있으면 더 좋겠습니다.

 private void validateEmailDuplicated(ManagerSignupRequestDto managerSignupRequestDto) {
     userRepository.findByEmail(managerSignupRequestDto.getEmail()).ifPresent(member -> {
-            throw new IllegalArgumentException();
+            throw new IllegalArgumentException("이미 사용 중인 이메일입니다: " + managerSignupRequestDto.getEmail());
         }
     );
 }
 private void validateNickNameDuplicated(String nickName) {
     userRepository.findByNickname(nickName).ifPresent(member -> {
-            throw new IllegalArgumentException();
+            throw new IllegalArgumentException("이미 사용 중인 닉네임입니다: " + nickName);
         }
     );
 }

이렇게 하면 API 응답에서도 더 명확한 오류 메시지를 제공할 수 있습니다.

📝 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
private void validateEmailDuplicated(ManagerSignupRequestDto managerSignupRequestDto) {
userRepository.findByEmail(managerSignupRequestDto.getEmail()).ifPresent(member -> {
throw new IllegalArgumentException();
}
);
}
private void validateNickNameDuplicated(String nickName) {
userRepository.findByNickname(nickName).ifPresent(member -> {
throw new IllegalArgumentException();
}
);
}
private void validateEmailDuplicated(ManagerSignupRequestDto managerSignupRequestDto) {
userRepository.findByEmail(managerSignupRequestDto.getEmail()).ifPresent(member -> {
throw new IllegalArgumentException("이미 사용 중인 이메일입니다: " + managerSignupRequestDto.getEmail());
}
);
}
private void validateNickNameDuplicated(String nickName) {
userRepository.findByNickname(nickName).ifPresent(member -> {
throw new IllegalArgumentException("이미 사용 중인 닉네임입니다: " + nickName);
}
);
}
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/user/service/UserService.java around lines
32 to 43, the IllegalArgumentException thrown in validateEmailDuplicated and
validateNickNameDuplicated methods lacks descriptive messages. Update these
exceptions to include clear, specific error messages indicating that the email
or nickname is already in use, so clients and logs can better understand the
cause of the error.

Copy link
Member Author

Choose a reason for hiding this comment

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

Exception 처리 관련 정책이 명확하게 결정되면 수정 예정

Copy link

Choose a reason for hiding this comment

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

@HyemIin 좋은 접근 방식이네요! 예외 처리 정책을 먼저 수립한 후 일관성 있게 적용하는 것이 코드베이스 전체의 품질과 유지보수성에 더 도움이 될 것 같습니다. 정책이 결정되면 그때 함께 개선해보시면 되겠습니다. 👍

}
Loading