From 9913e7fda96bed6ded7489554979d745dbc94060 Mon Sep 17 00:00:00 2001 From: Wi Seungjae <132977754+discipline24@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:39:05 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[BE]=20[REFACTOR]=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81,=20oauth2=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 1 - .../{ => oauth2}/GithubServiceImpl.java | 3 +- .../{ => oauth2}/GoogleServiceImpl.java | 3 +- .../{ => oauth2}/KakaoServiceImpl.java | 3 +- .../service/{ => oauth2}/Oauth2Service.java | 2 +- .../{ => oauth2}/OauthStateService.java | 2 +- .../{ => oauth2}/OauthStateServiceImpl.java | 6 +--- .../{ => oauth2}/OauthUnlinkService.java | 2 +- .../{ => oauth2}/OauthUnlinkServiceImpl.java | 2 +- .../config/PrimaryDataSourceConfig.java | 2 +- .../sejongisc/backend/user/entity/Gender.java | 6 ++++ .../sejongisc/backend/user/entity/User.java | 35 +++++++++++++++++++ .../backend/user/entity/UserStatus.java | 17 +++++++++ .../backend/user/service/UserServiceImpl.java | 2 +- .../auth/controller/AuthControllerTest.java | 4 +-- .../auth/service/GithubServiceImplTest.java | 1 + .../auth/service/GoogleServiceImplTest.java | 1 + .../auth/service/KakaoServiceImplTest.java | 2 +- .../service/OauthUnlinkServiceImplTest.java | 1 + 19 files changed, 74 insertions(+), 21 deletions(-) rename backend/src/main/java/org/sejongisc/backend/auth/service/{ => oauth2}/GithubServiceImpl.java (97%) rename backend/src/main/java/org/sejongisc/backend/auth/service/{ => oauth2}/GoogleServiceImpl.java (98%) rename backend/src/main/java/org/sejongisc/backend/auth/service/{ => oauth2}/KakaoServiceImpl.java (98%) rename backend/src/main/java/org/sejongisc/backend/auth/service/{ => oauth2}/Oauth2Service.java (88%) rename backend/src/main/java/org/sejongisc/backend/auth/service/{ => oauth2}/OauthStateService.java (82%) rename backend/src/main/java/org/sejongisc/backend/auth/service/{ => oauth2}/OauthStateServiceImpl.java (82%) rename backend/src/main/java/org/sejongisc/backend/auth/service/{ => oauth2}/OauthUnlinkService.java (91%) rename backend/src/main/java/org/sejongisc/backend/auth/service/{ => oauth2}/OauthUnlinkServiceImpl.java (98%) create mode 100644 backend/src/main/java/org/sejongisc/backend/user/entity/Gender.java create mode 100644 backend/src/main/java/org/sejongisc/backend/user/entity/UserStatus.java diff --git a/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java b/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java index 2b03db9d..85165706 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java @@ -49,7 +49,6 @@ public class AuthController { private final LoginService loginService; private final UserService userService; - private final JwtProvider jwtProvider; private final RefreshTokenService refreshTokenService; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/GithubServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java similarity index 97% rename from backend/src/main/java/org/sejongisc/backend/auth/service/GithubServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java index 8e5ccfc1..df5c1bed 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/GithubServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.auth.service.oauth2; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.auth.dto.GithubTokenResponse; @@ -9,7 +9,6 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/GoogleServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java similarity index 98% rename from backend/src/main/java/org/sejongisc/backend/auth/service/GoogleServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java index aa4d02f5..e6d189bb 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/GoogleServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java @@ -1,6 +1,5 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.auth.service.oauth2; -import io.netty.handler.codec.http.HttpHeaderValues; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.auth.dto.GoogleTokenResponse; import org.sejongisc.backend.auth.dto.GoogleUserInfoResponse; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/KakaoServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java similarity index 98% rename from backend/src/main/java/org/sejongisc/backend/auth/service/KakaoServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java index 58158481..c55a8035 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/KakaoServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.auth.service.oauth2; import io.netty.handler.codec.http.HttpHeaderValues; import lombok.extern.slf4j.Slf4j; @@ -9,7 +9,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/Oauth2Service.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/Oauth2Service.java similarity index 88% rename from backend/src/main/java/org/sejongisc/backend/auth/service/Oauth2Service.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/Oauth2Service.java index f7b5ed5a..d634c87a 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/Oauth2Service.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/Oauth2Service.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.auth.service.oauth2; public interface Oauth2Service { diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/OauthStateService.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateService.java similarity index 82% rename from backend/src/main/java/org/sejongisc/backend/auth/service/OauthStateService.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateService.java index ccfa2e27..9b889354 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/OauthStateService.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateService.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.auth.service.oauth2; import jakarta.servlet.http.HttpSession; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/OauthStateServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateServiceImpl.java similarity index 82% rename from backend/src/main/java/org/sejongisc/backend/auth/service/OauthStateServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateServiceImpl.java index 9c27eea8..607fae7b 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/OauthStateServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateServiceImpl.java @@ -1,13 +1,9 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.auth.service.oauth2; import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Service; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; @Service public class OauthStateServiceImpl implements OauthStateService { diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/OauthUnlinkService.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkService.java similarity index 91% rename from backend/src/main/java/org/sejongisc/backend/auth/service/OauthUnlinkService.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkService.java index cd66e859..99cf852d 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/OauthUnlinkService.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkService.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.auth.service.oauth2; public interface OauthUnlinkService { diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java similarity index 98% rename from backend/src/main/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java index 6569a738..23eeb433 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.auth.service.oauth2; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java index 2d0fbf72..88304b14 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java @@ -73,7 +73,7 @@ public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory( Map jpaProps = new HashMap<>(); jpaProps.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); - // ddl-auto는 yml로 관리 권장 + jpaProps.put("hibernate.hbm2ddl.auto", "update"); return builder diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/Gender.java b/backend/src/main/java/org/sejongisc/backend/user/entity/Gender.java new file mode 100644 index 00000000..5c1da2ee --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/Gender.java @@ -0,0 +1,6 @@ +package org.sejongisc.backend.user.entity; + +public enum Gender { + MALE, + FEMALE +} diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java index ca9cf537..441289a6 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java @@ -36,13 +36,36 @@ public class User extends BasePostgresEntity{ @Column(nullable = false) private String name; + @Column(name = "student_id", unique = true, nullable = false) + private String studentId; // 학번: 엑셀 매칭 및 계정 식별의 핵심 키 + @Column(name = "phone_number") private String phoneNumber; + // --- 엑셀 장부 기반 추가 데이터 --- + private String college; // 단과대학 + private String department; // 학과 + private Integer generation; // 기수 (처음 활동한 연도 기준) + private String teamName; // 활동팀 (예: 매크로팀, 리서치팀) + + @Enumerated(EnumType.STRING) + private Gender gender; // 성별 + + @Column(nullable = false) + private boolean isNewMember; // 신규 여부 (포인트나 이벤트 대상자 선정용) + @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private UserStatus status = UserStatus.ACTIVE; // 활동 상태 (ACTIVE, INACTIVE, GRADUATED, OUT 등) + // 새 장부 업로드 시: 기존에 ACTIVE한 모든 인원을 INACTIVE로 일괄 업데이트 + // 새 엑셀에 있는 studentId을 대조하여, 명단에 있는 사람만 다시 ACTIVE로 바꾸고 + // generation(기수)과 positionName(직위)을 최신화 + @Column(columnDefinition = "integer default 0",nullable = false) private Integer point; @@ -56,6 +79,18 @@ public class User extends BasePostgresEntity{ @JsonIgnore private List oauthAccounts = new ArrayList<>(); + @Column(name = "position_name") // 엑셀의 '직위' 컬럼 데이터 그대로 저장 + private String positionName; + + // 권한 확인용 편의 메서드 + public boolean isManagerPosition() { + if (this.positionName == null) return false; + // 직위에 '팀장', '대표', '부대표' 등의 키워드가 있으면 운영진 권한 부여 후보 + return this.positionName.contains("팀장") || + this.positionName.contains("대표") || + this.positionName.contains("회장"); + } + // 기본값 지정 @PrePersist public void prePersist() { diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/UserStatus.java b/backend/src/main/java/org/sejongisc/backend/user/entity/UserStatus.java new file mode 100644 index 00000000..65a8d51c --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/UserStatus.java @@ -0,0 +1,17 @@ +package org.sejongisc.backend.user.entity; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UserStatus { + ACTIVE("활동 중"), + INACTIVE("활동 중지"), + GRADUATED("졸업생"), + OUT("탈퇴"), + PENDING("승인 대기"); + + private final String description; // 한글 명칭 +} diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java index 1356784b..1983ba2d 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java @@ -3,7 +3,7 @@ import org.sejongisc.backend.auth.entity.AuthProvider; import org.sejongisc.backend.auth.service.EmailService; -import org.sejongisc.backend.auth.service.OauthUnlinkService; +import org.sejongisc.backend.auth.service.oauth2.OauthUnlinkService; import org.sejongisc.backend.auth.service.RefreshTokenService; import org.sejongisc.backend.common.annotation.OptimisticRetry; import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java index d75008f9..c7d07e63 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java @@ -13,8 +13,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.sejongisc.backend.auth.dto.*; import org.sejongisc.backend.auth.service.LoginService; -import org.sejongisc.backend.auth.service.Oauth2Service; -import org.sejongisc.backend.auth.service.OauthStateService; +import org.sejongisc.backend.auth.service.oauth2.Oauth2Service; +import org.sejongisc.backend.auth.service.oauth2.OauthStateService; import org.sejongisc.backend.auth.service.RefreshTokenService; import org.sejongisc.backend.common.auth.jwt.JwtProvider; import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java index 610c73cf..7d72aa71 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.sejongisc.backend.auth.dto.GithubTokenResponse; import org.sejongisc.backend.auth.dto.GithubUserInfoResponse; +import org.sejongisc.backend.auth.service.oauth2.GithubServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java index 6fc2ef9b..a81dfc9a 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.sejongisc.backend.auth.dto.GoogleTokenResponse; import org.sejongisc.backend.auth.dto.GoogleUserInfoResponse; +import org.sejongisc.backend.auth.service.oauth2.GoogleServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java index 24444f4f..8f4a5b41 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java @@ -6,8 +6,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.sejongisc.backend.auth.dto.KakaoTokenResponse; import org.sejongisc.backend.auth.dto.KakaoUserInfoResponse; +import org.sejongisc.backend.auth.service.oauth2.KakaoServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImplTest.java index 7129473e..ec54a61f 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImplTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.*; +import org.sejongisc.backend.auth.service.oauth2.OauthUnlinkServiceImpl; import org.springframework.http.*; import org.springframework.web.client.RestTemplate; From 87a09566ec116c52a821e85cfc90ccaa0ea30048 Mon Sep 17 00:00:00 2001 From: Wi Seungjae <132977754+discipline24@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:44:35 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[BE]=20[REFACTOR]=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attendance/service/AttendanceService.java | 2 +- .../service/AttendanceSessionService.java | 2 +- .../attendance/service/SessionUserService.java | 2 +- .../backend/auth/controller/AuthController.java | 14 -------------- .../auth/dto/{ => oauth}/GithubTokenResponse.java | 2 +- .../{ => dto}/oauth/GithubUserInfoAdapter.java | 3 +-- .../dto/{ => oauth}/GithubUserInfoResponse.java | 2 +- .../auth/dto/{ => oauth}/GoogleTokenResponse.java | 2 +- .../{ => dto}/oauth/GoogleUserInfoAdapter.java | 3 +-- .../dto/{ => oauth}/GoogleUserInfoResponse.java | 2 +- .../auth/dto/{ => oauth}/KakaoTokenResponse.java | 2 +- .../auth/{ => dto}/oauth/KakaoUserInfoAdapter.java | 3 +-- .../dto/{ => oauth}/KakaoUserInfoResponse.java | 2 +- .../auth/{ => dto}/oauth/OauthUserInfo.java | 2 +- .../UserOauthAccountRepository.java | 2 +- .../backend/auth/service/EmailService.java | 3 +-- .../backend/auth/service/LoginServiceImpl.java | 7 +------ .../auth/service/RefreshTokenServiceImpl.java | 2 +- .../auth/service/oauth2/GithubServiceImpl.java | 4 ++-- .../auth/service/oauth2/GoogleServiceImpl.java | 4 ++-- .../auth/service/oauth2/KakaoServiceImpl.java | 4 ++-- .../service/oauth2/OauthUnlinkServiceImpl.java | 2 +- .../oauth2}/exception/OauthUnlinkException.java | 2 +- .../backend/backtest/service/BacktestService.java | 3 +-- .../board/service/PostInteractionService.java | 2 +- .../backend/board/service/PostServiceImpl.java | 2 +- .../auth/config/CustomOAuth2UserService.java | 5 ++--- .../common/auth/config/CustomOidcUserService.java | 6 ++---- .../common/auth/config/OAuth2SuccessHandler.java | 12 ++---------- .../springsecurity/CustomUserDetailsService.java | 2 +- .../backend/template/service/TemplateService.java | 3 +-- .../user/{dao => repository}/UserRepository.java | 2 +- .../backend/user/service/UserService.java | 2 +- .../backend/user/service/UserServiceImpl.java | 6 +++--- .../service/AttendanceRoundCheckInTest.java | 13 +------------ .../attendance/service/AttendanceServiceTest.java | 12 +----------- .../auth/controller/AuthControllerTest.java | 1 + .../backend/auth/service/EmailServiceTest.java | 2 +- .../auth/service/GithubServiceImplTest.java | 4 ++-- .../auth/service/GoogleServiceImplTest.java | 4 ++-- .../backend/auth/service/KakaoServiceImplTest.java | 2 +- .../backend/auth/service/LoginServiceImplTest.java | 5 +---- .../auth/service/RefreshTokenServiceImplTest.java | 2 +- .../service/BettingServiceTransactionalTest.java | 2 +- .../board/service/PostInteractionServiceTest.java | 2 +- .../backend/board/service/PostServiceImplTest.java | 2 +- .../template/service/TemplateServiceTest.java | 2 +- .../backend/user/service/UserServiceImplTest.java | 6 +++--- 48 files changed, 59 insertions(+), 118 deletions(-) rename backend/src/main/java/org/sejongisc/backend/auth/dto/{ => oauth}/GithubTokenResponse.java (95%) rename backend/src/main/java/org/sejongisc/backend/auth/{ => dto}/oauth/GithubUserInfoAdapter.java (89%) rename backend/src/main/java/org/sejongisc/backend/auth/dto/{ => oauth}/GithubUserInfoResponse.java (96%) rename backend/src/main/java/org/sejongisc/backend/auth/dto/{ => oauth}/GoogleTokenResponse.java (97%) rename backend/src/main/java/org/sejongisc/backend/auth/{ => dto}/oauth/GoogleUserInfoAdapter.java (88%) rename backend/src/main/java/org/sejongisc/backend/auth/dto/{ => oauth}/GoogleUserInfoResponse.java (96%) rename backend/src/main/java/org/sejongisc/backend/auth/dto/{ => oauth}/KakaoTokenResponse.java (97%) rename backend/src/main/java/org/sejongisc/backend/auth/{ => dto}/oauth/KakaoUserInfoAdapter.java (91%) rename backend/src/main/java/org/sejongisc/backend/auth/dto/{ => oauth}/KakaoUserInfoResponse.java (99%) rename backend/src/main/java/org/sejongisc/backend/auth/{ => dto}/oauth/OauthUserInfo.java (85%) rename backend/src/main/java/org/sejongisc/backend/auth/{dao => repository}/UserOauthAccountRepository.java (92%) rename backend/src/main/java/org/sejongisc/backend/auth/{ => service/oauth2}/exception/OauthUnlinkException.java (80%) rename backend/src/main/java/org/sejongisc/backend/user/{dao => repository}/UserRepository.java (95%) diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java index 8953a050..6aa5571b 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java @@ -15,7 +15,7 @@ import org.sejongisc.backend.attendance.repository.AttendanceRoundRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java index ea66373e..e8ac7bd2 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java @@ -12,7 +12,7 @@ import org.sejongisc.backend.attendance.repository.SessionUserRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java index 8ad8e6fb..a3cc43c4 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java @@ -13,7 +13,7 @@ import org.sejongisc.backend.attendance.repository.SessionUserRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java b/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java index 85165706..85bfcecb 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java @@ -5,26 +5,15 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.auth.dto.*; -import org.sejongisc.backend.auth.repository.RefreshTokenRepository; import org.sejongisc.backend.auth.service.*; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; -import org.sejongisc.backend.common.exception.CustomException; -import org.sejongisc.backend.user.entity.User; -import org.sejongisc.backend.auth.oauth.GithubUserInfoAdapter; -import org.sejongisc.backend.auth.oauth.GoogleUserInfoAdapter; -import org.sejongisc.backend.auth.oauth.KakaoUserInfoAdapter; import org.sejongisc.backend.user.service.UserService; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -32,9 +21,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.Map; @Slf4j diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/GithubTokenResponse.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubTokenResponse.java similarity index 95% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/GithubTokenResponse.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubTokenResponse.java index cdebc9a8..a1d3f816 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/GithubTokenResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubTokenResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/oauth/GithubUserInfoAdapter.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoAdapter.java similarity index 89% rename from backend/src/main/java/org/sejongisc/backend/auth/oauth/GithubUserInfoAdapter.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoAdapter.java index f33eb4c6..a9e8bb25 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/oauth/GithubUserInfoAdapter.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoAdapter.java @@ -1,6 +1,5 @@ -package org.sejongisc.backend.auth.oauth; +package org.sejongisc.backend.auth.dto.oauth; -import org.sejongisc.backend.auth.dto.GithubUserInfoResponse; import org.sejongisc.backend.auth.entity.AuthProvider; import java.util.Optional; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/GithubUserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoResponse.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/GithubUserInfoResponse.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoResponse.java index 74af77fd..2a95bd32 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/GithubUserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/GoogleTokenResponse.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleTokenResponse.java similarity index 97% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/GoogleTokenResponse.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleTokenResponse.java index a286adec..aa6dec76 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/GoogleTokenResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleTokenResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/oauth/GoogleUserInfoAdapter.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoAdapter.java similarity index 88% rename from backend/src/main/java/org/sejongisc/backend/auth/oauth/GoogleUserInfoAdapter.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoAdapter.java index 286d4b68..5e2bf598 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/oauth/GoogleUserInfoAdapter.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoAdapter.java @@ -1,6 +1,5 @@ -package org.sejongisc.backend.auth.oauth; +package org.sejongisc.backend.auth.dto.oauth; -import org.sejongisc.backend.auth.dto.GoogleUserInfoResponse; import org.sejongisc.backend.auth.entity.AuthProvider; import java.util.Optional; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/GoogleUserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoResponse.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/GoogleUserInfoResponse.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoResponse.java index ad3a0bcf..785ff751 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/GoogleUserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/KakaoTokenResponse.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoTokenResponse.java similarity index 97% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/KakaoTokenResponse.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoTokenResponse.java index ec0f8aca..2e855c97 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/KakaoTokenResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoTokenResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/oauth/KakaoUserInfoAdapter.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoAdapter.java similarity index 91% rename from backend/src/main/java/org/sejongisc/backend/auth/oauth/KakaoUserInfoAdapter.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoAdapter.java index 642e77fa..0827ec57 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/oauth/KakaoUserInfoAdapter.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoAdapter.java @@ -1,6 +1,5 @@ -package org.sejongisc.backend.auth.oauth; +package org.sejongisc.backend.auth.dto.oauth; -import org.sejongisc.backend.auth.dto.KakaoUserInfoResponse; import org.sejongisc.backend.auth.entity.AuthProvider; import java.util.Optional; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/KakaoUserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoResponse.java similarity index 99% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/KakaoUserInfoResponse.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoResponse.java index b4bb2ddc..3bc672b6 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/KakaoUserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/oauth/OauthUserInfo.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/OauthUserInfo.java similarity index 85% rename from backend/src/main/java/org/sejongisc/backend/auth/oauth/OauthUserInfo.java rename to backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/OauthUserInfo.java index 849bdd18..4fd0e7dd 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/oauth/OauthUserInfo.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/OauthUserInfo.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.oauth; +package org.sejongisc.backend.auth.dto.oauth; import org.sejongisc.backend.auth.entity.AuthProvider; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dao/UserOauthAccountRepository.java b/backend/src/main/java/org/sejongisc/backend/auth/repository/UserOauthAccountRepository.java similarity index 92% rename from backend/src/main/java/org/sejongisc/backend/auth/dao/UserOauthAccountRepository.java rename to backend/src/main/java/org/sejongisc/backend/auth/repository/UserOauthAccountRepository.java index 21d92029..f29e4e6d 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dao/UserOauthAccountRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/repository/UserOauthAccountRepository.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dao; +package org.sejongisc.backend.auth.repository; import org.sejongisc.backend.auth.entity.AuthProvider; import org.sejongisc.backend.auth.entity.UserOauthAccount; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/EmailService.java b/backend/src/main/java/org/sejongisc/backend/auth/service/EmailService.java index 699902f2..9a937e2d 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/EmailService.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/EmailService.java @@ -12,12 +12,11 @@ import org.sejongisc.backend.auth.config.EmailProperties; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import org.thymeleaf.context.Context; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/LoginServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/LoginServiceImpl.java index 1ab7d953..a36243ab 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/LoginServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/LoginServiceImpl.java @@ -1,25 +1,20 @@ package org.sejongisc.backend.auth.service; -import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.auth.repository.RefreshTokenRepository; import org.sejongisc.backend.common.auth.jwt.JwtParser; import org.sejongisc.backend.common.auth.jwt.JwtProvider; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.auth.dto.LoginRequest; import org.sejongisc.backend.auth.dto.LoginResponse; import org.sejongisc.backend.user.entity.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.sql.Ref; import java.util.UUID; @Slf4j diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImpl.java index a069f7e6..1e1452f6 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImpl.java @@ -9,7 +9,7 @@ import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java index df5c1bed..e48a1ed8 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java @@ -1,8 +1,8 @@ package org.sejongisc.backend.auth.service.oauth2; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dto.GithubTokenResponse; -import org.sejongisc.backend.auth.dto.GithubUserInfoResponse; +import org.sejongisc.backend.auth.dto.oauth.GithubTokenResponse; +import org.sejongisc.backend.auth.dto.oauth.GithubUserInfoResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java index e6d189bb..bdfaa476 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java @@ -1,8 +1,8 @@ package org.sejongisc.backend.auth.service.oauth2; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dto.GoogleTokenResponse; -import org.sejongisc.backend.auth.dto.GoogleUserInfoResponse; +import org.sejongisc.backend.auth.dto.oauth.GoogleTokenResponse; +import org.sejongisc.backend.auth.dto.oauth.GoogleUserInfoResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java index c55a8035..68442e6c 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java @@ -2,8 +2,8 @@ import io.netty.handler.codec.http.HttpHeaderValues; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dto.KakaoTokenResponse; -import org.sejongisc.backend.auth.dto.KakaoUserInfoResponse; +import org.sejongisc.backend.auth.dto.oauth.KakaoTokenResponse; +import org.sejongisc.backend.auth.dto.oauth.KakaoUserInfoResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java index 23eeb433..9721eea1 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.exception.OauthUnlinkException; +import org.sejongisc.backend.auth.service.oauth2.exception.OauthUnlinkException; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/exception/OauthUnlinkException.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/exception/OauthUnlinkException.java similarity index 80% rename from backend/src/main/java/org/sejongisc/backend/auth/exception/OauthUnlinkException.java rename to backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/exception/OauthUnlinkException.java index af9908b1..13cd1ee8 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/exception/OauthUnlinkException.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/exception/OauthUnlinkException.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.exception; +package org.sejongisc.backend.auth.service.oauth2.exception; public class OauthUnlinkException extends RuntimeException { public OauthUnlinkException(String message) { diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java b/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java index 968b2d61..2994a92e 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java @@ -1,7 +1,6 @@ package org.sejongisc.backend.backtest.service; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.backtest.dto.BacktestRequest; @@ -17,7 +16,7 @@ import org.sejongisc.backend.stock.repository.PriceDataRepository; import org.sejongisc.backend.template.entity.Template; import org.sejongisc.backend.template.repository.TemplateRepository; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java b/backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java index 4bd0862b..5e264b07 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java +++ b/backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java @@ -16,7 +16,7 @@ import org.sejongisc.backend.board.repository.PostRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.springframework.orm.ObjectOptimisticLockingFailureException; diff --git a/backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java index 1e90384c..382d8f91 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java @@ -23,7 +23,7 @@ import org.sejongisc.backend.board.repository.PostRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.dto.UserInfoResponse; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java index 3ad7350c..16cbebca 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java @@ -3,13 +3,12 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; +import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; import org.sejongisc.backend.auth.entity.AuthProvider; import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; -import org.sejongisc.backend.user.service.UserServiceImpl; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java index 46167f55..02fd4ffe 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java @@ -3,13 +3,12 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; +import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; import org.sejongisc.backend.auth.entity.AuthProvider; import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -17,7 +16,6 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Service; -import java.util.Collections; import java.util.HashMap; import java.util.Map; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java index baa3c490..fac6fc96 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java @@ -1,37 +1,29 @@ package org.sejongisc.backend.common.auth.config; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.env.Environment; -import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; +import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; import org.sejongisc.backend.auth.entity.AuthProvider; import org.sejongisc.backend.auth.entity.UserOauthAccount; import org.sejongisc.backend.common.auth.jwt.JwtProvider; import org.sejongisc.backend.auth.service.RefreshTokenService; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.UUID; @Slf4j @Component diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java index 93ba2658..50fa314c 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; diff --git a/backend/src/main/java/org/sejongisc/backend/template/service/TemplateService.java b/backend/src/main/java/org/sejongisc/backend/template/service/TemplateService.java index 66d10e5f..5000bddd 100644 --- a/backend/src/main/java/org/sejongisc/backend/template/service/TemplateService.java +++ b/backend/src/main/java/org/sejongisc/backend/template/service/TemplateService.java @@ -1,7 +1,6 @@ package org.sejongisc.backend.template.service; -import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.backtest.repository.BacktestRunRepository; @@ -11,7 +10,7 @@ import org.sejongisc.backend.template.dto.TemplateResponse; import org.sejongisc.backend.template.entity.Template; import org.sejongisc.backend.template.repository.TemplateRepository; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/org/sejongisc/backend/user/dao/UserRepository.java b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java similarity index 95% rename from backend/src/main/java/org/sejongisc/backend/user/dao/UserRepository.java rename to backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java index 79bd620a..f50fb81d 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dao/UserRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.user.dao; +package org.sejongisc.backend.user.repository; import org.sejongisc.backend.user.entity.User; import org.sejongisc.backend.user.service.projection.UserIdNameProjection; diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java index 03bd6b7a..344f5a80 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java @@ -4,7 +4,7 @@ import org.sejongisc.backend.auth.dto.SignupResponse; import org.sejongisc.backend.user.dto.UserUpdateRequest; import org.sejongisc.backend.user.entity.User; -import org.sejongisc.backend.auth.oauth.OauthUserInfo; +import org.sejongisc.backend.auth.dto.oauth.OauthUserInfo; import org.sejongisc.backend.user.service.projection.UserIdNameProjection; import java.util.List; diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java index 1983ba2d..07bc65b8 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java @@ -21,15 +21,15 @@ import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.auth.dto.SignupRequest; import org.sejongisc.backend.auth.dto.SignupResponse; import org.sejongisc.backend.user.dto.UserUpdateRequest; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.auth.oauth.OauthUserInfo; +import org.sejongisc.backend.auth.dto.oauth.OauthUserInfo; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceRoundCheckInTest.java b/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceRoundCheckInTest.java index 763c22d4..a716dd62 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceRoundCheckInTest.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceRoundCheckInTest.java @@ -1,7 +1,5 @@ package org.sejongisc.backend.attendance.service; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -9,20 +7,11 @@ import org.sejongisc.backend.attendance.repository.AttendanceRepository; import org.sejongisc.backend.attendance.repository.AttendanceRoundRepository; import org.sejongisc.backend.attendance.repository.AttendanceSessionRepository; -import org.sejongisc.backend.user.dao.UserRepository; -import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.repository.UserRepository; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.Optional; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class AttendanceRoundCheckInTest { diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceServiceTest.java b/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceServiceTest.java index e5f48360..d9719910 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceServiceTest.java @@ -1,24 +1,14 @@ package org.sejongisc.backend.attendance.service; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.sejongisc.backend.attendance.entity.*; import org.sejongisc.backend.attendance.repository.AttendanceRepository; import org.sejongisc.backend.attendance.repository.AttendanceSessionRepository; -import org.sejongisc.backend.user.dao.UserRepository; -import org.sejongisc.backend.user.entity.Role; -import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.repository.UserRepository; -import java.time.LocalDateTime; -import java.util.*; - -import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class AttendanceServiceTest { diff --git a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java index c7d07e63..63df5e3a 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java @@ -12,6 +12,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.sejongisc.backend.auth.dto.*; +import org.sejongisc.backend.auth.dto.oauth.*; import org.sejongisc.backend.auth.service.LoginService; import org.sejongisc.backend.auth.service.oauth2.Oauth2Service; import org.sejongisc.backend.auth.service.oauth2.OauthStateService; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/EmailServiceTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/EmailServiceTest.java index 9c5484bb..bfcd5912 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/EmailServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/EmailServiceTest.java @@ -18,7 +18,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.sejongisc.backend.auth.config.EmailProperties; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.mail.javamail.JavaMailSender; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java index 7d72aa71..47121486 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java @@ -6,8 +6,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.sejongisc.backend.auth.dto.GithubTokenResponse; -import org.sejongisc.backend.auth.dto.GithubUserInfoResponse; +import org.sejongisc.backend.auth.dto.oauth.GithubTokenResponse; +import org.sejongisc.backend.auth.dto.oauth.GithubUserInfoResponse; import org.sejongisc.backend.auth.service.oauth2.GithubServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java index a81dfc9a..e73b27bb 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java @@ -6,8 +6,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.sejongisc.backend.auth.dto.GoogleTokenResponse; -import org.sejongisc.backend.auth.dto.GoogleUserInfoResponse; +import org.sejongisc.backend.auth.dto.oauth.GoogleTokenResponse; +import org.sejongisc.backend.auth.dto.oauth.GoogleUserInfoResponse; import org.sejongisc.backend.auth.service.oauth2.GoogleServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java index 8f4a5b41..722df639 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.sejongisc.backend.auth.dto.KakaoUserInfoResponse; +import org.sejongisc.backend.auth.dto.oauth.KakaoUserInfoResponse; import org.sejongisc.backend.auth.service.oauth2.KakaoServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/LoginServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/LoginServiceImplTest.java index 318de394..cb4aacc1 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/LoginServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/LoginServiceImplTest.java @@ -11,13 +11,11 @@ import org.sejongisc.backend.common.auth.jwt.JwtProvider; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.auth.dto.LoginRequest; import org.sejongisc.backend.auth.dto.LoginResponse; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.Optional; @@ -26,7 +24,6 @@ import static org.mockito.Mockito.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java index 2454352f..c2b21ebb 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java @@ -11,7 +11,7 @@ import org.sejongisc.backend.common.auth.jwt.JwtProvider; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; diff --git a/backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java b/backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java index 2bb139ee..b641e01b 100644 --- a/backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java +++ b/backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java @@ -19,7 +19,7 @@ import org.sejongisc.backend.point.entity.PointHistory; import org.sejongisc.backend.point.repository.PointHistoryRepository; import org.sejongisc.backend.point.service.PointHistoryService; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; diff --git a/backend/src/test/java/org/sejongisc/backend/board/service/PostInteractionServiceTest.java b/backend/src/test/java/org/sejongisc/backend/board/service/PostInteractionServiceTest.java index b9ae930e..05049a32 100644 --- a/backend/src/test/java/org/sejongisc/backend/board/service/PostInteractionServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/board/service/PostInteractionServiceTest.java @@ -31,7 +31,7 @@ import org.sejongisc.backend.board.repository.PostRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; diff --git a/backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java index 8dd734c9..efd1f4fb 100644 --- a/backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java @@ -37,7 +37,7 @@ import org.sejongisc.backend.board.repository.PostRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.springframework.data.domain.Page; diff --git a/backend/src/test/java/org/sejongisc/backend/template/service/TemplateServiceTest.java b/backend/src/test/java/org/sejongisc/backend/template/service/TemplateServiceTest.java index 44e6ef15..ded275e1 100644 --- a/backend/src/test/java/org/sejongisc/backend/template/service/TemplateServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/template/service/TemplateServiceTest.java @@ -14,7 +14,7 @@ import org.sejongisc.backend.template.entity.Template; import org.sejongisc.backend.template.repository.TemplateRepository; import org.sejongisc.backend.user.entity.User; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.repository.UserRepository; import java.util.Optional; import java.util.UUID; diff --git a/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceImplTest.java index a18224ba..faa4a599 100644 --- a/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceImplTest.java @@ -20,8 +20,8 @@ import org.sejongisc.backend.auth.service.RefreshTokenService; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; -import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; +import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.auth.dto.SignupRequest; import org.sejongisc.backend.auth.dto.SignupResponse; import org.sejongisc.backend.auth.entity.AuthProvider; @@ -29,7 +29,7 @@ import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.auth.oauth.OauthUserInfo; +import org.sejongisc.backend.auth.dto.oauth.OauthUserInfo; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; From 0883949787f88501a5e84689c0fae03494a07dc2 Mon Sep 17 00:00:00 2001 From: Wi Seungjae <132977754+discipline24@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:48:47 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[BE]=20[REFACTOR]=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B0=8F=202=EC=B0=A8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AttendanceController.java | 2 +- .../controller/AttendanceRoundController.java | 2 +- .../AttendanceSessionController.java | 2 +- .../controller/SessionUserController.java | 2 +- .../backend/attendance/util/AuthUserUtil.java | 3 +- .../auth/controller/AuthController.java | 347 -------------- .../backend/auth/service/LoginService.java | 11 - .../service/oauth2/GithubServiceImpl.java | 103 ---- .../controller/BacktestController.java | 4 +- .../betting/controller/BettingController.java | 5 +- .../board/controller/BoardController.java | 3 +- .../auth/config/CustomOAuth2UserService.java | 117 ----- .../auth/config/CustomOidcUserService.java | 91 ---- .../auth/config/OAuth2SuccessHandler.java | 164 ------- .../auth/controller/AuthController.java | 87 ++++ .../auth/controller/AuthCookieHelper.java | 36 ++ .../auth/controller/EmailController.java | 5 +- .../CustomUserDetails.java | 4 +- .../{ => common}/auth/dto/LoginRequest.java | 2 +- .../{ => common}/auth/dto/LoginResponse.java | 8 +- .../{ => common}/auth/dto/SignupRequest.java | 2 +- .../{ => common}/auth/dto/SignupResponse.java | 2 +- .../auth/dto/oauth/GithubTokenResponse.java | 2 +- .../auth/dto/oauth/GithubUserInfoAdapter.java | 4 +- .../dto/oauth/GithubUserInfoResponse.java | 2 +- .../auth/dto/oauth/GoogleTokenResponse.java | 2 +- .../auth/dto/oauth/GoogleUserInfoAdapter.java | 4 +- .../dto/oauth/GoogleUserInfoResponse.java | 2 +- .../auth/dto/oauth/KakaoTokenResponse.java | 2 +- .../auth/dto/oauth/KakaoUserInfoAdapter.java | 4 +- .../auth/dto/oauth/KakaoUserInfoResponse.java | 2 +- .../auth/dto/oauth/OauthUserInfo.java | 4 +- .../auth/entity/AuthProvider.java | 2 +- .../auth/entity/RefreshToken.java | 2 +- .../auth/entity/UserOauthAccount.java | 2 +- .../JwtAuthenticationFilter.java | 3 +- .../backend/common/auth/jwt/JwtParser.java | 3 +- .../repository/RefreshTokenRepository.java | 4 +- .../UserOauthAccountRepository.java | 6 +- .../auth/service/AuthService.java} | 15 +- .../CustomUserDetailsService.java | 3 +- .../auth/service/EmailService.java | 4 +- .../auth/service/RefreshTokenService.java | 2 +- .../auth/service/RefreshTokenServiceImpl.java | 6 +- .../service/oauth2/GithubServiceImpl.java | 440 ++++++++++++++++++ .../service/oauth2/GoogleServiceImpl.java | 6 +- .../auth/service/oauth2/KakaoServiceImpl.java | 6 +- .../auth/service/oauth2/Oauth2Service.java | 2 +- .../service/oauth2/OauthStateService.java | 2 +- .../service/oauth2/OauthStateServiceImpl.java | 2 +- .../service/oauth2/OauthUnlinkService.java | 2 +- .../oauth2/OauthUnlinkServiceImpl.java | 4 +- .../exception/OauthUnlinkException.java | 2 +- .../config/EmailProperties.java | 2 +- .../{auth => }/config/OpenApiConfig.java | 2 +- .../config/PrimaryDataSourceConfig.java | 2 +- .../{auth => }/config/RestTemplateConfig.java | 2 +- .../{auth => }/config/SecurityConfig.java | 15 +- .../controller}/JwtAccessDeniedHandler.java | 2 +- .../JwtAuthenticationEntryPoint.java | 2 +- .../controller/PointHistoryController.java | 3 +- .../controller/TemplateController.java | 4 +- .../user/controller/UserController.java | 371 +++------------ .../backend/user/dto/UserInfoResponse.java | 3 +- .../sejongisc/backend/user/entity/User.java | 2 +- .../user/repository/UserRepository.java | 10 - .../backend/user/service/UserService.java | 279 ++++++++++- .../backend/user/service/UserServiceImpl.java | 397 ---------------- .../controller/AttendanceControllerTest.java | 17 - .../auth/controller/AuthControllerTest.java | 43 +- ...viceImplTest.java => AuthServiceTest.java} | 21 +- .../auth/service/EmailServiceTest.java | 3 +- .../auth/service/GithubServiceImplTest.java | 6 +- .../auth/service/GoogleServiceImplTest.java | 6 +- .../auth/service/KakaoServiceImplTest.java | 4 +- .../service/OauthUnlinkServiceImplTest.java | 2 +- .../service/RefreshTokenServiceImplTest.java | 5 +- .../controller/BacktestControllerTest.java | 2 +- .../controller/TemplateControllerTest.java | 13 +- .../user/controller/UserControllerTest.java | 8 +- ...viceImplTest.java => UserServiceTest.java} | 40 +- 81 files changed, 1053 insertions(+), 1761 deletions(-) delete mode 100644 backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java delete mode 100644 backend/src/main/java/org/sejongisc/backend/auth/service/LoginService.java delete mode 100644 backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java delete mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java delete mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java delete mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java create mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthController.java create mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthCookieHelper.java rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/controller/EmailController.java (93%) rename backend/src/main/java/org/sejongisc/backend/common/auth/{springsecurity => dto}/CustomUserDetails.java (96%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/LoginRequest.java (95%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/LoginResponse.java (84%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/SignupRequest.java (97%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/SignupResponse.java (97%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/GithubTokenResponse.java (95%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/GithubUserInfoAdapter.java (88%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/GithubUserInfoResponse.java (96%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/GoogleTokenResponse.java (97%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/GoogleUserInfoAdapter.java (87%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/GoogleUserInfoResponse.java (95%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/KakaoTokenResponse.java (97%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/KakaoUserInfoAdapter.java (90%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/KakaoUserInfoResponse.java (99%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/dto/oauth/OauthUserInfo.java (64%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/entity/AuthProvider.java (89%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/entity/RefreshToken.java (88%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/entity/UserOauthAccount.java (96%) rename backend/src/main/java/org/sejongisc/backend/common/auth/{springsecurity => filter}/JwtAuthenticationFilter.java (98%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/repository/RefreshTokenRepository.java (75%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/repository/UserOauthAccountRepository.java (70%) rename backend/src/main/java/org/sejongisc/backend/{auth/service/LoginServiceImpl.java => common/auth/service/AuthService.java} (87%) rename backend/src/main/java/org/sejongisc/backend/common/auth/{springsecurity => service}/CustomUserDetailsService.java (91%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/EmailService.java (98%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/RefreshTokenService.java (92%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/RefreshTokenServiceImpl.java (95%) create mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/GithubServiceImpl.java rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/oauth2/GoogleServiceImpl.java (96%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/oauth2/KakaoServiceImpl.java (96%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/oauth2/Oauth2Service.java (86%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/oauth2/OauthStateService.java (80%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/oauth2/OauthStateServiceImpl.java (92%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/oauth2/OauthUnlinkService.java (89%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/oauth2/OauthUnlinkServiceImpl.java (96%) rename backend/src/main/java/org/sejongisc/backend/{ => common}/auth/service/oauth2/exception/OauthUnlinkException.java (78%) rename backend/src/main/java/org/sejongisc/backend/{auth => common}/config/EmailProperties.java (93%) rename backend/src/main/java/org/sejongisc/backend/common/{auth => }/config/OpenApiConfig.java (91%) rename backend/src/main/java/org/sejongisc/backend/common/{auth => }/config/RestTemplateConfig.java (93%) rename backend/src/main/java/org/sejongisc/backend/common/{auth => }/config/SecurityConfig.java (91%) rename backend/src/main/java/org/sejongisc/backend/common/{auth/jwt => exception/controller}/JwtAccessDeniedHandler.java (95%) rename backend/src/main/java/org/sejongisc/backend/common/{auth/jwt => exception/controller}/JwtAuthenticationEntryPoint.java (95%) delete mode 100644 backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java rename backend/src/test/java/org/sejongisc/backend/auth/service/{LoginServiceImplTest.java => AuthServiceTest.java} (90%) rename backend/src/test/java/org/sejongisc/backend/user/service/{UserServiceImplTest.java => UserServiceTest.java} (94%) diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java index 52a37db0..7b5c0ee7 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java @@ -11,7 +11,7 @@ import org.sejongisc.backend.attendance.dto.AttendanceRoundQrTokenRequest; import org.sejongisc.backend.attendance.dto.AttendanceStatusUpdateRequest; import org.sejongisc.backend.attendance.service.AttendanceService; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java index 36d134bf..4389b761 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java @@ -12,7 +12,7 @@ import org.sejongisc.backend.attendance.dto.AttendanceRoundRequest; import org.sejongisc.backend.attendance.dto.AttendanceRoundResponse; import org.sejongisc.backend.attendance.service.AttendanceRoundService; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java index 5aa25f0e..3d8acbc9 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java @@ -11,7 +11,7 @@ import org.sejongisc.backend.attendance.dto.AttendanceSessionRequest; import org.sejongisc.backend.attendance.dto.AttendanceSessionResponse; import org.sejongisc.backend.attendance.service.AttendanceSessionService; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/SessionUserController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/SessionUserController.java index 24a9797b..47a1dfd2 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/SessionUserController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/SessionUserController.java @@ -10,7 +10,7 @@ import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.attendance.dto.SessionUserResponse; import org.sejongisc.backend.attendance.service.SessionUserService; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/util/AuthUserUtil.java b/backend/src/main/java/org/sejongisc/backend/attendance/util/AuthUserUtil.java index f7568677..8f4d2cb6 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/util/AuthUserUtil.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/util/AuthUserUtil.java @@ -1,7 +1,8 @@ package org.sejongisc.backend.attendance.util; import java.util.UUID; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; + +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; public class AuthUserUtil { private AuthUserUtil() {} diff --git a/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java b/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java deleted file mode 100644 index 85bfcecb..00000000 --- a/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java +++ /dev/null @@ -1,347 +0,0 @@ -package org.sejongisc.backend.auth.controller; - -import io.jsonwebtoken.JwtException; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dto.*; -import org.sejongisc.backend.auth.service.*; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; -import org.sejongisc.backend.user.service.UserService; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseCookie; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -@Slf4j -@RestController -@RequestMapping("/api/auth") -@RequiredArgsConstructor -@Tag( - name = "인증 API", - description = "회원 인증 및 소셜 로그인 관련 API를 제공합니다." -) -public class AuthController { - - private final LoginService loginService; - private final UserService userService; - private final RefreshTokenService refreshTokenService; - - - @Operation( - summary = "회원가입 API", - description = """ - 회원 이메일, 비밀번호, 이름, 전화번호 정보를 입력받아 새로운 사용자를 생성합니다. - - 비밀번호 정책: - - 길이: 8~20자 - - 최소 1개의 대문자(A-Z) - - 최소 1개의 소문자(a-z) - - 최소 1개의 숫자(0-9) - - 최소 1개의 특수문자(!@#$%^&*()_+=-{};:'",.<>/?) - - 위 조건을 모두 만족하지 않으면 400 (INVALID_INPUT) 예외가 발생합니다. - """, - - responses = { - @ApiResponse( - responseCode = "201", - description = "회원가입 성공", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "userId": "1c54b9f3-8234-4e8f-b001-11cc4d9012ab", - "email": "testuser@example.com", - "name": "홍길동", - "phoneNumber": "01012345678", - "role": "TEAM_MEMBER" - } - """)) - ), - @ApiResponse( - responseCode = "400", - description = "요청 데이터 유효성 검증 실패 (비밀번호 정책 미준수 포함)", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "비밀번호는 8~20자, 대소문자/숫자/특수문자를 모두 포함해야 합니다." - } - """)) - ) - } - ) - @PostMapping("/signup") - public ResponseEntity signup(@Valid @RequestBody SignupRequest request) { - log.info("[SIGNUP] request: {}", request.getEmail()); - SignupResponse response = userService.signUp(request); - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - @Operation( - summary = "일반 로그인 API", - description = "이메일과 비밀번호로 로그인하고 Access Token과 Refresh Token을 발급합니다.", - responses = { - @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "accessToken": "eyJhbGciOiJIUzI1NiJ9...", - "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", - "userId": "1c54b9f3-8234-4e8f-b001-11cc4d9012ab", - "name": "홍길동", - "role": "TEAM_MEMBER", - "phoneNumber": "01012345678" - } - """)) - ), - @ApiResponse(responseCode = "401", description = "이메일 또는 비밀번호 불일치") - } - ) - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - - LoginResponse response = loginService.login(request); - - // accessToken을 HttpOnly 쿠키로 설정 - ResponseCookie accessCookie = ResponseCookie.from("access", response.getAccessToken()) - .httpOnly(true) - .secure(true) - .sameSite("None") - .path("/") - .maxAge(60L * 60) // 1 hour - .build(); - - // refreshToken을 HttpOnly 쿠키로 설정 - ResponseCookie refreshCookie = ResponseCookie.from("refresh", response.getRefreshToken()) - .httpOnly(true) - .secure(true) - .sameSite("None") - .path("/") - .maxAge(60L * 60 * 24 * 14) // 2 weeks - .build(); - - // JSON 응답에서 토큰 제거, 유저 정보만 포함 - LoginResponse safeResponse = LoginResponse.builder() - .userId(response.getUserId()) - .email(response.getEmail()) - .name(response.getName()) - .role(response.getRole()) - .phoneNumber(response.getPhoneNumber()) - .point(response.getPoint()) - .build(); - - return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, accessCookie.toString()) - .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) - .body(safeResponse); - } - - @Operation( - summary = "Access Token 재발급 API", - description = "만료된 Access Token을 Refresh Token으로 재발급받습니다.", - responses = { - @ApiResponse( - responseCode = "200", - description = "Access Token 재발급 성공", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "accessToken": "eyJhbGciOiJIUzI1NiJ9..." - } - """)) - ), - @ApiResponse( - responseCode = "401", - description = "Refresh Token이 없거나 만료됨", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "Refresh Token이 유효하지 않거나 만료되었습니다." - } - """)) - ) - } - ) - @PostMapping("/reissue") - public ResponseEntity reissue( - @Parameter(description = "Refresh Token 쿠키", example = "refresh=abc123") - @CookieValue(value = "refresh", required = false) String refreshToken - ) { - - // 쿠키에 refreshToken이 없으면 401 - if (refreshToken == null || refreshToken.isEmpty()) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(Map.of("message", "Refresh Token이 없습니다.")); - } - - try { - // 서비스 호출 → accessToken / refreshToken 갱신 - Map tokens = refreshTokenService.reissueTokens(refreshToken); - - // accessToken을 Authorization 헤더로 전달 - ResponseEntity.BodyBuilder response = ResponseEntity.ok() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.get("accessToken")); - - // refreshToken이 새로 발급된 경우 쿠키 교체 - if (tokens.containsKey("refreshToken")) { - ResponseCookie cookie = ResponseCookie.from("refresh", tokens.get("refreshToken")) - .httpOnly(true) - .secure(true) // Swagger/Postman 테스트 중일 땐 false - .sameSite("None") - .path("/") - .maxAge(60L * 60 * 24 * 14) // 2주 - .build(); - - response.header(HttpHeaders.SET_COOKIE, cookie.toString()); - } - - // accessToken을 HttpOnly 쿠키로 설정 - ResponseCookie accessCookie = ResponseCookie.from("access", tokens.get("accessToken")) - .httpOnly(true) - .secure(true) - .sameSite("None") - .path("/") - .maxAge(60L * 60) // 1 hour - .build(); - - response.header(HttpHeaders.SET_COOKIE, accessCookie.toString()); - - // JSON에서 accessToken 제거 - return response.body(Map.of("message", "토큰 갱신 성공")); - - } catch (Exception e) { - log.warn("토큰 재발급 실패: {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(Map.of("message", "Refresh Token이 유효하지 않거나 만료되었습니다.")); - } - } - - @Operation( - summary = "로그아웃 API", - description = "Access Token을 무효화하고 Access/Refresh Token 쿠키를 삭제합니다. 토큰이 없어도 정상적으로 처리됩니다.", - responses = { - @ApiResponse( - responseCode = "200", - description = "로그아웃 성공", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "로그아웃 성공" - } - """)) - ) - } - ) - @PostMapping("/logout") - public ResponseEntity logout( - @Parameter(description = "Access Token 쿠키", example = "access=abc123") - @CookieValue(value = "access", required = false) String accessToken, - @Parameter(description = "Refresh Token 쿠키", example = "refresh=abc123") - @CookieValue(value = "refresh", required = false) String refreshToken - ) { - // 토큰이 없어도 로그아웃 처리 (멱등성 보장) - if (accessToken != null && !accessToken.isEmpty()) { - try { - loginService.logout(accessToken); - } catch (JwtException | IllegalArgumentException e) { - log.warn("Invalid or expired JWT during logout: {}", e.getMessage()); - } catch (Exception e) { - log.error("Unexpected error during logout", e); - } - } - - // Access Token 쿠키 삭제 - ResponseCookie deleteAccessCookie = ResponseCookie.from("access", "") - .httpOnly(true) - .secure(true) - .sameSite("None") - .path("/") - .maxAge(0) - .build(); - - // Refresh Token 쿠키 삭제 - ResponseCookie deleteRefreshCookie = ResponseCookie.from("refresh", "") - .httpOnly(true) - .secure(true) - .sameSite("None") - .path("/") - .maxAge(0) - .build(); - - return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, deleteAccessCookie.toString()) - .header(HttpHeaders.SET_COOKIE, deleteRefreshCookie.toString()) - .body(Map.of("message", "로그아웃 성공")); - } - - @Operation( - summary = "회원 탈퇴 API", - description = "현재 로그인한 사용자의 계정을 삭제합니다.", - responses = { - @ApiResponse( - responseCode = "200", - description = "회원 탈퇴 완료", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "회원 탈퇴가 완료되었습니다." - } - """)) - ), - @ApiResponse( - responseCode = "401", - description = "인증되지 않은 사용자", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "인증이 필요합니다." - } - """)) - ) - } - ) - @DeleteMapping("/withdraw") - public ResponseEntity withdraw( - @Parameter(hidden = true) - @AuthenticationPrincipal CustomUserDetails user - ) { - if (user == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(Map.of("message", "인증이 필요합니다.")); - } - - // DB에서 사용자 정보 삭제 - userService.deleteUserWithOauth(user.getUserId()); - log.info("회원 탈퇴 완료: {}", user.getEmail()); - - //Refresh Token DB에서도 삭제 - refreshTokenService.deleteByUserId(user.getUserId()); - - // 브라우저 쿠키 삭제 - ResponseCookie deleteCookie = ResponseCookie.from("refresh", "") - .httpOnly(true) - .secure(true) - .sameSite("None") - .path("/") - .maxAge(0) - .build(); - - return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, deleteCookie.toString()) // 나중에 추가 - .body(Map.of("message", "회원 탈퇴가 완료되었습니다.")); - } - -} - diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/LoginService.java b/backend/src/main/java/org/sejongisc/backend/auth/service/LoginService.java deleted file mode 100644 index 3f6b10cc..00000000 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/LoginService.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.sejongisc.backend.auth.service; - -import jakarta.servlet.http.HttpServletRequest; -import org.sejongisc.backend.auth.dto.LoginRequest; -import org.sejongisc.backend.auth.dto.LoginResponse; - -public interface LoginService { - LoginResponse login(LoginRequest request); - - void logout(String accessToken); -} diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java deleted file mode 100644 index e48a1ed8..00000000 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GithubServiceImpl.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.sejongisc.backend.auth.service.oauth2; - -import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dto.oauth.GithubTokenResponse; -import org.sejongisc.backend.auth.dto.oauth.GithubUserInfoResponse; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -import java.util.function.Function; - -@Slf4j -@Service("GITHUB") -public class GithubServiceImpl implements Oauth2Service { - - private final String clientId; - private final String clientSecret; - - private final String TOKEN_URL; - private final String USERINFO_URL; - - @Autowired - public GithubServiceImpl( - @Value("${github.client.id}") String clientId, - @Value("${github.client.secret}") String clientSecret) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.TOKEN_URL = "https://github.com/login/oauth/access_token"; - this.USERINFO_URL = "https://api.github.com/user"; - } - - // ✅ 테스트용 생성자 - public GithubServiceImpl(String clientId, String clientSecret, - String tokenUrl, String userInfoUrl) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.TOKEN_URL = tokenUrl; - this.USERINFO_URL = userInfoUrl; - } - - @Override - public GithubTokenResponse getAccessToken(String code) { - GithubTokenResponse tokenResponse = WebClient.create(TOKEN_URL).post() - .uri(uriBuilder -> uriBuilder.build(true)) - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .body(BodyInserters.fromFormData("client_id", clientId) - .with("client_secret", clientSecret) - .with("code", code)) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, - clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) - .onStatus(HttpStatusCode::is5xxServerError, - clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) - .bodyToMono(GithubTokenResponse.class) - .block(); - - if (tokenResponse == null || tokenResponse.getAccessToken() == null) { - throw new RuntimeException("Token response is empty"); - } - - Function mask = token -> { - if(token == null || token.length() < 8) return "****"; - return token.substring(0, 4) + "..." + token.substring(token.length() - 4); - }; - - log.debug(" [Github Service] Access Token ------> {}", mask.apply(tokenResponse.getAccessToken())); - log.debug(" [Github Service] Scope ------> {}", mask.apply(tokenResponse.getScope())); - - return tokenResponse; - } - - @Override - public GithubUserInfoResponse getUserInfo(String accessToken) { - GithubUserInfoResponse userInfo = WebClient.create(USERINFO_URL).get() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, - clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) - .onStatus(HttpStatusCode::is5xxServerError, - clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) - .bodyToMono(GithubUserInfoResponse.class) - .block(); - - if (userInfo == null) { - throw new RuntimeException("UserInfo response is empty"); - } - - if (log.isDebugEnabled()) { - log.debug(" [Github Service] ID ------> {}", userInfo.getId()); - log.debug(" [Github Service] Login ------> {}", userInfo.getLogin()); - log.debug(" [Github Service] Name ------> {}", userInfo.getName()); - } - - return userInfo; - } -} diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java b/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java index 311f64b3..0b185672 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java @@ -10,9 +10,9 @@ import org.sejongisc.backend.backtest.dto.BacktestRequest; import org.sejongisc.backend.backtest.dto.BacktestResponse; -import org.sejongisc.backend.backtest.dto.BacktestRunRequest; import org.sejongisc.backend.backtest.service.BacktestService; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; diff --git a/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java b/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java index 17de4597..134181d4 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java @@ -5,15 +5,14 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.betting.dto.BetRoundResponse; import org.sejongisc.backend.betting.dto.UserBetRequest; import org.sejongisc.backend.betting.entity.BetRound; import org.sejongisc.backend.betting.entity.Scope; -import org.sejongisc.backend.betting.entity.UserBet; import org.sejongisc.backend.betting.service.BettingService; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; diff --git a/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java b/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java index fbb77cc1..e4aee521 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java +++ b/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java @@ -13,7 +13,8 @@ import org.sejongisc.backend.board.dto.PostResponse; import org.sejongisc.backend.board.service.PostInteractionService; import org.sejongisc.backend.board.service.PostService; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.springframework.data.domain.Page; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java deleted file mode 100644 index 16cbebca..00000000 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.sejongisc.backend.common.auth.config; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; -import org.sejongisc.backend.auth.entity.AuthProvider; -import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.user.entity.Role; -import org.sejongisc.backend.user.entity.User; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Map; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional -public class CustomOAuth2UserService implements OAuth2UserService { - private final UserRepository userRepository; - private final UserOauthAccountRepository oauthAccountRepository; - - @Override - public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationException { - // log.info("[CustomOAuth2UserService] loadUser START"); - - OAuth2UserService delegate = - new DefaultOAuth2UserService(); - OAuth2User oAuth2User = delegate.loadUser(req); - - String provider = req.getClientRegistration().getRegistrationId(); // google, kakao, github - Map attrs = oAuth2User.getAttributes(); - - String providerUid; - String email; - String name; - - // log.info("[OAuth2] Provider = {}", provider); - if (log.isDebugEnabled()) { - log.debug("[OAuth2] Attributes = {}", attrs); - } - - switch (provider) { - case "google" -> { - providerUid = (String) attrs.get("sub"); - email = (String) attrs.get("email"); - name = (String) attrs.get("name"); - } - case "kakao" -> { - providerUid = attrs.get("id").toString(); - Map kakaoAccount = (Map) attrs.get("kakao_account"); - email = (String) kakaoAccount.get("email"); // null 가능 - Map profile = (Map) kakaoAccount.get("profile"); - name = (String) profile.get("nickname"); - } - case "github" -> { - providerUid = attrs.get("id").toString(); - email = (String) attrs.get("email"); - name = (String) attrs.get("login"); // GitHub은 login이 닉네임 - } - default -> throw new RuntimeException("지원하지 않는 provider: " + provider); - } - - // log.info("provider={}, providerUid={}, email={}, name={}", provider, providerUid, email, name); - - final String fProviderUid = providerUid; - final String fEmail = email; - final String fName = name; - final AuthProvider fAuthProvider = AuthProvider.valueOf(provider.toUpperCase()); - - User user = oauthAccountRepository - .findByProviderAndProviderUid(AuthProvider.from(provider), providerUid) - .map(UserOauthAccount::getUser) - .orElseGet(() -> { - User newUser = User.builder() - .email(email) - .name(name) - .role(Role.TEAM_MEMBER) - .build(); - User saved = userRepository.save(newUser); - - UserOauthAccount oauth = UserOauthAccount.builder() - .user(saved) - .provider(AuthProvider.from(provider)) - .providerUid(providerUid) - .build(); - oauthAccountRepository.save(oauth); - - return saved; - }); - - // log.info("[CustomOAuth2UserService] User resolved → returning OAuth2User"); - - Map attributes = new java.util.HashMap<>(); - attributes.put("provider", provider); // google / kakao / github - attributes.put("providerUid", providerUid); // 소셜 계정 UID - attributes.put("email", user.getEmail()); // DB email - attributes.put("name", user.getName()); - attributes.put("userId", user.getUserId()); // DB user uuid - - return new DefaultOAuth2User( - List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER")), - attributes, - "userId" // 또는 "email" -> email null 이면 id가 더 안전 - ); - - } -} diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java deleted file mode 100644 index 02fd4ffe..00000000 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.sejongisc.backend.common.auth.config; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; -import org.sejongisc.backend.auth.entity.AuthProvider; -import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.user.entity.Role; -import org.sejongisc.backend.user.entity.User; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.Map; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional -public class CustomOidcUserService extends OidcUserService { - - private final UserRepository userRepository; - private final UserOauthAccountRepository oauthAccountRepository; - - @Override - public OidcUser loadUser(OidcUserRequest req) throws OAuth2AuthenticationException { - // log.info("[CustomOidcUserService] Google OIDC loadUser START"); - - OidcUser oidcUser = super.loadUser(req); - - Map original = oidcUser.getAttributes(); - // log.info("OIDC claims: {}", original); - - // SuccessHandler가 필요로 하는 attributes 넣기 - String provider = "google"; // provider - String providerUid = (String) original.get("sub"); // 구글 고유 ID - String email = (String) original.get("email"); - String name = (String) original.get("name"); - - // 신규 가입 or 기존 유저 조회 - User user = oauthAccountRepository - .findByProviderAndProviderUid(AuthProvider.GOOGLE, providerUid) - .map(UserOauthAccount::getUser) - .orElseGet(() -> { - // (1) User 생성 - User newUser = User.builder() - .email(email) - .name(name) - .role(Role.TEAM_MEMBER) - .build(); - User savedUser = userRepository.save(newUser); - - // (2) UserOauthAccount 생성 - UserOauthAccount oauth = UserOauthAccount.builder() - .user(savedUser) - .provider(AuthProvider.GOOGLE) - .providerUid(providerUid) - .build(); - oauthAccountRepository.save(oauth); - - // log.info("[CustomOidcUserService] 신규 User 및 UserOauthAccount 생성됨"); - return savedUser; - }); - - Map attrs = new HashMap<>(original); - attrs.put("provider", provider); - attrs.put("providerUid", providerUid); - attrs.put("email", user.getEmail()); - attrs.put("name", user.getName()); - attrs.put("userId", user.getUserId()); // SuccessHandler가 필요로 함 - - // 여기서 attrs를 defaultOidcUser에 직접 넣음 - return new DefaultOidcUser( - oidcUser.getAuthorities(), - oidcUser.getIdToken(), - oidcUser.getUserInfo(), - "sub" - ) { - @Override - public Map getAttributes() { - return attrs; // 커스텀 attributes 적용 - } - }; - } -} diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java deleted file mode 100644 index fac6fc96..00000000 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.sejongisc.backend.common.auth.config; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.env.Environment; -import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; -import org.sejongisc.backend.auth.entity.AuthProvider; -import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; -import org.sejongisc.backend.auth.service.RefreshTokenService; -import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.user.entity.User; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -@Slf4j -@Component -@RequiredArgsConstructor -public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - - private final JwtProvider jwtProvider; - private final RefreshTokenService refreshTokenService; - private final UserRepository userRepository; - private final UserOauthAccountRepository userOauthAccountRepository; - private final Environment env; - - @Value("${app.oauth2.redirect-success}") - private String redirectSuccessBase; - - @Override - public void onAuthenticationSuccess( - HttpServletRequest request, - HttpServletResponse response, - Authentication authentication) throws IOException{ - - // log.info("[OAuth2SuccessHandler] SUCCESS HANDLER CALLED!"); - - if (!(authentication.getPrincipal() instanceof DefaultOAuth2User oauthUser)) { - throw new IllegalStateException("Unknown principal type: " + authentication.getPrincipal().getClass()); - } - - // 1. CustomOAuth2UserService에서 넣어준 attributes 가져오기 - Map attrs = oauthUser.getAttributes(); - - String providerStr = (String) attrs.get("provider"); - String providerUid = (String) attrs.get("providerUid"); - if (providerStr == null) { - throw new IllegalStateException("OAuth provider attribute missing from attributes"); - } - - AuthProvider provider = - switch (providerStr) { - case "kakao" -> AuthProvider.KAKAO; - case "github" -> AuthProvider.GITHUB; - case "google" -> AuthProvider.GOOGLE; - default -> throw new IllegalStateException("Unknown OAuth provider: " + providerStr); - }; - - - // log.info("[OAuth2SuccessHandler] provider={}, providerUid={}", provider, providerUid); - - // DB 조회 - UserOauthAccount account = userOauthAccountRepository - .findByProviderAndProviderUid(provider, providerUid) - .orElseThrow(() -> new RuntimeException("소셜 계정이 DB에 없습니다. (회원가입 필요)")); - - User user = userRepository.findById(account.getUser().getUserId()) - .orElseThrow(() -> new RuntimeException("User not found")); - - // JWT 생성 - String accessToken = jwtProvider.createToken( - user.getUserId(), - user.getRole(), - user.getEmail() - ); - - - // 4. RefreshToken 생성 - String refreshToken = jwtProvider.createRefreshToken(user.getUserId()); - // 5. RefreshToken 저장(DB or Redis) - refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken); - - String[] activeProfiles = env.getActiveProfiles(); - List profiles = Arrays.asList(activeProfiles); - - boolean isProd = profiles.contains("prod"); - boolean isDev = profiles.contains("dev"); - -// SameSite, Secure 설정 (dev도 prod와 동일하게) - String sameSite = (isProd || isDev) ? "None" : "Lax"; - boolean secure = (isProd || isDev); - -// 도메인 설정 - String domain; - if (isProd) { - domain = "sjusisc.com"; // 운영 도메인 - } else if (isDev) { - domain = "sisc-web.duckdns.org"; // 개발 도메인 - } else { - domain = "localhost"; // 기본값 - } - - - - - // 6. HttpOnly 쿠키로 refreshToken 저장 - ResponseCookie.ResponseCookieBuilder accessCookieBuilder = ResponseCookie.from("access", accessToken) - .httpOnly(true) - .secure(secure) // 로컬=false, 배포=true - .sameSite(sameSite) // 로컬= "Lax", 배포="None" - .path("/") - .maxAge(60L * 60); // 1 hour - - // 로컬 환경에서는 domain 설정하지 않음 - if (isProd || isDev) { - accessCookieBuilder.domain(domain); - } - - ResponseCookie.ResponseCookieBuilder refreshCookieBuilder = ResponseCookie.from("refresh", refreshToken) - .httpOnly(true) - .secure(secure) - .sameSite(sameSite) - .path("/") - .maxAge(60L * 60 * 24 * 14); // 2 weeks - - // 로컬 환경에서는 domain 설정하지 않음 - if (isProd || isDev) { - refreshCookieBuilder.domain(domain); - } - - ResponseCookie accessCookie = accessCookieBuilder.build(); - ResponseCookie refreshCookie = refreshCookieBuilder.build(); - - - response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString()); - response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); - - - // 7. 프론트로 redirect - // application-local.yml → http://localhost:5173/oauth/success - // application-prod.yml → https://sisc-web.duckdns.org/oauth/success - //String redirectUrl = redirectSuccessBase; -// + "?accessToken=" + accessToken -// + "&name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) -// + "&userId=" + userId; - - // log.info("[OAuth2 Redirect] {}", redirectUrl); - - getRedirectStrategy().sendRedirect(request, response, redirectSuccessBase); - } - -} diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthController.java b/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthController.java new file mode 100644 index 00000000..e242205e --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthController.java @@ -0,0 +1,87 @@ +package org.sejongisc.backend.common.auth.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.common.auth.dto.LoginRequest; +import org.sejongisc.backend.common.auth.dto.LoginResponse; +import org.sejongisc.backend.common.auth.service.AuthService; +import org.sejongisc.backend.common.auth.service.RefreshTokenService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@Tag(name = "인증 API", description = "회원 인증 및 소셜 로그인 관련 API를 제공합니다.") +public class AuthController { + + private final AuthService authService; + private final RefreshTokenService refreshTokenService; + private final AuthCookieHelper cookieHelper; // 주입 + + @Operation(summary = "일반 로그인 API", description = "") + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + LoginResponse response = authService.login(request); + + ResponseCookie accessCookie = cookieHelper.createAccessCookie(response.getAccessToken()); + ResponseCookie refreshCookie = cookieHelper.createRefreshCookie(response.getRefreshToken()); + + LoginResponse safeResponse = LoginResponse.builder() + .userId(response.getUserId()).email(response.getEmail()) + .name(response.getName()).role(response.getRole()) + .phoneNumber(response.getPhoneNumber()).point(response.getPoint()) + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, accessCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) + .body(safeResponse); + } + + @Operation(summary = "Access Token 재발급 API", description = "...") + @PostMapping("/reissue") + public ResponseEntity reissue(@CookieValue(value = "refresh", required = false) String refreshToken) { + if (refreshToken == null || refreshToken.isEmpty()) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Refresh Token이 없습니다.")); + } + + try { + Map tokens = refreshTokenService.reissueTokens(refreshToken); + ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.get("accessToken")); + + if (tokens.containsKey("refreshToken")) { + responseBuilder.header(HttpHeaders.SET_COOKIE, cookieHelper.createRefreshCookie(tokens.get("refreshToken")).toString()); + } + + responseBuilder.header(HttpHeaders.SET_COOKIE, cookieHelper.createAccessCookie(tokens.get("accessToken")).toString()); + return responseBuilder.body(Map.of("message", "토큰 갱신 성공")); + + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Refresh Token이 유효하지 않거나 만료되었습니다.")); + } + } + + @Operation(summary = "로그아웃 API", description = "...") + @PostMapping("/logout") + public ResponseEntity logout(@CookieValue(value = "access", required = false) String accessToken) { + if (accessToken != null && !accessToken.isEmpty()) { + authService.logout(accessToken); + } + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, cookieHelper.deleteCookie("access").toString()) + .header(HttpHeaders.SET_COOKIE, cookieHelper.deleteCookie("refresh").toString()) + .body(Map.of("message", "로그아웃 성공")); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthCookieHelper.java b/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthCookieHelper.java new file mode 100644 index 00000000..87d0d63f --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthCookieHelper.java @@ -0,0 +1,36 @@ +package org.sejongisc.backend.common.auth.controller; + +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +@Component +public class AuthCookieHelper { + + public ResponseCookie createAccessCookie(String token) { + return createCookie("access", token, 60L * 60); + } + + public ResponseCookie createRefreshCookie(String token) { + return createCookie("refresh", token, 60L * 60 * 24 * 14); + } + + public ResponseCookie deleteCookie(String name) { + return ResponseCookie.from(name, "") + .httpOnly(true) + .secure(true) + .sameSite("None") + .path("/") + .maxAge(0) + .build(); + } + + private ResponseCookie createCookie(String name, String value, long maxAge) { + return ResponseCookie.from(name, value) + .httpOnly(true) + .secure(true) + .sameSite("None") + .path("/") + .maxAge(maxAge) + .build(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/auth/controller/EmailController.java b/backend/src/main/java/org/sejongisc/backend/common/auth/controller/EmailController.java similarity index 93% rename from backend/src/main/java/org/sejongisc/backend/auth/controller/EmailController.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/controller/EmailController.java index d0ecfece..f40149d9 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/controller/EmailController.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/controller/EmailController.java @@ -1,12 +1,11 @@ -package org.sejongisc.backend.auth.controller; +package org.sejongisc.backend.common.auth.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.sejongisc.backend.auth.service.EmailService; +import org.sejongisc.backend.common.auth.service.EmailService; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetails.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/CustomUserDetails.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetails.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/CustomUserDetails.java index c5069d7d..87875178 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetails.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/CustomUserDetails.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.auth.springsecurity; +package org.sejongisc.backend.common.auth.dto; import lombok.Getter; import org.sejongisc.backend.user.entity.Role; @@ -67,4 +67,4 @@ public boolean isCredentialsNonExpired() { public boolean isEnabled() { return UserDetails.super.isEnabled(); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/LoginRequest.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginRequest.java similarity index 95% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/LoginRequest.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginRequest.java index e16f5ef2..86870493 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/LoginRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginRequest.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.common.auth.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/LoginResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginResponse.java similarity index 84% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/LoginResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginResponse.java index 7a9c5284..5866234a 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/LoginResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginResponse.java @@ -1,11 +1,17 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.common.auth.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import org.sejongisc.backend.user.entity.Role; +import org.sejongisc.backend.user.entity.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import java.util.Collection; +import java.util.List; import java.util.UUID; @Getter diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/SignupRequest.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupRequest.java similarity index 97% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/SignupRequest.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupRequest.java index 6b443ab7..dd2d9587 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/SignupRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupRequest.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.common.auth.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/SignupResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupResponse.java similarity index 97% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/SignupResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupResponse.java index d87f230a..dab21c8a 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/SignupResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto; +package org.sejongisc.backend.common.auth.dto; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubTokenResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubTokenResponse.java similarity index 95% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubTokenResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubTokenResponse.java index a1d3f816..925adf99 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubTokenResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubTokenResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoAdapter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubUserInfoAdapter.java similarity index 88% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoAdapter.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubUserInfoAdapter.java index a9e8bb25..0b1d9a09 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoAdapter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubUserInfoAdapter.java @@ -1,6 +1,6 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; -import org.sejongisc.backend.auth.entity.AuthProvider; +import org.sejongisc.backend.common.auth.entity.AuthProvider; import java.util.Optional; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubUserInfoResponse.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubUserInfoResponse.java index 2a95bd32..1daa26c6 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GithubUserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GithubUserInfoResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleTokenResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleTokenResponse.java similarity index 97% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleTokenResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleTokenResponse.java index aa6dec76..60cfb67e 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleTokenResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleTokenResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoAdapter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleUserInfoAdapter.java similarity index 87% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoAdapter.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleUserInfoAdapter.java index 5e2bf598..7ce0cca7 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoAdapter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleUserInfoAdapter.java @@ -1,6 +1,6 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; -import org.sejongisc.backend.auth.entity.AuthProvider; +import org.sejongisc.backend.common.auth.entity.AuthProvider; import java.util.Optional; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleUserInfoResponse.java similarity index 95% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleUserInfoResponse.java index 785ff751..814928d5 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/GoogleUserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/GoogleUserInfoResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoTokenResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoTokenResponse.java similarity index 97% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoTokenResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoTokenResponse.java index 2e855c97..30e12a4a 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoTokenResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoTokenResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoAdapter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoUserInfoAdapter.java similarity index 90% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoAdapter.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoUserInfoAdapter.java index 0827ec57..208d1cdc 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoAdapter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoUserInfoAdapter.java @@ -1,6 +1,6 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; -import org.sejongisc.backend.auth.entity.AuthProvider; +import org.sejongisc.backend.common.auth.entity.AuthProvider; import java.util.Optional; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoUserInfoResponse.java similarity index 99% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoUserInfoResponse.java index 3bc672b6..7ee73510 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/KakaoUserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/KakaoUserInfoResponse.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/OauthUserInfo.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/OauthUserInfo.java similarity index 64% rename from backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/OauthUserInfo.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/OauthUserInfo.java index 4fd0e7dd..bd3b5cb7 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/oauth/OauthUserInfo.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/oauth/OauthUserInfo.java @@ -1,6 +1,6 @@ -package org.sejongisc.backend.auth.dto.oauth; +package org.sejongisc.backend.common.auth.dto.oauth; -import org.sejongisc.backend.auth.entity.AuthProvider; +import org.sejongisc.backend.common.auth.entity.AuthProvider; public interface OauthUserInfo { String getProviderUid(); diff --git a/backend/src/main/java/org/sejongisc/backend/auth/entity/AuthProvider.java b/backend/src/main/java/org/sejongisc/backend/common/auth/entity/AuthProvider.java similarity index 89% rename from backend/src/main/java/org/sejongisc/backend/auth/entity/AuthProvider.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/entity/AuthProvider.java index 6f488d8d..dc5bc582 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/entity/AuthProvider.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/entity/AuthProvider.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.entity; +package org.sejongisc.backend.common.auth.entity; public enum AuthProvider { GOOGLE, // 구글 diff --git a/backend/src/main/java/org/sejongisc/backend/auth/entity/RefreshToken.java b/backend/src/main/java/org/sejongisc/backend/common/auth/entity/RefreshToken.java similarity index 88% rename from backend/src/main/java/org/sejongisc/backend/auth/entity/RefreshToken.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/entity/RefreshToken.java index 8f1b390e..ba166217 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/entity/RefreshToken.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/entity/RefreshToken.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.entity; +package org.sejongisc.backend.common.auth.entity; import jakarta.persistence. *; import lombok.*; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/entity/UserOauthAccount.java b/backend/src/main/java/org/sejongisc/backend/common/auth/entity/UserOauthAccount.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/auth/entity/UserOauthAccount.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/entity/UserOauthAccount.java index e85ae311..24aed5cf 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/entity/UserOauthAccount.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/entity/UserOauthAccount.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.entity; +package org.sejongisc.backend.common.auth.entity; import jakarta.persistence.*; import lombok.*; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java similarity index 98% rename from backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java index 34d20242..ea4b1d90 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.auth.springsecurity; +package org.sejongisc.backend.common.auth.filter; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,7 +12,6 @@ import jakarta.validation.constraints.NotNull; import java.io.IOException; import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java index ac76865e..c91520ba 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java @@ -5,8 +5,7 @@ import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetailsService; +import org.sejongisc.backend.common.auth.service.CustomUserDetailsService; import org.sejongisc.backend.user.entity.Role; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/repository/RefreshTokenRepository.java b/backend/src/main/java/org/sejongisc/backend/common/auth/repository/RefreshTokenRepository.java similarity index 75% rename from backend/src/main/java/org/sejongisc/backend/auth/repository/RefreshTokenRepository.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/repository/RefreshTokenRepository.java index 362282de..5d271507 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/repository/RefreshTokenRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/repository/RefreshTokenRepository.java @@ -1,6 +1,6 @@ -package org.sejongisc.backend.auth.repository; +package org.sejongisc.backend.common.auth.repository; -import org.sejongisc.backend.auth.entity.RefreshToken; +import org.sejongisc.backend.common.auth.entity.RefreshToken; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/repository/UserOauthAccountRepository.java b/backend/src/main/java/org/sejongisc/backend/common/auth/repository/UserOauthAccountRepository.java similarity index 70% rename from backend/src/main/java/org/sejongisc/backend/auth/repository/UserOauthAccountRepository.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/repository/UserOauthAccountRepository.java index f29e4e6d..f47bc2fa 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/repository/UserOauthAccountRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/repository/UserOauthAccountRepository.java @@ -1,7 +1,7 @@ -package org.sejongisc.backend.auth.repository; +package org.sejongisc.backend.common.auth.repository; -import org.sejongisc.backend.auth.entity.AuthProvider; -import org.sejongisc.backend.auth.entity.UserOauthAccount; +import org.sejongisc.backend.common.auth.entity.AuthProvider; +import org.sejongisc.backend.common.auth.entity.UserOauthAccount; import org.sejongisc.backend.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/LoginServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java similarity index 87% rename from backend/src/main/java/org/sejongisc/backend/auth/service/LoginServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java index a36243ab..93d99414 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/LoginServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java @@ -1,16 +1,17 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.common.auth.service; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.repository.RefreshTokenRepository; +import org.sejongisc.backend.common.auth.entity.RefreshToken; +import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; import org.sejongisc.backend.common.auth.jwt.JwtParser; import org.sejongisc.backend.common.auth.jwt.JwtProvider; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.auth.dto.LoginRequest; -import org.sejongisc.backend.auth.dto.LoginResponse; +import org.sejongisc.backend.common.auth.dto.LoginRequest; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.user.entity.User; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -20,7 +21,7 @@ @Slf4j @Service @RequiredArgsConstructor -public class LoginServiceImpl implements LoginService { +public class AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @@ -28,7 +29,6 @@ public class LoginServiceImpl implements LoginService { private final RefreshTokenRepository refreshTokenRepository; private final JwtParser jwtParser; - @Override @Transactional public LoginResponse login(LoginRequest request) { User user = userRepository.findUserByEmail(request.getEmail()) @@ -46,7 +46,7 @@ public LoginResponse login(LoginRequest request) { .ifPresent(refreshTokenRepository::delete); refreshTokenRepository.save( - org.sejongisc.backend.auth.entity.RefreshToken.builder() + RefreshToken.builder() .userId(user.getUserId()) .token(refreshToken) .build() @@ -65,7 +65,6 @@ public LoginResponse login(LoginRequest request) { .build(); } - @Override @Transactional public void logout(String accessToken) { UUID userId = jwtParser.getUserIdFromToken(accessToken); diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/CustomUserDetailsService.java similarity index 91% rename from backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/CustomUserDetailsService.java index 50fa314c..a44be946 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/CustomUserDetailsService.java @@ -1,6 +1,7 @@ -package org.sejongisc.backend.common.auth.springsecurity; +package org.sejongisc.backend.common.auth.service; import lombok.RequiredArgsConstructor; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.repository.UserRepository; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/EmailService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java similarity index 98% rename from backend/src/main/java/org/sejongisc/backend/auth/service/EmailService.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java index 9a937e2d..05dbd20f 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/EmailService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.common.auth.service; import jakarta.mail.Message; import jakarta.mail.MessagingException; @@ -9,7 +9,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.validator.routines.EmailValidator; -import org.sejongisc.backend.auth.config.EmailProperties; +import org.sejongisc.backend.common.config.EmailProperties; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.repository.UserRepository; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java similarity index 92% rename from backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenService.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java index e61d4886..fd31208f 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.common.auth.service; import java.util.Map; import java.util.UUID; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenServiceImpl.java similarity index 95% rename from backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenServiceImpl.java index 1e1452f6..793bf2eb 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenServiceImpl.java @@ -1,10 +1,10 @@ -package org.sejongisc.backend.auth.service; +package org.sejongisc.backend.common.auth.service; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.entity.RefreshToken; -import org.sejongisc.backend.auth.repository.RefreshTokenRepository; +import org.sejongisc.backend.common.auth.entity.RefreshToken; +import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; import org.sejongisc.backend.common.auth.jwt.JwtProvider; import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; import org.sejongisc.backend.common.exception.CustomException; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/GithubServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/GithubServiceImpl.java new file mode 100644 index 00000000..0ce8d365 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/GithubServiceImpl.java @@ -0,0 +1,440 @@ +package org.sejongisc.backend.common.auth.service.oauth2; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.common.auth.dto.oauth.GithubTokenResponse; +import org.sejongisc.backend.common.auth.dto.oauth.GithubUserInfoResponse; +import org.sejongisc.backend.common.auth.entity.AuthProvider; +import org.sejongisc.backend.common.auth.entity.UserOauthAccount; +import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.repository.UserOauthAccountRepository; +import org.sejongisc.backend.common.auth.service.RefreshTokenService; +import org.sejongisc.backend.user.entity.Role; +import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +@Slf4j +@Service("GITHUB") +public class GithubServiceImpl implements Oauth2Service { + + private final String clientId; + private final String clientSecret; + + private final String TOKEN_URL; + private final String USERINFO_URL; + + @Autowired + public GithubServiceImpl( + @Value("${github.client.id}") String clientId, + @Value("${github.client.secret}") String clientSecret) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.TOKEN_URL = "https://github.com/login/oauth/access_token"; + this.USERINFO_URL = "https://api.github.com/user"; + } + + // ✅ 테스트용 생성자 + public GithubServiceImpl(String clientId, String clientSecret, + String tokenUrl, String userInfoUrl) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.TOKEN_URL = tokenUrl; + this.USERINFO_URL = userInfoUrl; + } + + @Override + public GithubTokenResponse getAccessToken(String code) { + GithubTokenResponse tokenResponse = WebClient.create(TOKEN_URL).post() + .uri(uriBuilder -> uriBuilder.build(true)) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .body(BodyInserters.fromFormData("client_id", clientId) + .with("client_secret", clientSecret) + .with("code", code)) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) + .onStatus(HttpStatusCode::is5xxServerError, + clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) + .bodyToMono(GithubTokenResponse.class) + .block(); + + if (tokenResponse == null || tokenResponse.getAccessToken() == null) { + throw new RuntimeException("Token response is empty"); + } + + Function mask = token -> { + if(token == null || token.length() < 8) return "****"; + return token.substring(0, 4) + "..." + token.substring(token.length() - 4); + }; + + log.debug(" [Github Service] Access Token ------> {}", mask.apply(tokenResponse.getAccessToken())); + log.debug(" [Github Service] Scope ------> {}", mask.apply(tokenResponse.getScope())); + + return tokenResponse; + } + + @Override + public GithubUserInfoResponse getUserInfo(String accessToken) { + GithubUserInfoResponse userInfo = WebClient.create(USERINFO_URL).get() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) + .onStatus(HttpStatusCode::is5xxServerError, + clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) + .bodyToMono(GithubUserInfoResponse.class) + .block(); + + if (userInfo == null) { + throw new RuntimeException("UserInfo response is empty"); + } + + if (log.isDebugEnabled()) { + log.debug(" [Github Service] ID ------> {}", userInfo.getId()); + log.debug(" [Github Service] Login ------> {}", userInfo.getLogin()); + log.debug(" [Github Service] Name ------> {}", userInfo.getName()); + } + + return userInfo; + } + + @Slf4j + @Service + @RequiredArgsConstructor + @Transactional + public static class CustomOAuth2UserService implements OAuth2UserService { + private final UserRepository userRepository; + private final UserOauthAccountRepository oauthAccountRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationException { + // log.info("[CustomOAuth2UserService] loadUser START"); + + OAuth2UserService delegate = + new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(req); + + String provider = req.getClientRegistration().getRegistrationId(); // google, kakao, github + Map attrs = oAuth2User.getAttributes(); + + String providerUid; + String email; + String name; + + // log.info("[OAuth2] Provider = {}", provider); + if (log.isDebugEnabled()) { + log.debug("[OAuth2] Attributes = {}", attrs); + } + + switch (provider) { + case "google" -> { + providerUid = (String) attrs.get("sub"); + email = (String) attrs.get("email"); + name = (String) attrs.get("name"); + } + case "kakao" -> { + providerUid = attrs.get("id").toString(); + Map kakaoAccount = (Map) attrs.get("kakao_account"); + email = (String) kakaoAccount.get("email"); // null 가능 + Map profile = (Map) kakaoAccount.get("profile"); + name = (String) profile.get("nickname"); + } + case "github" -> { + providerUid = attrs.get("id").toString(); + email = (String) attrs.get("email"); + name = (String) attrs.get("login"); // GitHub은 login이 닉네임 + } + default -> throw new RuntimeException("지원하지 않는 provider: " + provider); + } + + // log.info("provider={}, providerUid={}, email={}, name={}", provider, providerUid, email, name); + + final String fProviderUid = providerUid; + final String fEmail = email; + final String fName = name; + final AuthProvider fAuthProvider = AuthProvider.valueOf(provider.toUpperCase()); + + User user = oauthAccountRepository + .findByProviderAndProviderUid(AuthProvider.from(provider), providerUid) + .map(UserOauthAccount::getUser) + .orElseGet(() -> { + User newUser = User.builder() + .email(email) + .name(name) + .role(Role.TEAM_MEMBER) + .build(); + User saved = userRepository.save(newUser); + + UserOauthAccount oauth = UserOauthAccount.builder() + .user(saved) + .provider(AuthProvider.from(provider)) + .providerUid(providerUid) + .build(); + oauthAccountRepository.save(oauth); + + return saved; + }); + + // log.info("[CustomOAuth2UserService] User resolved → returning OAuth2User"); + + Map attributes = new java.util.HashMap<>(); + attributes.put("provider", provider); // google / kakao / github + attributes.put("providerUid", providerUid); // 소셜 계정 UID + attributes.put("email", user.getEmail()); // DB email + attributes.put("name", user.getName()); + attributes.put("userId", user.getUserId()); // DB user uuid + + return new DefaultOAuth2User( + List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER")), + attributes, + "userId" // 또는 "email" -> email null 이면 id가 더 안전 + ); + + } + } + + @Slf4j + @Service + @RequiredArgsConstructor + @Transactional + public static class CustomOidcUserService extends OidcUserService { + + private final UserRepository userRepository; + private final UserOauthAccountRepository oauthAccountRepository; + + @Override + public OidcUser loadUser(OidcUserRequest req) throws OAuth2AuthenticationException { + // log.info("[CustomOidcUserService] Google OIDC loadUser START"); + + OidcUser oidcUser = super.loadUser(req); + + Map original = oidcUser.getAttributes(); + // log.info("OIDC claims: {}", original); + + // SuccessHandler가 필요로 하는 attributes 넣기 + String provider = "google"; // provider + String providerUid = (String) original.get("sub"); // 구글 고유 ID + String email = (String) original.get("email"); + String name = (String) original.get("name"); + + // 신규 가입 or 기존 유저 조회 + User user = oauthAccountRepository + .findByProviderAndProviderUid(AuthProvider.GOOGLE, providerUid) + .map(UserOauthAccount::getUser) + .orElseGet(() -> { + // (1) User 생성 + User newUser = User.builder() + .email(email) + .name(name) + .role(Role.TEAM_MEMBER) + .build(); + User savedUser = userRepository.save(newUser); + + // (2) UserOauthAccount 생성 + UserOauthAccount oauth = UserOauthAccount.builder() + .user(savedUser) + .provider(AuthProvider.GOOGLE) + .providerUid(providerUid) + .build(); + oauthAccountRepository.save(oauth); + + // log.info("[CustomOidcUserService] 신규 User 및 UserOauthAccount 생성됨"); + return savedUser; + }); + + Map attrs = new HashMap<>(original); + attrs.put("provider", provider); + attrs.put("providerUid", providerUid); + attrs.put("email", user.getEmail()); + attrs.put("name", user.getName()); + attrs.put("userId", user.getUserId()); // SuccessHandler가 필요로 함 + + // 여기서 attrs를 defaultOidcUser에 직접 넣음 + return new DefaultOidcUser( + oidcUser.getAuthorities(), + oidcUser.getIdToken(), + oidcUser.getUserInfo(), + "sub" + ) { + @Override + public Map getAttributes() { + return attrs; // 커스텀 attributes 적용 + } + }; + } + } + + @Slf4j + @Component + @RequiredArgsConstructor + public static class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtProvider jwtProvider; + private final RefreshTokenService refreshTokenService; + private final UserRepository userRepository; + private final UserOauthAccountRepository userOauthAccountRepository; + private final Environment env; + + @Value("${app.oauth2.redirect-success}") + private String redirectSuccessBase; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException { + + // log.info("[OAuth2SuccessHandler] SUCCESS HANDLER CALLED!"); + + if (!(authentication.getPrincipal() instanceof DefaultOAuth2User oauthUser)) { + throw new IllegalStateException("Unknown principal type: " + authentication.getPrincipal().getClass()); + } + + // 1. CustomOAuth2UserService에서 넣어준 attributes 가져오기 + Map attrs = oauthUser.getAttributes(); + + String providerStr = (String) attrs.get("provider"); + String providerUid = (String) attrs.get("providerUid"); + if (providerStr == null) { + throw new IllegalStateException("OAuth provider attribute missing from attributes"); + } + + AuthProvider provider = + switch (providerStr) { + case "kakao" -> AuthProvider.KAKAO; + case "github" -> AuthProvider.GITHUB; + case "google" -> AuthProvider.GOOGLE; + default -> throw new IllegalStateException("Unknown OAuth provider: " + providerStr); + }; + + + // log.info("[OAuth2SuccessHandler] provider={}, providerUid={}", provider, providerUid); + + // DB 조회 + UserOauthAccount account = userOauthAccountRepository + .findByProviderAndProviderUid(provider, providerUid) + .orElseThrow(() -> new RuntimeException("소셜 계정이 DB에 없습니다. (회원가입 필요)")); + + User user = userRepository.findById(account.getUser().getUserId()) + .orElseThrow(() -> new RuntimeException("User not found")); + + // JWT 생성 + String accessToken = jwtProvider.createToken( + user.getUserId(), + user.getRole(), + user.getEmail() + ); + + + // 4. RefreshToken 생성 + String refreshToken = jwtProvider.createRefreshToken(user.getUserId()); + // 5. RefreshToken 저장(DB or Redis) + refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken); + + String[] activeProfiles = env.getActiveProfiles(); + List profiles = Arrays.asList(activeProfiles); + + boolean isProd = profiles.contains("prod"); + boolean isDev = profiles.contains("dev"); + + // SameSite, Secure 설정 (dev도 prod와 동일하게) + String sameSite = (isProd || isDev) ? "None" : "Lax"; + boolean secure = (isProd || isDev); + + // 도메인 설정 + String domain; + if (isProd) { + domain = "sjusisc.com"; // 운영 도메인 + } else if (isDev) { + domain = "sisc-web.duckdns.org"; // 개발 도메인 + } else { + domain = "localhost"; // 기본값 + } + + + + + // 6. HttpOnly 쿠키로 refreshToken 저장 + ResponseCookie.ResponseCookieBuilder accessCookieBuilder = ResponseCookie.from("access", accessToken) + .httpOnly(true) + .secure(secure) // 로컬=false, 배포=true + .sameSite(sameSite) // 로컬= "Lax", 배포="None" + .path("/") + .maxAge(60L * 60); // 1 hour + + // 로컬 환경에서는 domain 설정하지 않음 + if (isProd || isDev) { + accessCookieBuilder.domain(domain); + } + + ResponseCookie.ResponseCookieBuilder refreshCookieBuilder = ResponseCookie.from("refresh", refreshToken) + .httpOnly(true) + .secure(secure) + .sameSite(sameSite) + .path("/") + .maxAge(60L * 60 * 24 * 14); // 2 weeks + + // 로컬 환경에서는 domain 설정하지 않음 + if (isProd || isDev) { + refreshCookieBuilder.domain(domain); + } + + ResponseCookie accessCookie = accessCookieBuilder.build(); + ResponseCookie refreshCookie = refreshCookieBuilder.build(); + + + response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString()); + response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); + + + // 7. 프론트로 redirect + // application-local.yml → http://localhost:5173/oauth/success + // application-prod.yml → https://sisc-web.duckdns.org/oauth/success + //String redirectUrl = redirectSuccessBase; + // + "?accessToken=" + accessToken + // + "&name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) + // + "&userId=" + userId; + + // log.info("[OAuth2 Redirect] {}", redirectUrl); + + getRedirectStrategy().sendRedirect(request, response, redirectSuccessBase); + } + + } +} diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/GoogleServiceImpl.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/GoogleServiceImpl.java index bdfaa476..ae5edcc2 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/GoogleServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/GoogleServiceImpl.java @@ -1,8 +1,8 @@ -package org.sejongisc.backend.auth.service.oauth2; +package org.sejongisc.backend.common.auth.service.oauth2; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dto.oauth.GoogleTokenResponse; -import org.sejongisc.backend.auth.dto.oauth.GoogleUserInfoResponse; +import org.sejongisc.backend.common.auth.dto.oauth.GoogleTokenResponse; +import org.sejongisc.backend.common.auth.dto.oauth.GoogleUserInfoResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/KakaoServiceImpl.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/KakaoServiceImpl.java index 68442e6c..a13415f6 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/KakaoServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/KakaoServiceImpl.java @@ -1,9 +1,9 @@ -package org.sejongisc.backend.auth.service.oauth2; +package org.sejongisc.backend.common.auth.service.oauth2; import io.netty.handler.codec.http.HttpHeaderValues; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.dto.oauth.KakaoTokenResponse; -import org.sejongisc.backend.auth.dto.oauth.KakaoUserInfoResponse; +import org.sejongisc.backend.common.auth.dto.oauth.KakaoTokenResponse; +import org.sejongisc.backend.common.auth.dto.oauth.KakaoUserInfoResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/Oauth2Service.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/Oauth2Service.java similarity index 86% rename from backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/Oauth2Service.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/Oauth2Service.java index d634c87a..b17d731f 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/Oauth2Service.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/Oauth2Service.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service.oauth2; +package org.sejongisc.backend.common.auth.service.oauth2; public interface Oauth2Service { diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthStateService.java similarity index 80% rename from backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateService.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthStateService.java index 9b889354..a897794d 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthStateService.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service.oauth2; +package org.sejongisc.backend.common.auth.service.oauth2; import jakarta.servlet.http.HttpSession; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthStateServiceImpl.java similarity index 92% rename from backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthStateServiceImpl.java index 607fae7b..e19e6892 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthStateServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthStateServiceImpl.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service.oauth2; +package org.sejongisc.backend.common.auth.service.oauth2; import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthUnlinkService.java similarity index 89% rename from backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkService.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthUnlinkService.java index 99cf852d..4244ce46 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthUnlinkService.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service.oauth2; +package org.sejongisc.backend.common.auth.service.oauth2; public interface OauthUnlinkService { diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthUnlinkServiceImpl.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthUnlinkServiceImpl.java index 9721eea1..7ca0850f 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/OauthUnlinkServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/OauthUnlinkServiceImpl.java @@ -1,8 +1,8 @@ -package org.sejongisc.backend.auth.service.oauth2; +package org.sejongisc.backend.common.auth.service.oauth2; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.auth.service.oauth2.exception.OauthUnlinkException; +import org.sejongisc.backend.common.auth.service.oauth2.exception.OauthUnlinkException; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/exception/OauthUnlinkException.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/exception/OauthUnlinkException.java similarity index 78% rename from backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/exception/OauthUnlinkException.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/exception/OauthUnlinkException.java index 13cd1ee8..15376a8e 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/service/oauth2/exception/OauthUnlinkException.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/oauth2/exception/OauthUnlinkException.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.service.oauth2.exception; +package org.sejongisc.backend.common.auth.service.oauth2.exception; public class OauthUnlinkException extends RuntimeException { public OauthUnlinkException(String message) { diff --git a/backend/src/main/java/org/sejongisc/backend/auth/config/EmailProperties.java b/backend/src/main/java/org/sejongisc/backend/common/config/EmailProperties.java similarity index 93% rename from backend/src/main/java/org/sejongisc/backend/auth/config/EmailProperties.java rename to backend/src/main/java/org/sejongisc/backend/common/config/EmailProperties.java index c28e78ad..c2b8762f 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/config/EmailProperties.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/EmailProperties.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.auth.config; +package org.sejongisc.backend.common.config; import java.time.Duration; import lombok.Getter; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OpenApiConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/OpenApiConfig.java similarity index 91% rename from backend/src/main/java/org/sejongisc/backend/common/auth/config/OpenApiConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/OpenApiConfig.java index 7b90fee2..1239b2ec 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OpenApiConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/OpenApiConfig.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.auth.config; +package org.sejongisc.backend.common.config; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.servers.Server; diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java index 88304b14..3787f14c 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java @@ -80,7 +80,7 @@ public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory( .dataSource(dataSource) .packages( "org.sejongisc.backend.attendance.entity", - "org.sejongisc.backend.auth.entity", + "org.sejongisc.backend.common.auth.entity", "org.sejongisc.backend.backtest.entity", "org.sejongisc.backend.betting.entity", "org.sejongisc.backend.board.entity", diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/RestTemplateConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/RestTemplateConfig.java similarity index 93% rename from backend/src/main/java/org/sejongisc/backend/common/auth/config/RestTemplateConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/RestTemplateConfig.java index b34b9754..bf9252b6 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/RestTemplateConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/RestTemplateConfig.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.auth.config; +package org.sejongisc.backend.common.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/SecurityConfig.java similarity index 91% rename from backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/SecurityConfig.java index 0f573b7a..c4d8ccb6 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/SecurityConfig.java @@ -1,9 +1,10 @@ -package org.sejongisc.backend.common.auth.config; +package org.sejongisc.backend.common.config; import lombok.RequiredArgsConstructor; -import org.sejongisc.backend.common.auth.jwt.JwtAccessDeniedHandler; -import org.sejongisc.backend.common.auth.jwt.JwtAuthenticationEntryPoint; -import org.sejongisc.backend.common.auth.springsecurity.JwtAuthenticationFilter; +import org.sejongisc.backend.common.auth.service.oauth2.GithubServiceImpl; +import org.sejongisc.backend.common.exception.controller.JwtAccessDeniedHandler; +import org.sejongisc.backend.common.exception.controller.JwtAuthenticationEntryPoint; +import org.sejongisc.backend.common.auth.filter.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @@ -35,9 +36,9 @@ public class SecurityConfig { private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; - private final CustomOAuth2UserService customOAuth2UserService; - private final CustomOidcUserService customOidcUserService; - private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final GithubServiceImpl.CustomOAuth2UserService customOAuth2UserService; + private final GithubServiceImpl.CustomOidcUserService customOidcUserService; + private final GithubServiceImpl.OAuth2SuccessHandler oAuth2SuccessHandler; private final Environment env; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtAccessDeniedHandler.java b/backend/src/main/java/org/sejongisc/backend/common/exception/controller/JwtAccessDeniedHandler.java similarity index 95% rename from backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtAccessDeniedHandler.java rename to backend/src/main/java/org/sejongisc/backend/common/exception/controller/JwtAccessDeniedHandler.java index a6730bae..1ecaabe4 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtAccessDeniedHandler.java +++ b/backend/src/main/java/org/sejongisc/backend/common/exception/controller/JwtAccessDeniedHandler.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.auth.jwt; +package org.sejongisc.backend.common.exception.controller; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtAuthenticationEntryPoint.java b/backend/src/main/java/org/sejongisc/backend/common/exception/controller/JwtAuthenticationEntryPoint.java similarity index 95% rename from backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtAuthenticationEntryPoint.java rename to backend/src/main/java/org/sejongisc/backend/common/exception/controller/JwtAuthenticationEntryPoint.java index c148fc54..4c2bfa6d 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtAuthenticationEntryPoint.java +++ b/backend/src/main/java/org/sejongisc/backend/common/exception/controller/JwtAuthenticationEntryPoint.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.auth.jwt; +package org.sejongisc.backend.common.exception.controller; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; diff --git a/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java b/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java index c8fb0b48..6d1fc5da 100644 --- a/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java +++ b/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java @@ -4,7 +4,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.point.dto.PointHistoryResponse; import org.sejongisc.backend.point.service.PointHistoryService; import org.springframework.data.domain.PageRequest; diff --git a/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java b/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java index cdeca16a..e8faeced 100644 --- a/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java +++ b/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java @@ -3,7 +3,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.template.dto.TemplateRequest; import org.sejongisc.backend.template.dto.TemplateResponse; import org.sejongisc.backend.template.service.TemplateService; @@ -11,7 +12,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.List; import java.util.UUID; diff --git a/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java b/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java index 1ba9ee81..3bbef25e 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java +++ b/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java @@ -1,28 +1,25 @@ package org.sejongisc.backend.user.controller; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; -import org.sejongisc.backend.auth.dto.SignupRequest; -import org.sejongisc.backend.auth.dto.SignupResponse; +import org.sejongisc.backend.common.auth.controller.AuthCookieHelper; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.LoginResponse; +import org.sejongisc.backend.common.auth.dto.SignupRequest; +import org.sejongisc.backend.common.auth.dto.SignupResponse; +import org.sejongisc.backend.common.auth.service.RefreshTokenService; import org.sejongisc.backend.user.dto.*; import org.sejongisc.backend.user.service.UserService; -import org.sejongisc.backend.user.service.projection.UserIdNameProjection; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.List; import java.util.Map; import java.util.UUID; @@ -33,306 +30,56 @@ @Tag(name = "사용자 API", description = "회원 정보 조회 및 수정 관련 API") public class UserController { - private final UserService userService; - - @Operation( - summary = "내 정보 조회 API", - description = "로그인된 사용자의 정보를 조회합니다. Access Token이 필요합니다.", - responses = { - @ApiResponse( - responseCode = "200", - description = "조회 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UserInfoResponse.class), - examples = @ExampleObject(value = """ - { - "userId": "9f6d0e22-45f1-4e5e-bc94-f1f6e7d28b44", - "name": "홍길동", - "email": "testuser@example.com", - "phoneNumber": "01012345678", - "point": 1500, - "role": "USER", - "authorities": ["ROLE_USER"] - } - """) - ) - ), - @ApiResponse( - responseCode = "401", - description = "인증되지 않은 사용자", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "인증이 필요합니다." - } - """)) - ) - } - ) - @GetMapping("/details") - public ResponseEntity getUserInfo(@AuthenticationPrincipal CustomUserDetails user) { - if (user == null) { - log.warn("인증되지 않은 사용자 접근 시도"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(Map.of("message", "인증이 필요합니다.")); - } - - log.info("email: {} 권한: {}", user.getUsername(), user.getAuthorities()); - - UserInfoResponse response = new UserInfoResponse( - user.getUserId(), - user.getName(), - user.getEmail(), - user.getPhoneNumber(), - user.getPoint(), - user.getRole().name(), - user.getAuthorities() - ); - - return ResponseEntity.ok(response); - } - - @Operation( - summary = "회원 정보 수정 API", - description = "회원 정보를 수정합니다. 인증된 사용자만 이용 가능하며 본인 정보만 수정할 수 있습니다.", - responses = { - @ApiResponse( - responseCode = "200", - description = "수정 성공", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "회원 정보가 수정되었습니다." - } - """)) - ), - @ApiResponse( - responseCode = "401", - description = "인증되지 않은 사용자", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "인증 정보가 필요합니다." - } - """)) - ), - @ApiResponse( - responseCode = "403", - description = "본인 이외의 정보 수정 시도", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "본인의 정보만 수정할 수 있습니다." - } - """)) - ) - } - ) - @PatchMapping("/{userId}") - public ResponseEntity updateUser( - @PathVariable UUID userId, - @RequestBody @Valid UserUpdateRequest request, - @AuthenticationPrincipal CustomUserDetails authenticatedUser - ) { -// if(authenticatedUser == null){ -// return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "인증 정보가 필요합니다.")); -// } - - log.info("인증된 사용자 ID={}, 요청한 userId={}", authenticatedUser.getUserId(), userId); - - // 본인 허용 - if (!authenticatedUser.getUserId().equals(userId)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(Map.of("message", "본인의 정보만 수정할 수 있습니다.")); - } - - userService.updateUser(userId, request); - return ResponseEntity.ok("회원 정보가 수정되었습니다."); - } - - @Operation( - summary = "아이디 찾기 API", - description = """ - 사용자의 이름과 전화번호를 입력하면 가입된 이메일 주소를 반환합니다. - - 이름(name)과 전화번호(phoneNumber)가 모두 일치하는 회원만 조회됩니다. - - 일치하는 회원이 없을 경우 404 응답을 반환합니다. - """, - responses = { - @ApiResponse( - responseCode = "200", - description = "조회 성공", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "email": "testuser@example.com" - } - """) - ) - ), - @ApiResponse( - responseCode = "404", - description = "일치하는 회원 없음", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "해당 정보로 가입된 사용자를 찾을 수 없습니다." - } - """) - ) - ) - } - ) - @PostMapping("/id/find") - public ResponseEntity findUserID(@RequestBody @Valid UserIdFindRequest request) { - String name = request.name(); - String phone = request.phoneNumber(); - String email = userService.findEmailByNameAndPhone(name, phone); - - if (email == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(Map.of("message", "해당 정보로 가입된 사용자를 찾을 수 없습니다.")); - } - - return ResponseEntity.ok(Map.of("email", email)); - } - - @Operation( - summary = "비밀번호 재설정: 인증코드 발송 API", - description = """ - 가입된 이메일 주소로 비밀번호 재설정을 위한 인증코드를 전송합니다. - - 인증코드는 3분간 유효합니다. - - 존재하지 않는 이메일일 경우 404 에러를 반환합니다. - """, - responses = { - @ApiResponse( - responseCode = "200", - description = "인증코드 발송 성공", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "인증코드를 전송했습니다." - } - """) - ) - ), - @ApiResponse( - responseCode = "404", - description = "이메일 미존재", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "해당 이메일로 가입된 사용자를 찾을 수 없습니다." - } - """) - ) - ) - } - ) - @PostMapping("/password/reset/send") - public ResponseEntity sendReset(@RequestBody @Valid PasswordResetSendRequest req){ - String email = req.email().trim(); - log.info("비밀번호 재설정 요청"); // 개인정보 로그 남기지 않기 - userService.passwordReset(email); - return ResponseEntity.ok(Map.of("message", "인증코드를 전송했습니다.")); - } - - @Operation( - summary = "비밀번호 재설정: 인증코드 검증 API", - description = """ - 이메일과 인증코드를 검증하고, 유효한 경우 비밀번호 재설정용 토큰(`resetToken`)을 발급합니다. - - 인증코드는 3분간만 유효합니다. - - 검증에 성공하면 resetToken(10분 유효)을 반환합니다. - """, - responses = { - @ApiResponse( - responseCode = "200", - description = "검증 성공 및 resetToken 발급", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "resetToken": "c8a2434d-7e11-4f7e-a201-b9fbc9d7d43a" - } - """) - ) - ), - @ApiResponse( - responseCode = "400", - description = "잘못된 코드 또는 만료된 코드", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "인증코드가 올바르지 않거나 만료되었습니다." - } - """) - ) - ) - } - ) - @PostMapping("/password/reset/verify") - public ResponseEntity verifyReset(@RequestBody @Valid PasswordResetVerifyRequest req){ - String email = req.email().trim(); - String code = req.code().trim(); - - String token = userService.verifyResetCodeAndIssueToken(email, code); - return ResponseEntity.ok(Map.of("resetToken", token)); - } - - @Operation( - summary = "비밀번호 재설정 최종 API", - description = """ - 검증된 resetToken과 새 비밀번호를 전달하면 비밀번호를 최종 변경합니다. - - resetToken은 10분간 유효합니다. - - 비밀번호 정책: - • 길이: 8~20자 - • 최소 1개의 대문자(A-Z) - • 최소 1개의 소문자(a-z) - • 최소 1개의 숫자(0-9) - • 최소 1개의 특수문자(!@#$%^&*()_+=-{};:'",.<>/?) - - 위 조건을 만족하지 않으면 400 응답을 반환합니다. - - 변경 완료 후, 로그인 화면으로 이동하여 새 비밀번호로 로그인할 수 있습니다. - """, - responses = { - @ApiResponse( - responseCode = "200", - description = "비밀번호 변경 성공", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "비밀번호가 변경되었습니다. 다시 로그인해 주세요." - } - """) - ) - ), - @ApiResponse( - responseCode = "400", - description = "비밀번호 정책 위반 또는 잘못된/만료된 토큰", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "비밀번호는 8~20자, 대소문자/숫자/특수문자를 모두 포함해야 합니다." - } - """) - ) - ) - } - ) - @PostMapping("/password/reset/commit") - public ResponseEntity commitReset(@RequestBody @Valid PasswordResetCommitRequest req){ - userService.resetPasswordByToken(req.resetToken(), req.newPassword()); - return ResponseEntity.ok(Map.of("message", "비밀번호가 변경되었습니다. 다시 로그인해 주세요.")); - } - - - - - + private final UserService userService; + private final RefreshTokenService refreshTokenService; + private final AuthCookieHelper authCookieHelper; + + @Operation(summary = "회원 가입", description = "회장이 승인하기 전까지 PENDING 상태가 유지되며, 웹사이트를 사용할 수 없습니다.") + @ApiResponse(responseCode = "201", description = "회원가입 성공") + @PostMapping("/signup") + public ResponseEntity signup(@Valid @RequestBody SignupRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(userService.signup(request)); + } + + @Operation(summary = "회원 탈퇴", description = "리프레시 토큰을 삭제합니다.") + @DeleteMapping("/withdraw") + public ResponseEntity withdraw(@AuthenticationPrincipal CustomUserDetails user) { + userService.deleteUserWithOauth(user.getUserId()); + refreshTokenService.deleteByUserId(user.getUserId()); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, authCookieHelper.deleteCookie("refresh").toString()) + .body(Map.of("message", "회원 탈퇴가 완료되었습니다.")); + } + + @Operation(summary = "내 정보 조회") + @GetMapping("/details") + public ResponseEntity getUserInfo(@AuthenticationPrincipal CustomUserDetails user) { + return ResponseEntity.ok(new UserInfoResponse(user.getUserId(), user.getName(), user.getEmail(), user.getPhoneNumber(), user.getPoint(), user.getRole().name(), user.getAuthorities())); + } + + @Operation(summary = "내 정보 수정") + @PatchMapping("/{userId}") + public ResponseEntity updateUser( + @PathVariable UUID userId, + @RequestBody @Valid UserUpdateRequest request, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + userService.updateUser(userId, request); + return ResponseEntity.ok("회원 정보가 수정되었습니다."); + } + + @Operation(summary = "아이디 찾기") + @PostMapping("/id/find") + public ResponseEntity findUserID(@RequestBody @Valid UserIdFindRequest request) { + String email = userService.findEmailByNameAndPhone(request.name(), request.phoneNumber()); + return ResponseEntity.ok(Map.of("email", email)); + } + + @Operation(summary = "비밀번호 재설정") + @PostMapping("/password/reset/send") + public ResponseEntity sendReset(@RequestBody @Valid PasswordResetSendRequest req){ + userService.passwordReset(req.email().trim()); + return ResponseEntity.ok(Map.of("message", "인증코드를 전송했습니다.")); + } } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java index 262631e3..2defc4e7 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java @@ -5,7 +5,8 @@ import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Getter; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.user.entity.User; @Getter diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java index 441289a6..92c57916 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.*; -import org.sejongisc.backend.auth.entity.UserOauthAccount; +import org.sejongisc.backend.common.auth.entity.UserOauthAccount; import org.sejongisc.backend.common.entity.postgres.BasePostgresEntity; import java.util.ArrayList; diff --git a/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java index f50fb81d..c7f88e34 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java @@ -16,18 +16,8 @@ public interface UserRepository extends JpaRepository { Optional findUserByEmail(String email); - List findAllByOrderByPointDesc(); - Optional findByNameAndPhoneNumber(String name, String phoneNumber); - @Query(""" - select u.userId as userId, - u.name as name, - u.email as email - from User u - """) - List findAllUserIdAndName(); - @Query( "SELECT u FROM User u " + "LEFT JOIN Account a ON u.userId = a.ownerId " + diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java index 344f5a80..a70ee3a3 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java @@ -1,37 +1,280 @@ package org.sejongisc.backend.user.service; -import org.sejongisc.backend.auth.dto.SignupRequest; -import org.sejongisc.backend.auth.dto.SignupResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.common.auth.dto.SignupRequest; +import org.sejongisc.backend.common.auth.dto.SignupResponse; +import org.sejongisc.backend.common.auth.entity.AuthProvider; +import org.sejongisc.backend.common.auth.entity.UserOauthAccount; +import org.sejongisc.backend.common.auth.dto.oauth.OauthUserInfo; +import org.sejongisc.backend.common.auth.repository.UserOauthAccountRepository; +import org.sejongisc.backend.common.auth.service.EmailService; +import org.sejongisc.backend.common.auth.service.RefreshTokenService; +import org.sejongisc.backend.common.auth.service.oauth2.OauthUnlinkService; +import org.sejongisc.backend.common.annotation.OptimisticRetry; +import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; +import org.sejongisc.backend.common.exception.CustomException; +import org.sejongisc.backend.common.exception.ErrorCode; +import org.sejongisc.backend.point.dto.AccountEntry; +import org.sejongisc.backend.point.entity.Account; +import org.sejongisc.backend.point.entity.AccountName; +import org.sejongisc.backend.point.entity.TransactionReason; +import org.sejongisc.backend.point.service.AccountService; +import org.sejongisc.backend.point.service.PointLedgerService; import org.sejongisc.backend.user.dto.UserUpdateRequest; +import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; -import org.sejongisc.backend.auth.dto.oauth.OauthUserInfo; -import org.sejongisc.backend.user.service.projection.UserIdNameProjection; +import org.sejongisc.backend.user.repository.UserRepository; +import org.sejongisc.backend.user.util.PasswordPolicyValidator; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; import java.util.List; import java.util.UUID; -public interface UserService { - SignupResponse signUp(SignupRequest dto); +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { - User findOrCreateUser(OauthUserInfo oauthInfo); + private final UserRepository userRepository; + private final UserOauthAccountRepository oauthAccountRepository; + private final OauthUnlinkService oauthUnlinkService; + private final PasswordEncoder passwordEncoder; + private final TokenEncryptor tokenEncryptor; + private final EmailService emailService; + private final RedisTemplate redisTemplate; + private final RefreshTokenService refreshTokenService; + private final AccountService accountService; + private final PointLedgerService pointLedgerService; - void updateUser(UUID userId, UserUpdateRequest request); + // --- 핵심 회원 서비스 --- - User getUserById(UUID userId); + @Transactional + @OptimisticRetry + public SignupResponse signup(SignupRequest dto) { + validateDuplicateUser(dto.getEmail(), dto.getPhoneNumber()); - void deleteUserWithOauth(UUID userId); + String trimmedPassword = validateAndGetTrimmedPassword(dto.getPassword()); + PasswordPolicyValidator.validate(trimmedPassword); - String findEmailByNameAndPhone(String name, String phoneNumber); + User user = User.builder() + .name(dto.getName()) + .email(dto.getEmail()) + .passwordHash(passwordEncoder.encode(trimmedPassword)) + .role(dto.getRole() != null ? dto.getRole() : Role.TEAM_MEMBER) + .point(0) + .phoneNumber(dto.getPhoneNumber()) + .build(); - void passwordReset(String email); + try { + User saved = userRepository.save(user); + Account userAccount = accountService.createUserAccount(user.getUserId()); + pointLedgerService.processTransaction( + TransactionReason.SIGNUP_REWARD, + user.getUserId(), + AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), 100L), + AccountEntry.debit(userAccount, 100L) + ); + log.info("포인트 계정 생성 및 초기 포인트 지급 완료: {}", user.getEmail()); + return SignupResponse.from(saved); + } catch (DataIntegrityViolationException e) { + throw new CustomException(ErrorCode.DUPLICATE_USER); + } + } - String verifyResetCodeAndIssueToken(String email, String code); + @Transactional + public void updateUser(UUID userId, UserUpdateRequest request) { + User user = findUserById(userId); - void resetPasswordByToken(String resetToken, String newPassword); + if (isNotBlank(request.getName())) { + user.setName(request.getName().trim()); + } - User upsertOAuthUser(String provider, String providerId, String email, String name); + if (isNotBlank(request.getPhoneNumber())) { + user.setPhoneNumber(request.getPhoneNumber().trim()); + } - List getUserProjectionList(); + if (request.getPassword() != null) { + String trimmedPassword = validateAndGetTrimmedPassword(request.getPassword()); + PasswordPolicyValidator.validate(trimmedPassword); + user.setPasswordHash(passwordEncoder.encode(trimmedPassword)); + } - List findAllUsersMissingAccount(); -} + log.info("회원 정보 수정 완료: userId={}", userId); + } + + + public String findEmailByNameAndPhone(String name, String phone) { + String nName = validateNotBlank(name, "이름"); + String nPhone = validateNotBlank(phone, "전화번호"); + + return userRepository.findByNameAndPhoneNumber(nName, nPhone) + .map(User::getEmail) + .orElse(null); + } + + public void passwordReset(String email) { + String nEmail = validateNotBlank(email, "이메일"); + + if (!userRepository.existsByEmail(nEmail)) { + log.debug("존재하지 않는 이메일로 비밀번호 재설정 요청: {}", nEmail); + return; + } + + emailService.sendResetEmail(nEmail); + } + + public String verifyResetCodeAndIssueToken(String email, String code) { + String nEmail = validateNotBlank(email, "이메일"); + String nCode = validateNotBlank(code, "인증코드"); + + emailService.verifyResetEmail(nEmail, nCode); + + String token = UUID.randomUUID().toString(); + saveResetTokenToRedis(token, nEmail); + + return token; + } + + @Transactional + public void resetPasswordByToken(String resetToken, String newPassword) { + String email = getEmailFromRedis(resetToken); + User user = userRepository.findUserByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + String trimmedPassword = validateAndGetTrimmedPassword(newPassword); + PasswordPolicyValidator.validate(trimmedPassword); + + user.setPasswordHash(passwordEncoder.encode(trimmedPassword)); + + deleteResetTokenFromRedis(resetToken); + refreshTokenService.deleteByUserId(user.getUserId()); + } + + public List findAllUsersMissingAccount() { + return userRepository.findAllUsersMissingAccount(); + } + + // --- 내부 헬퍼 메서드 --- + + private User findUserById(UUID userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + } + + private void validateDuplicateUser(String email, String phone) { + if (userRepository.existsByEmail(email)) throw new CustomException(ErrorCode.DUPLICATE_EMAIL); + if (userRepository.existsByPhoneNumber(phone)) throw new CustomException(ErrorCode.DUPLICATE_PHONE); + } + + private String validateAndGetTrimmedPassword(String password) { + if (password == null || password.trim().isEmpty()) { + throw new CustomException(ErrorCode.INVALID_INPUT); + } + return password.trim(); + } + + private String validateNotBlank(String value, String fieldName) { + if (value == null || value.trim().isEmpty()) { + throw new CustomException(ErrorCode.INVALID_INPUT); + } + return value.trim(); + } + + private boolean isNotBlank(String value) { + return value != null && !value.trim().isEmpty(); + } + + private void completeSignup(User user) { + + } + + private void saveResetTokenToRedis(String token, String email) { + try { + redisTemplate.opsForValue().set("PASSWORD_RESET:" + token, email, Duration.ofMinutes(10)); + } catch (Exception e) { + log.error("Redis 저장 실패", e); + throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + + private String getEmailFromRedis(String token) { + try { + String email = (String) redisTemplate.opsForValue().get("PASSWORD_RESET:" + token); + if (email == null) throw new CustomException(ErrorCode.EMAIL_CODE_NOT_FOUND); + return email; + } catch (Exception e) { + log.error("Redis 조회 실패", e); + throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + + private void deleteResetTokenFromRedis(String token) { + try { + redisTemplate.delete("PASSWORD_RESET:" + token); + } catch (Exception e) { + log.error("Redis 삭제 실패", e); + } + } + + // ------------------------ (비활성화) OAuth2 관련 로직 ------------------------ + + @Transactional + public User upsertOAuthUser(String provider, String providerUid, String email, String name) { + AuthProvider authProvider = AuthProvider.valueOf(provider.toUpperCase()); + return oauthAccountRepository.findByProviderAndProviderUid(authProvider, providerUid) + .map(UserOauthAccount::getUser) + .orElseGet(() -> { + User savedUser = userRepository.save(User.builder().email(email).name(name).role(Role.TEAM_MEMBER).build()); + oauthAccountRepository.save(UserOauthAccount.builder().user(savedUser).provider(authProvider).providerUid(providerUid).build()); + return savedUser; + }); + } + + // 기존 findOrCreateUser는 upsertOAuthUser와 로직이 겹치므로 통합 권장하나, 유지 시 하단에 배치 + @Transactional + @OptimisticRetry + public User findOrCreateUser(OauthUserInfo oauthInfo) { + return oauthAccountRepository.findByProviderAndProviderUid(oauthInfo.getProvider(), oauthInfo.getProviderUid()) + .map(UserOauthAccount::getUser) + .orElseGet(() -> { + User savedUser = userRepository.save(User.builder().name(oauthInfo.getName()).role(Role.TEAM_MEMBER).build()); + Account userAccount = accountService.createUserAccount(savedUser.getUserId()); + pointLedgerService.processTransaction( + TransactionReason.SIGNUP_REWARD, + savedUser.getUserId(), + AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), 100L), + AccountEntry.debit(userAccount, 100L) + ); + log.info("포인트 계정 생성 및 초기 포인트 지급 완료: {}", savedUser.getEmail()); + oauthAccountRepository.save(UserOauthAccount.builder() + .user(savedUser).provider(oauthInfo.getProvider()).providerUid(oauthInfo.getProviderUid()) + .accessToken(tokenEncryptor.encrypt(oauthInfo.getAccessToken())).build()); + return savedUser; + }); + } + + @Transactional + public void deleteUserWithOauth(UUID userId) { + User user = findUserById(userId); + user.getOauthAccounts().forEach(account -> { + String provider = account.getProvider().name().toLowerCase(); + String accessToken = tokenEncryptor.decrypt(account.getAccessToken()); + switch (provider) { + case "kakao" -> oauthUnlinkService.unlinkKakao(accessToken); + case "google" -> oauthUnlinkService.unlinkGoogle(accessToken); + case "github" -> oauthUnlinkService.unlinkGithub(accessToken); + default -> log.warn("지원하지 않는 소셜 서비스: {}", provider); + } + }); + + userRepository.delete(user); + log.info("회원 탈퇴 완료: userId={}", userId); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java deleted file mode 100644 index 07bc65b8..00000000 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java +++ /dev/null @@ -1,397 +0,0 @@ -package org.sejongisc.backend.user.service; - - -import org.sejongisc.backend.auth.entity.AuthProvider; -import org.sejongisc.backend.auth.service.EmailService; -import org.sejongisc.backend.auth.service.oauth2.OauthUnlinkService; -import org.sejongisc.backend.auth.service.RefreshTokenService; -import org.sejongisc.backend.common.annotation.OptimisticRetry; -import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; -import org.sejongisc.backend.point.dto.AccountEntry; -import org.sejongisc.backend.point.entity.Account; -import org.sejongisc.backend.point.entity.AccountName; -import org.sejongisc.backend.point.entity.TransactionReason; -import org.sejongisc.backend.point.service.AccountService; -import org.sejongisc.backend.point.service.PointLedgerService; -import org.sejongisc.backend.user.service.projection.UserIdNameProjection; -import org.sejongisc.backend.user.util.PasswordPolicyValidator; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.common.exception.CustomException; -import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; -import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.auth.dto.SignupRequest; -import org.sejongisc.backend.auth.dto.SignupResponse; -import org.sejongisc.backend.user.dto.UserUpdateRequest; -import org.sejongisc.backend.user.entity.Role; -import org.sejongisc.backend.user.entity.User; -import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.auth.dto.oauth.OauthUserInfo; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.time.Duration; -import java.util.List; -import java.util.UUID; - - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional -public class UserServiceImpl implements UserService { - - private final UserRepository userRepository; - private final UserOauthAccountRepository oauthAccountRepository; - private final OauthUnlinkService oauthUnlinkService; - private final PasswordEncoder passwordEncoder; - private final TokenEncryptor tokenEncryptor; - private final EmailService emailService; - private final RedisTemplate redisTemplate; - private final RefreshTokenService refreshTokenService; - private final AccountService accountService; - private final PointLedgerService pointLedgerService; - - - @Override - @Transactional - @OptimisticRetry - public SignupResponse signUp(SignupRequest dto) { - log.debug("[SIGNUP] request: {}", dto.getEmail()); - if (userRepository.existsByEmail(dto.getEmail())) { - throw new CustomException(ErrorCode.DUPLICATE_EMAIL); - } - - if (userRepository.existsByPhoneNumber(dto.getPhoneNumber())) { - throw new CustomException(ErrorCode.DUPLICATE_PHONE); - } - - // trim 적용 후 검증 및 저장 - String rawPassword = dto.getPassword(); - String trimmedPassword = rawPassword == null ? null : rawPassword.trim(); - - // null / 공백 검사 - if (trimmedPassword == null || trimmedPassword.isEmpty()) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - - // 비밀번호 정책 검증 (trim된 값으로) - PasswordPolicyValidator.validate(trimmedPassword); - - // 패스워드 인코딩 (trim된 값 사용) - String encodedPw = passwordEncoder.encode(trimmedPassword); - - Role role = dto.getRole(); - if (role == null) { - role = Role.TEAM_MEMBER; - } - - User user = User.builder() - .name(dto.getName()) - .email(dto.getEmail()) - .passwordHash(encodedPw) - .role(role) - .point(0) - .phoneNumber(dto.getPhoneNumber()) - .build(); - - try { - User saved = userRepository.save(user); - // 포인트 계정 생성 및 기본 포인트 제공 - completeSignup(saved); - return SignupResponse.from(saved); - } catch (DataIntegrityViolationException e) { - throw new CustomException(ErrorCode.DUPLICATE_USER); - } - - } - - @Override - @Transactional - @OptimisticRetry - public User findOrCreateUser(OauthUserInfo oauthInfo) { - String providerUid = oauthInfo.getProviderUid(); - - // 기존 OAuth 계정 찾기 - return oauthAccountRepository - .findByProviderAndProviderUid(oauthInfo.getProvider(), providerUid) - .map(UserOauthAccount::getUser) - .orElseGet(() -> { - // 새로운 User 생성 - User newUser = User.builder() - .name(oauthInfo.getName()) - .role(Role.TEAM_MEMBER) - .build(); - - User savedUser = userRepository.save(newUser); - - completeSignup(savedUser); - - String encryptedToken = tokenEncryptor.encrypt(oauthInfo.getAccessToken()); - - UserOauthAccount newOauth = UserOauthAccount.builder() - .user(savedUser) - .provider(oauthInfo.getProvider()) - .providerUid(providerUid) - .accessToken(encryptedToken) - .build(); - - oauthAccountRepository.save(newOauth); - - return savedUser; - }); - } - - @Override - @Transactional - public void updateUser(UUID userId, UserUpdateRequest request) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 이름 업데이트 - if (request.getName() != null && !request.getName().trim().isEmpty()) { - user.setName(request.getName().trim()); - } - - // 전화번호 업데이트 - if (request.getPhoneNumber() != null && !request.getPhoneNumber().trim().isEmpty()) { - user.setPhoneNumber(request.getPhoneNumber().trim()); - } - - if (request.getPassword() != null) { - String trimmedPassword = request.getPassword().trim(); - if (trimmedPassword.isEmpty()) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - user.setPasswordHash(passwordEncoder.encode(trimmedPassword)); - } - - log.info("회원 정보가 수정되었습니다. userId={}", userId); - userRepository.save(user); - } - - @Override - @Transactional - public User getUserById(UUID userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - } - - - @Override - @Transactional - public void deleteUserWithOauth(UUID userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // Lazy 로딩 강제 초기화 (안정성 보강) - user.getOauthAccounts().size(); - - // 연동된 OAuth 계정이 있을 경우 모두 해제 - if (!user.getOauthAccounts().isEmpty()) { - for (UserOauthAccount account : user.getOauthAccounts()) { - String provider = account.getProvider().name(); - String providerUid = account.getProviderUid(); - String accessToken = tokenEncryptor.decrypt(account.getAccessToken()); - - log.info("연결된 OAuth 계정 해제 중: provider={}, userId={}", provider, userId); - - // Kakao / Google / GitHub 연동 해제 서비스 연결 - switch (provider.toLowerCase()) { - case "kakao" -> oauthUnlinkService.unlinkKakao(accessToken); - case "google" -> oauthUnlinkService.unlinkGoogle(accessToken); - case "github" -> oauthUnlinkService.unlinkGithub(accessToken); - default -> log.warn("지원하지 않는 provider: {}", provider); - } - } - } - - // Refresh Token (추후 구현 시 삭제) - //refreshTokenRepository.deleteByUserId(userId); - - // User 삭제 (연관된 OAuthAccount는 Cascade로 자동 삭제) - userRepository.delete(user); - log.info("회원 탈퇴 완료: userId={}", userId); - } - - @Override - public String findEmailByNameAndPhone(String name, String phone){ - String normalizedName = name == null ? null : name.trim(); - String normalizedPhone = phone == null ? null : phone.trim(); - - if (normalizedName == null || normalizedName.isEmpty() || - normalizedPhone == null || normalizedPhone.isEmpty()) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - - return userRepository.findByNameAndPhoneNumber(normalizedName, normalizedPhone) - .map(User::getEmail) - .orElse(null); - } - - @Override - public void passwordReset(String email) { - if (email == null) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - - String normalizedEmail = email.trim(); - if (normalizedEmail.isEmpty()) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - - if (!userRepository.existsByEmail(normalizedEmail)) { - log.debug("Password reset requested for non-existent email: {}", normalizedEmail); - return; - } - - // 정상적인 이메일일 경우만 발송 - emailService.sendResetEmail(normalizedEmail); - } - - @Override - public String verifyResetCodeAndIssueToken(String email, String code) { - if (email == null || code == null) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - - String normalizedEmail = email.trim(); - String normalizedCode = code.trim(); - - if (normalizedEmail.isEmpty() || normalizedCode.isEmpty()) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - - // 정규화된 값으로 검증 - emailService.verifyResetEmail(normalizedEmail, normalizedCode); - - // 토큰 발급 - String token = UUID.randomUUID().toString(); - - try { - redisTemplate.opsForValue().set( - "PASSWORD_RESET:" + token, - normalizedEmail, - Duration.ofMinutes(10) - ); - } catch (Exception e) { - log.error("Redis 연결 실패: 비밀번호 재설정 토큰 저장 불가", e); - throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); - } - - return token; - } - - @Override - @Transactional - public void resetPasswordByToken(String resetToken, String newPassword) { -// String email = (String) redisTemplate.opsForValue().get("PASSWORD_RESET:" + resetToken); - String email = null; - - try { - email = (String) redisTemplate.opsForValue().get("PASSWORD_RESET:" + resetToken); - } catch (Exception e) { - log.error("Redis 연결 실패 - 비밀번호 재설정 토큰 조회 불가", e); - throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); - } - - if(email == null) { - throw new CustomException(ErrorCode.EMAIL_CODE_NOT_FOUND); - } - - User user = userRepository.findUserByEmail(email) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - if (newPassword == null) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - - String trimmedPassword = newPassword.trim(); - if (trimmedPassword.isEmpty()) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - - // 반드시 trim된 값으로 정책 검증 - PasswordPolicyValidator.validate(trimmedPassword); - - // trim된 값을 인코딩하여 저장 - user.setPasswordHash(passwordEncoder.encode(trimmedPassword)); - userRepository.save(user); - - try { - redisTemplate.delete("PASSWORD_RESET:" + resetToken); - } catch (Exception e) { - log.error("Redis 연결 실패 - 비밀번호 재설정 토큰 삭제 불가", e); - throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); - } - - refreshTokenService.deleteByUserId(user.getUserId()); - } - - - - @Override - @Transactional - public User upsertOAuthUser(String provider, String providerUid, String email, String name) { - - AuthProvider authProvider = AuthProvider.valueOf(provider.toUpperCase()); - - return oauthAccountRepository - .findByProviderAndProviderUid(authProvider, providerUid) - .map(UserOauthAccount::getUser) - .orElseGet(() -> { - User newUser = User.builder() - .email(email) - .name(name) - .role(Role.TEAM_MEMBER) - .build(); - - User savedUser = userRepository.save(newUser); - - UserOauthAccount oauthAccount = UserOauthAccount.builder() - .user(savedUser) - .provider(authProvider) - .providerUid(providerUid) - .build(); - - oauthAccountRepository.save(oauthAccount); - - return savedUser; - }); - } - - @Override - public List getUserProjectionList() { - return userRepository.findAllUserIdAndName(); - } - - /** - * 포인트 계정이 존재하지 않는 사용자 리스트 조회 - */ - @Override - public List findAllUsersMissingAccount() { - return userRepository.findAllUsersMissingAccount(); - } - - /** - * 사용자의 포인트 계정 생성 및 기본 포인트 지급 - */ - private void completeSignup(User user) { - // 사용자의 포인트 계정 생성 - Account userAccount = accountService.createUserAccount(user.getUserId()); - - // 회원가입 포인트 지급 - pointLedgerService.processTransaction( - TransactionReason.SIGNUP_REWARD, - user.getUserId(), - AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), 100L), - AccountEntry.debit(userAccount, 100L) - ); - - log.info("회원가입 완료: 회원가입 및 초기 포인트 지급이 완료되었습니다. User: {}", user.getEmail()); - } -} diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceControllerTest.java b/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceControllerTest.java index 61e36c7e..635df7a5 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceControllerTest.java @@ -2,34 +2,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.sejongisc.backend.attendance.service.AttendanceService; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; -import org.sejongisc.backend.user.entity.Role; -import org.sejongisc.backend.user.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; -import org.springframework.http.MediaType; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(AttendanceController.class) @Import(TestSecurityConfig.class) diff --git a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java index 63df5e3a..a19fd760 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java @@ -11,14 +11,15 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.sejongisc.backend.auth.dto.*; -import org.sejongisc.backend.auth.dto.oauth.*; -import org.sejongisc.backend.auth.service.LoginService; -import org.sejongisc.backend.auth.service.oauth2.Oauth2Service; -import org.sejongisc.backend.auth.service.oauth2.OauthStateService; -import org.sejongisc.backend.auth.service.RefreshTokenService; +import org.sejongisc.backend.common.auth.controller.AuthController; +import org.sejongisc.backend.common.auth.controller.AuthCookieHelper; +import org.sejongisc.backend.common.auth.dto.*; +import org.sejongisc.backend.common.auth.dto.oauth.*; +import org.sejongisc.backend.common.auth.service.AuthService; +import org.sejongisc.backend.common.auth.service.oauth2.Oauth2Service; +import org.sejongisc.backend.common.auth.service.oauth2.OauthStateService; +import org.sejongisc.backend.common.auth.service.RefreshTokenService; import org.sejongisc.backend.common.auth.jwt.JwtProvider; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.common.exception.controller.GlobalExceptionHandler; @@ -59,11 +60,14 @@ class AuthControllerTest { @Mock Oauth2Service kakaoService; @Mock Oauth2Service githubService; - @Mock LoginService loginService; + @Mock + AuthService authService; @Mock UserService userService; @Mock JwtProvider jwtProvider; @Mock OauthStateService oauthStateService; @Mock RefreshTokenService refreshTokenService; + @Mock + AuthCookieHelper authCookieHelper; @InjectMocks AuthController authController; @@ -79,14 +83,7 @@ void setUp() { "GITHUB", githubService ); - authController = new AuthController( - oauth2Services, - loginService, - userService, - jwtProvider, - oauthStateService, - refreshTokenService - ); + authController = new AuthController(authService, refreshTokenService, authCookieHelper); objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); @@ -127,7 +124,7 @@ void login_success() throws Exception { .point(100) .build(); - when(loginService.login(any(LoginRequest.class))).thenReturn(resp); + when(authService.login(any(LoginRequest.class))).thenReturn(resp); mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON) @@ -144,7 +141,7 @@ void login_success() throws Exception { @Test @DisplayName("POST /api/auth/login - 존재하지 않는 사용자면 404 반환") void login_userNotFound() throws Exception { - when(loginService.login(any(LoginRequest.class))) + when(authService.login(any(LoginRequest.class))) .thenThrow(new CustomException(ErrorCode.USER_NOT_FOUND)); LoginRequest req = new LoginRequest("notfound@example.com", "Password123!"); @@ -159,7 +156,7 @@ void login_userNotFound() throws Exception { @Test @DisplayName("POST /api/auth/login - 비밀번호 틀리면 401 반환") void login_wrongPassword() throws Exception { - when(loginService.login(any(LoginRequest.class))) + when(authService.login(any(LoginRequest.class))) .thenThrow(new CustomException(ErrorCode.UNAUTHORIZED)); LoginRequest req = new LoginRequest("hong@example.com", "WrongPassword!"); @@ -262,7 +259,7 @@ void logout_success() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("로그아웃 성공")); - verify(loginService, times(1)).logout(token); + verify(authService, times(1)).logout(token); } // Authorization 헤더 누락 @@ -272,14 +269,14 @@ void logout_missingHeader() throws Exception { mockMvc.perform(post("/api/auth/logout")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value("잘못된 Authorization 헤더 형식입니다.")); - verify(loginService, never()).logout(anyString()); + verify(authService, never()).logout(anyString()); } // 잘못된 토큰 (JwtException) @Test @DisplayName("POST /api/auth/logout - 잘못된 토큰이어도 200 OK 응답 (멱등성 보장)") void logout_invalidToken() throws Exception { - doThrow(new JwtException("Invalid Token")).when(loginService).logout(anyString()); + doThrow(new JwtException("Invalid Token")).when(authService).logout(anyString()); mockMvc.perform(post("/api/auth/logout") .header(HttpHeaders.AUTHORIZATION, "Bearer invalid.token")) @@ -310,7 +307,7 @@ void signup_success() throws Exception { .build(); SignupResponse resp = SignupResponse.from(entity); - when(userService.signUp(any(SignupRequest.class))).thenReturn(resp); + when(userService.signup(any(SignupRequest.class))).thenReturn(resp); mockMvc.perform(post("/api/auth/signup") .contentType(MediaType.APPLICATION_JSON) diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/LoginServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java similarity index 90% rename from backend/src/test/java/org/sejongisc/backend/auth/service/LoginServiceImplTest.java rename to backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java index cb4aacc1..b175110a 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/LoginServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java @@ -6,14 +6,15 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.sejongisc.backend.auth.repository.RefreshTokenRepository; +import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; import org.sejongisc.backend.common.auth.jwt.JwtParser; import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.service.AuthService; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.auth.dto.LoginRequest; -import org.sejongisc.backend.auth.dto.LoginResponse; +import org.sejongisc.backend.common.auth.dto.LoginRequest; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.springframework.security.crypto.password.PasswordEncoder; @@ -28,7 +29,7 @@ @ExtendWith(MockitoExtension.class) -class LoginServiceImplTest { +class AuthServiceTest { @Mock private UserRepository userRepository; @@ -47,7 +48,7 @@ class LoginServiceImplTest { @InjectMocks - private LoginServiceImpl loginService; + private AuthService authService; @Test @DisplayName("정상 로그인 시 LoginResponse 반환") @@ -77,7 +78,7 @@ void login_success() { .willReturn("mocked-jwt-token"); // when - LoginResponse response = loginService.login(request); + LoginResponse response = authService.login(request); // then assertThat(response).isNotNull(); @@ -101,7 +102,7 @@ void login_userNotFound() { // when & then CustomException exception = assertThrows(CustomException.class, - () -> loginService.login(request)); + () -> authService.login(request)); assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.USER_NOT_FOUND); } @@ -132,7 +133,7 @@ void login_wrongPassword() { // when & then CustomException exception = assertThrows(CustomException.class, - () -> loginService.login(request)); + () -> authService.login(request)); assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED); } @@ -160,7 +161,7 @@ void login_nullPassword() { // when & then CustomException exception = assertThrows(CustomException.class, - () -> loginService.login(request)); + () -> authService.login(request)); assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED); } @@ -176,7 +177,7 @@ void logout_success() { when(jwtParser.getUserIdFromToken(fakeToken)).thenReturn(userId); - loginService.logout(fakeToken); + authService.logout(fakeToken); verify(jwtParser, times(1)).getUserIdFromToken(fakeToken); diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/EmailServiceTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/EmailServiceTest.java index bfcd5912..20e37191 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/EmailServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/EmailServiceTest.java @@ -17,7 +17,8 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import org.sejongisc.backend.auth.config.EmailProperties; +import org.sejongisc.backend.common.config.EmailProperties; +import org.sejongisc.backend.common.auth.service.EmailService; import org.sejongisc.backend.user.repository.UserRepository; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java index 47121486..7bc7676d 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/GithubServiceImplTest.java @@ -6,9 +6,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.sejongisc.backend.auth.dto.oauth.GithubTokenResponse; -import org.sejongisc.backend.auth.dto.oauth.GithubUserInfoResponse; -import org.sejongisc.backend.auth.service.oauth2.GithubServiceImpl; +import org.sejongisc.backend.common.auth.dto.oauth.GithubTokenResponse; +import org.sejongisc.backend.common.auth.dto.oauth.GithubUserInfoResponse; +import org.sejongisc.backend.common.auth.service.oauth2.GithubServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java index e73b27bb..a995b160 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/GoogleServiceImplTest.java @@ -6,9 +6,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.sejongisc.backend.auth.dto.oauth.GoogleTokenResponse; -import org.sejongisc.backend.auth.dto.oauth.GoogleUserInfoResponse; -import org.sejongisc.backend.auth.service.oauth2.GoogleServiceImpl; +import org.sejongisc.backend.common.auth.dto.oauth.GoogleTokenResponse; +import org.sejongisc.backend.common.auth.dto.oauth.GoogleUserInfoResponse; +import org.sejongisc.backend.common.auth.service.oauth2.GoogleServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java index 722df639..31bab628 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/KakaoServiceImplTest.java @@ -6,8 +6,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.sejongisc.backend.auth.dto.oauth.KakaoUserInfoResponse; -import org.sejongisc.backend.auth.service.oauth2.KakaoServiceImpl; +import org.sejongisc.backend.common.auth.dto.oauth.KakaoUserInfoResponse; +import org.sejongisc.backend.common.auth.service.oauth2.KakaoServiceImpl; import java.io.IOException; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImplTest.java index ec54a61f..dd1cab8a 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/OauthUnlinkServiceImplTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.*; -import org.sejongisc.backend.auth.service.oauth2.OauthUnlinkServiceImpl; +import org.sejongisc.backend.common.auth.service.oauth2.OauthUnlinkServiceImpl; import org.springframework.http.*; import org.springframework.web.client.RestTemplate; diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java index c2b21ebb..d36fbabf 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java @@ -6,9 +6,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; -import org.sejongisc.backend.auth.entity.RefreshToken; -import org.sejongisc.backend.auth.repository.RefreshTokenRepository; +import org.sejongisc.backend.common.auth.entity.RefreshToken; +import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.service.RefreshTokenServiceImpl; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.repository.UserRepository; diff --git a/backend/src/test/java/org/sejongisc/backend/backtest/controller/BacktestControllerTest.java b/backend/src/test/java/org/sejongisc/backend/backtest/controller/BacktestControllerTest.java index a8d92e56..19fd095d 100644 --- a/backend/src/test/java/org/sejongisc/backend/backtest/controller/BacktestControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/backtest/controller/BacktestControllerTest.java @@ -10,7 +10,7 @@ //import org.sejongisc.backend.backtest.service.BacktestService; //import org.sejongisc.backend.common.auth.config.SecurityConfig; //import org.sejongisc.backend.common.auth.jwt.JwtParser; -//import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; +//import org.sejongisc.backend.common.auth.dto.LoginResponse.CustomUserDetails; //import org.sejongisc.backend.user.entity.Role; //import org.sejongisc.backend.user.entity.User; //import org.springframework.beans.factory.annotation.Autowired; diff --git a/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java b/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java index 1a1c0080..71cbbcc9 100644 --- a/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java @@ -3,10 +3,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.sejongisc.backend.common.auth.config.SecurityConfig; +import org.sejongisc.backend.common.config.SecurityConfig; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.common.auth.jwt.JwtParser; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; -import org.sejongisc.backend.common.auth.springsecurity.JwtAuthenticationFilter; import org.sejongisc.backend.template.dto.TemplateRequest; import org.sejongisc.backend.template.dto.TemplateResponse; import org.sejongisc.backend.template.entity.Template; @@ -14,7 +13,6 @@ import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; @@ -25,14 +23,9 @@ import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; -import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -71,7 +64,7 @@ class TemplateControllerTest { .userId(uid).name("tester").email("test@example.com") .role(Role.TEAM_MEMBER).point(0).build(); - CustomUserDetails customUserDetails = new CustomUserDetails(domainUser); + LoginResponse.CustomUserDetails customUserDetails = new LoginResponse.CustomUserDetails(domainUser); // SecurityConfig 에서 hasRole("TEAM_MEMBER") 라면 ROLE_ 접두어 필요 return new UsernamePasswordAuthenticationToken(customUserDetails, "", List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER"))); diff --git a/backend/src/test/java/org/sejongisc/backend/user/controller/UserControllerTest.java b/backend/src/test/java/org/sejongisc/backend/user/controller/UserControllerTest.java index 5c304214..d5f02b12 100644 --- a/backend/src/test/java/org/sejongisc/backend/user/controller/UserControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/user/controller/UserControllerTest.java @@ -8,13 +8,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; -import org.sejongisc.backend.auth.dto.SignupRequest; -import org.sejongisc.backend.auth.dto.SignupResponse; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.sejongisc.backend.user.service.UserService; -import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -23,12 +21,10 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import java.time.LocalDateTime; import java.util.UUID; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; diff --git a/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java similarity index 94% rename from backend/src/test/java/org/sejongisc/backend/user/service/UserServiceImplTest.java rename to backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java index faa4a599..f1409e7b 100644 --- a/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java @@ -16,25 +16,25 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.sejongisc.backend.auth.service.EmailService; -import org.sejongisc.backend.auth.service.RefreshTokenService; +import org.sejongisc.backend.common.auth.service.EmailService; +import org.sejongisc.backend.common.auth.service.RefreshTokenService; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.auth.repository.UserOauthAccountRepository; +import org.sejongisc.backend.common.auth.repository.UserOauthAccountRepository; import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.auth.dto.SignupRequest; -import org.sejongisc.backend.auth.dto.SignupResponse; -import org.sejongisc.backend.auth.entity.AuthProvider; +import org.sejongisc.backend.common.auth.dto.SignupRequest; +import org.sejongisc.backend.common.auth.dto.SignupResponse; +import org.sejongisc.backend.common.auth.entity.AuthProvider; import org.sejongisc.backend.user.dto.UserUpdateRequest; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; -import org.sejongisc.backend.auth.entity.UserOauthAccount; -import org.sejongisc.backend.auth.dto.oauth.OauthUserInfo; +import org.sejongisc.backend.common.auth.entity.UserOauthAccount; +import org.sejongisc.backend.common.auth.dto.oauth.OauthUserInfo; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; @ExtendWith(MockitoExtension.class) -class UserServiceImplTest { +class UserServiceTest { @Mock private UserRepository userRepository; @@ -57,11 +57,11 @@ class UserServiceImplTest { @Mock private org.sejongisc.backend.common.auth.jwt.TokenEncryptor tokenEncryptor; - @InjectMocks private UserServiceImpl userService; + @InjectMocks private UserService userService; @Test @DisplayName("회원가입 성공: 비밀번호 인코딩, 저장, DTO 매핑 확인") - void signUp_success() { + void signup_success() { // given SignupRequest req = SignupRequest.builder() .name("홍길동") @@ -89,7 +89,7 @@ void signUp_success() { }); // when - SignupResponse res = userService.signUp(req); + SignupResponse res = userService.signup(req); // then assertAll( @@ -109,7 +109,7 @@ void signUp_success() { @Test @DisplayName("회원가입 실패: 이메일 중복이면 CustomException(DUPLICATE_EMAIL)") - void signUp_duplicateEmail_throws() { + void signup_duplicateEmail_throws() { // given SignupRequest req = SignupRequest.builder() .name("홍길동") @@ -122,7 +122,7 @@ void signUp_duplicateEmail_throws() { when(userRepository.existsByEmail(req.getEmail())).thenReturn(true); // when - CustomException ex = assertThrows(CustomException.class, () -> userService.signUp(req)); + CustomException ex = assertThrows(CustomException.class, () -> userService.signup(req)); // then assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.DUPLICATE_EMAIL); @@ -134,7 +134,7 @@ void signUp_duplicateEmail_throws() { @Test @DisplayName("회원가입: Role이 null이면 기본값 MEMBER로 저장") - void signUp_nullRole_defaultsToMember() { + void signup_nullRole_defaultsToMember() { // given SignupRequest req = SignupRequest.builder() .name("이몽룡") @@ -161,7 +161,7 @@ void signUp_nullRole_defaultsToMember() { }); // when - SignupResponse res = userService.signUp(req); + SignupResponse res = userService.signup(req); // then assertThat(res.getRole()).isEqualTo(Role.TEAM_MEMBER); @@ -169,7 +169,7 @@ void signUp_nullRole_defaultsToMember() { @Test @DisplayName("회원가입 실패: 전화번호 중복이면 CustomException(DUPLICATE_PHONE)") - void signUp_duplicatePhone_throws() { + void signup_duplicatePhone_throws() { // given SignupRequest req = SignupRequest.builder() .name("성춘향") @@ -183,7 +183,7 @@ void signUp_duplicatePhone_throws() { when(userRepository.existsByPhoneNumber(req.getPhoneNumber())).thenReturn(true); // when - CustomException ex = assertThrows(CustomException.class, () -> userService.signUp(req)); + CustomException ex = assertThrows(CustomException.class, () -> userService.signup(req)); // then assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.DUPLICATE_PHONE); @@ -196,7 +196,7 @@ void signUp_duplicatePhone_throws() { @Test @DisplayName("회원가입 실패: DB 무결성 제약 위반 시 CustomException(DUPLICATE_USER)") - void signUp_dataIntegrityViolation_throws() { + void signup_dataIntegrityViolation_throws() { // given SignupRequest req = SignupRequest.builder() .name("임꺽정") @@ -214,7 +214,7 @@ void signUp_dataIntegrityViolation_throws() { .thenThrow(new org.springframework.dao.DataIntegrityViolationException("constraint")); // when - CustomException ex = assertThrows(CustomException.class, () -> userService.signUp(req)); + CustomException ex = assertThrows(CustomException.class, () -> userService.signup(req)); // then assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.DUPLICATE_USER); From 35ab9cecf3cab9166f8b6daec7a8eb17345e0ef5 Mon Sep 17 00:00:00 2001 From: Wi Seungjae <132977754+discipline24@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:40:55 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[BE]=20[FEAT]=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminUserController.java | 66 ++++++++++ .../backend/admin/dto/AdminUserRequest.java | 5 + .../controller/BacktestController.java | 1 - .../betting/controller/BettingController.java | 1 - .../board/controller/BoardController.java | 1 - .../auth/controller/AuthController.java | 25 ++-- .../{LoginRequest.java => AuthRequest.java} | 16 +-- .../{LoginResponse.java => AuthResponse.java} | 16 +-- .../common/auth/dto/CustomUserDetails.java | 11 +- .../common/auth/dto/SignupRequest.java | 81 ++++++------ .../auth/filter/JwtAuthenticationFilter.java | 35 +++--- .../backend/common/auth/jwt/JwtParser.java | 4 +- .../common/auth/service/AuthService.java | 12 +- .../service/CustomUserDetailsService.java | 15 +-- .../auth/service/RefreshTokenService.java | 118 +++++++++++++++--- .../auth/service/RefreshTokenServiceImpl.java | 109 ---------------- .../backend/common/exception/ErrorCode.java | 2 + .../controller/PointHistoryController.java | 1 - .../controller/TemplateController.java | 1 - .../user/controller/UserController.java | 32 +++-- .../backend/user/dto/UserInfoResponse.java | 1 - .../backend/user/dto/UserUpdateRequest.java | 23 +++- .../sejongisc/backend/user/entity/Role.java | 11 +- .../sejongisc/backend/user/entity/User.java | 52 +++++--- .../user/repository/UserRepository.java | 6 +- .../backend/user/service/UserService.java | 110 ++++++---------- .../user/util/PasswordPolicyValidator.java | 14 ++- .../auth/controller/AuthControllerTest.java | 14 +-- .../backend/auth/service/AuthServiceTest.java | 22 ++-- ...Test.java => RefreshTokenServiceTest.java} | 6 +- .../controller/TemplateControllerTest.java | 5 +- .../user/controller/UserControllerTest.java | 1 - .../backend/user/service/UserServiceTest.java | 35 ------ 33 files changed, 416 insertions(+), 436 deletions(-) create mode 100644 backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java create mode 100644 backend/src/main/java/org/sejongisc/backend/admin/dto/AdminUserRequest.java rename backend/src/main/java/org/sejongisc/backend/common/auth/dto/{LoginRequest.java => AuthRequest.java} (63%) rename backend/src/main/java/org/sejongisc/backend/common/auth/dto/{LoginResponse.java => AuthResponse.java} (76%) delete mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenServiceImpl.java rename backend/src/test/java/org/sejongisc/backend/auth/service/{RefreshTokenServiceImplTest.java => RefreshTokenServiceTest.java} (98%) diff --git a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java new file mode 100644 index 00000000..7ffa162c --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java @@ -0,0 +1,66 @@ +package org.sejongisc.backend.admin.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.sejongisc.backend.admin.dto.AdminUserRequest; +import org.sejongisc.backend.user.dto.UserInfoResponse; // 기존 DTO 활용 +import org.sejongisc.backend.user.entity.Role; +import org.sejongisc.backend.user.entity.UserStatus; +import org.sejongisc.backend.user.service.UserService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/admin") +@Tag(name = "관리자 API", description = "운영진 및 개발자용 회원 관리 API") +public class AdminUserController { + + private final UserService userService; + + // --- [회장/운영진용] 회원 관리 API --- + + @Operation(summary = "전체 회원 목록 조회", description = "모든 회원의 정보를 조회합니다. (회장/관리자용)") + @GetMapping("/users") + @PreAuthorize("hasAnyRole('SYSTEM_ADMIN', 'MANAGER')") + public ResponseEntity> getAllUsers(@RequestBody AdminUserRequest request) { + //return ResponseEntity.ok(userService.findAllUsers()); // TODO : 전체 조회, 기수별 조회, 이름 검색 등 기능 추가 + return null; + } + + @Operation(summary = "회원 활동 상태 변경", description = "ACTIVE, INACTIVE, GRADUATED 등으로 상태를 변경합니다.") + @PatchMapping("/users/{userId}/status") + @PreAuthorize("hasAnyRole('SYSTEM_ADMIN', 'MANAGER')") + public ResponseEntity updateUserStatus( + @PathVariable UUID userId, + @RequestParam UserStatus status) { + //userService.updateUserStatus(userId, status); + return ResponseEntity.ok(Map.of("message", "사용자 상태가 " + status + "(으)로 변경되었습니다.")); + } + + // --- [시스템 관리자용 or 회장용] 권한 및 계정 제어 API --- + // TODO : 회장 권한 논의 필요 + @Operation(summary = "회원 권한 변경", description = "특정 유저의 Role(PRESIDENT, VICE_PRESIDENT, TEAM_LEADER)을 변경합니다.)") + @PatchMapping("/users/{userId}/role") + @PreAuthorize("hasRole('SYSTEM_ADMIN')") + public ResponseEntity updateUserRole( + @PathVariable UUID userId, + @RequestParam Role role) { + //userService.updateUserRole(userId, role); + return ResponseEntity.ok(Map.of("message", "사용자 권한이 " + role + "(으)로 변경되었습니다.")); + } + + @Operation(summary = "회원 강제 탈퇴", description = "시스템에서 유저를 완전히 삭제합니다. (시스템 관리자용)") + @DeleteMapping("/users/{userId}") + @PreAuthorize("hasRole('SYSTEM_ADMIN')") + public ResponseEntity forceDeleteUser(@PathVariable UUID userId) { + //userService.deleteUserWithOauth(userId); + return ResponseEntity.ok(Map.of("message", "해당 사용자가 시스템에서 완전히 삭제되었습니다.")); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/admin/dto/AdminUserRequest.java b/backend/src/main/java/org/sejongisc/backend/admin/dto/AdminUserRequest.java new file mode 100644 index 00000000..a393dc81 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/admin/dto/AdminUserRequest.java @@ -0,0 +1,5 @@ +package org.sejongisc.backend.admin.dto; + +public class AdminUserRequest { + +} diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java b/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java index 0b185672..efd1f682 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java @@ -12,7 +12,6 @@ import org.sejongisc.backend.backtest.dto.BacktestResponse; import org.sejongisc.backend.backtest.service.BacktestService; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; diff --git a/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java b/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java index 134181d4..1d62b12d 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java @@ -12,7 +12,6 @@ import org.sejongisc.backend.betting.entity.Scope; import org.sejongisc.backend.betting.service.BettingService; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; diff --git a/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java b/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java index e4aee521..98e1dc95 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java +++ b/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java @@ -14,7 +14,6 @@ import org.sejongisc.backend.board.service.PostInteractionService; import org.sejongisc.backend.board.service.PostService; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.springframework.data.domain.Page; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthController.java b/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthController.java index e242205e..c8773cf3 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthController.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthController.java @@ -5,8 +5,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.common.auth.dto.LoginRequest; -import org.sejongisc.backend.common.auth.dto.LoginResponse; +import org.sejongisc.backend.common.auth.dto.AuthRequest; +import org.sejongisc.backend.common.auth.dto.AuthResponse; import org.sejongisc.backend.common.auth.service.AuthService; import org.sejongisc.backend.common.auth.service.RefreshTokenService; import org.springframework.http.HttpHeaders; @@ -30,13 +30,13 @@ public class AuthController { @Operation(summary = "일반 로그인 API", description = "") @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - LoginResponse response = authService.login(request); + public ResponseEntity login(@Valid @RequestBody AuthRequest request) { + AuthResponse response = authService.login(request); ResponseCookie accessCookie = cookieHelper.createAccessCookie(response.getAccessToken()); ResponseCookie refreshCookie = cookieHelper.createRefreshCookie(response.getRefreshToken()); - LoginResponse safeResponse = LoginResponse.builder() + AuthResponse safeResponse = AuthResponse.builder() .userId(response.getUserId()).email(response.getEmail()) .name(response.getName()).role(response.getRole()) .phoneNumber(response.getPhoneNumber()).point(response.getPoint()) @@ -51,22 +51,14 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest requ @Operation(summary = "Access Token 재발급 API", description = "...") @PostMapping("/reissue") public ResponseEntity reissue(@CookieValue(value = "refresh", required = false) String refreshToken) { - if (refreshToken == null || refreshToken.isEmpty()) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Refresh Token이 없습니다.")); - } - try { Map tokens = refreshTokenService.reissueTokens(refreshToken); - ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.get("accessToken")); - + ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok().header(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.get("accessToken")); if (tokens.containsKey("refreshToken")) { responseBuilder.header(HttpHeaders.SET_COOKIE, cookieHelper.createRefreshCookie(tokens.get("refreshToken")).toString()); } - responseBuilder.header(HttpHeaders.SET_COOKIE, cookieHelper.createAccessCookie(tokens.get("accessToken")).toString()); return responseBuilder.body(Map.of("message", "토큰 갱신 성공")); - } catch (Exception e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Refresh Token이 유효하지 않거나 만료되었습니다.")); } @@ -75,10 +67,7 @@ public ResponseEntity reissue(@CookieValue(value = "refresh", required = fals @Operation(summary = "로그아웃 API", description = "...") @PostMapping("/logout") public ResponseEntity logout(@CookieValue(value = "access", required = false) String accessToken) { - if (accessToken != null && !accessToken.isEmpty()) { - authService.logout(accessToken); - } - + authService.logout(accessToken); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookieHelper.deleteCookie("access").toString()) .header(HttpHeaders.SET_COOKIE, cookieHelper.deleteCookie("refresh").toString()) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginRequest.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthRequest.java similarity index 63% rename from backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginRequest.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthRequest.java index 86870493..9cc03400 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthRequest.java @@ -11,22 +11,18 @@ @Setter @NoArgsConstructor @AllArgsConstructor -@Schema( - name = "LoginRequest", - description = "일반 로그인 요청 객체 (이메일과 비밀번호 입력)" -) -public class LoginRequest { +public class AuthRequest { @Schema( - description = "사용자 이메일 주소", - example = "testuser@example.com", + description = "사용자 학번 (String)", + example = "21001001", requiredMode = Schema.RequiredMode.REQUIRED ) - @NotBlank(message = "이메일은 필수 입력값입니다.") - private String email; + @NotBlank(message = "학번은 필수 입력값입니다.") + private String studentNumber; @Schema( - description = "사용자 비밀번호 (8자 이상, 특수문자 포함 권장)", + description = "사용자 비밀번호 (8자 이상, 특수문자 포함)", example = "1234abcd!", requiredMode = Schema.RequiredMode.REQUIRED ) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginResponse.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthResponse.java similarity index 76% rename from backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginResponse.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthResponse.java index 5866234a..36768653 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/LoginResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthResponse.java @@ -5,23 +5,13 @@ import lombok.Builder; import lombok.Getter; import org.sejongisc.backend.user.entity.Role; -import org.sejongisc.backend.user.entity.User; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import java.util.Collection; -import java.util.List; import java.util.UUID; @Getter @Builder @AllArgsConstructor -@Schema( - name = "LoginResponse", - description = "로그인 성공 시 반환되는 응답 객체" -) -public class LoginResponse { +public class AuthResponse { @Schema( description = "Access Token (JWT 형식, API 요청 시 Authorization 헤더에 사용)", @@ -54,8 +44,8 @@ public class LoginResponse { private String name; @Schema( - description = "사용자 역할 (예: USER, ADMIN)", - example = "USER" + description = "사용자 직위", + example = "PRESIDENT" ) private Role role; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/CustomUserDetails.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/CustomUserDetails.java index 87875178..5b86c3b0 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/CustomUserDetails.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/CustomUserDetails.java @@ -17,10 +17,10 @@ public class CustomUserDetails implements UserDetails { private final UUID userId; private final String name; private final String email; - private final String password; - private final String phoneNumber; + private final String password; // TODO : 보안을 위해 password 필드 제거 고려 + private final String phoneNumber; // TODO : 굳이 있어야하나? 제거 고려 private final Role role; - private final Integer point; + private final Integer point; // TODO : 사용자 포인트는 가변적이기 때문에, 제거 고려 public CustomUserDetails(User user) { this.userId = user.getUserId(); @@ -35,7 +35,10 @@ public CustomUserDetails(User user) { @Override public Collection getAuthorities() { - return List.of(new SimpleGrantedAuthority(role.name())); + // role.name()이 "PRESIDENT"라면 "ROLE_PRESIDENT"로 변환해서 반환해야 함 + // Spring Security에서는 권한 앞에 "ROLE_" 접두사를 붙이는 것이 관례임 + // hasRole("PRESIDENT") 같은 메서드 호출 시 "ROLE_PRESIDENT"와 매칭되기 때문 + return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); } @Override diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupRequest.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupRequest.java index dd2d9587..216cb8f6 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/SignupRequest.java @@ -1,10 +1,12 @@ package org.sejongisc.backend.common.auth.dto; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.*; +import org.sejongisc.backend.user.entity.Gender; import org.sejongisc.backend.user.entity.Role; @Getter @@ -12,57 +14,50 @@ @NoArgsConstructor @AllArgsConstructor @Builder -@Schema( - name = "SignupRequest", - description = "회원가입 요청 객체 (이름, 이메일, 비밀번호, 역할, 전화번호 입력)" -) +@Schema(description = "회원가입 요청 DTO") public class SignupRequest { + public static final String STUDENT_ID_REGEX = "^[0-9]{8}$"; // 8자리 학번 + public static final String PHONE_FORMAT_REGEX = "^010-\\d{3,4}-\\d{4}$"; // xxx-xxxx-xxxx 형식 - @Schema( - description = "사용자 이름", - example = "홍길동", - requiredMode = Schema.RequiredMode.REQUIRED - ) - @NotBlank(message = "이름은 필수입니다.") + @NotBlank(message = "성함은 필수입니다.") + @Schema(description = "성함", example = "홍길동") private String name; - @Schema( - description = "사용자 이메일 주소 (유효한 이메일 형식이어야 함)", - example = "testuser@example.com", - requiredMode = Schema.RequiredMode.REQUIRED - ) - @NotBlank(message = "이메일은 필수입니다.") - @Pattern( - regexp = "^[A-Za-z0-9][A-Za-z0-9+_.'-]*[A-Za-z0-9]@[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*\\.[A-Za-z]{2,}$", - message = "유효한 이메일 형식이 아닙니다." - ) - private String email; + @NotBlank(message = "학번은 필수입니다.") + @Pattern(regexp = STUDENT_ID_REGEX, message = "학번은 8자리 숫자여야 합니다.") + @Schema(description = "학번 (로그인 ID로 사용)", example = "21010000") + private String studentId; - @Schema( - description = "사용자 비밀번호 (8자 이상, 숫자/영문/특수문자 조합 권장)", - example = "Abcd1234!", - requiredMode = Schema.RequiredMode.REQUIRED - ) @NotBlank(message = "비밀번호는 필수입니다.") + @Schema(description = "비밀번호 (대소문자/숫자/특수문자 포함)", example = "Sira1234!") private String password; - @Schema( - description = "사용자 역할 (USER 또는 ADMIN 등)", - example = "TEAM_MEMBER", - requiredMode = Schema.RequiredMode.REQUIRED - ) - @NotNull(message = "역할은 필수입니다.") - private Role role; - - @Schema( - description = "전화번호 (숫자만 입력, 10~11자리)", - example = "01012345678", - requiredMode = Schema.RequiredMode.REQUIRED - ) @NotBlank(message = "전화번호는 필수입니다.") - @Pattern( - regexp = "^[0-9]{10,11}$", - message = "전화번호는 10~11자리 숫자여야 합니다." - ) + @Pattern(regexp = PHONE_FORMAT_REGEX, message = "전화번호 형식이 올바르지 않습니다. (예: 010-1234-5678)") + @Schema(description = "전화번호", example = "010-1234-5678") private String phoneNumber; + + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "유효한 이메일 형식이 아닙니다.") + @Schema(description = "비밀번호 재설정용 이메일", example = "sira@sejong.ac.kr") + private String email; + + @NotNull(message = "성별은 필수입니다.") + @Schema(description = "성별", example = "MALE") + private Gender gender; + + @Schema(description = "단과대학", example = "인공지능융합대학") + private String college; + + @Schema(description = "학과", example = "컴퓨터공학과") + private String department; + + @Schema(description = "기수", example = "25") + private Integer generation; + + @Schema(description = "활동팀", example = "금융IT") + private String teamName; + + @Schema(description = "기타 특이사항 (선배/운영부 등 가입 목적)", example = "10기 운영진 가입 신청입니다.") + private String remark; } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java index ea4b1d90..ed325b38 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java @@ -34,8 +34,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtParser jwtParser; private final AntPathMatcher pathMatcher = new AntPathMatcher(); + private final ObjectMapper objectMapper; - + // TODO : 인증 제외 경로 클래스화 필요 (securityConfig와 중복) + JWTParser, JWTProvider 코드 중복 개선 필요 private static final List EXCLUDE_PATTERNS = List.of( "/api/auth/signup", "/api/auth/login", @@ -54,24 +55,19 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, - @NotNull FilterChain filterChain) - throws ServletException, IOException { + @NotNull FilterChain filterChain) throws ServletException, IOException { String requestURI = request.getRequestURI(); // 인증 제외 경로 - if (shouldNotFilter(request)) { - filterChain.doFilter(request, response); - return; - } - - if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + // 브라우저가 실제 요청 전에 서버에 보내는 CORS 예비 요청(Preflight 요청)은 OPTIONS 메서드 사용 (JWT 검사 제외) + if (shouldNotFilter(request) || "OPTIONS".equalsIgnoreCase(request.getMethod())) { filterChain.doFilter(request, response); return; } try { - String token = resolveToken(request); + String token = resolveTokenFromHeader(request); if (token == null) { token = resolveTokenFromCookie(request); @@ -84,18 +80,12 @@ protected void doFilterInternal(@NotNull HttpServletRequest request, } else { log.warn("토큰이 없거나 유효하지 않음"); } - + filterChain.doFilter(request, response); } catch (JwtException e) { log.error("JWT validation failed: {}", e.getMessage(), e); - ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INVALID_ACCESS_TOKEN); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(toJson(errorResponse)); + sendErrorResponse(response, ErrorCode.INVALID_ACCESS_TOKEN); return; // 예외 시 여기서 중단 } - - // 필터 체인은 항상 마지막에 한 번만 호출 - filterChain.doFilter(request, response); } @@ -118,7 +108,14 @@ protected boolean shouldNotFilter(HttpServletRequest request) { return excluded; } - private String resolveToken(HttpServletRequest request) { + private void sendErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException { + ErrorResponse errorResponse = ErrorResponse.of(errorCode); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } + + private String resolveTokenFromHeader(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java index c91520ba..d68aec6b 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java @@ -46,8 +46,8 @@ public boolean validationToken(String token) { // Authentication 생성 public UsernamePasswordAuthenticationToken getAuthentication(String token) { Claims claims = parseClaims(token); + // TODO : 유지보수성을 위해 클레임 키 상수화 고려 및 String userId = claims.get("uid", String.class); - String roleStr = claims.get("role", String.class); if (roleStr == null) { throw new JwtException("JWT에 role 클레임이 없습니다."); @@ -65,7 +65,7 @@ public UsernamePasswordAuthenticationToken getAuthentication(String token) { } // DB에서 다시 유저를 불러오기 (CustomUserDetailsService 사용) - UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId); + UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId); // TODO : 성능 고려해서 DB 조회 제거 고민 log.debug("인증 객체 생성 완료"); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java index 93d99414..75246ee1 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java @@ -10,8 +10,8 @@ import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.common.auth.dto.LoginRequest; -import org.sejongisc.backend.common.auth.dto.LoginResponse; +import org.sejongisc.backend.common.auth.dto.AuthRequest; +import org.sejongisc.backend.common.auth.dto.AuthResponse; import org.sejongisc.backend.user.entity.User; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -30,14 +30,12 @@ public class AuthService { private final JwtParser jwtParser; @Transactional - public LoginResponse login(LoginRequest request) { - User user = userRepository.findUserByEmail(request.getEmail()) + public AuthResponse login(AuthRequest request) { + User user = userRepository.findUserByEmail(request.getStudentNumber()) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - if (user.getPasswordHash() == null || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { throw new CustomException(ErrorCode.UNAUTHORIZED); } - String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail()); String refreshToken = jwtProvider.createRefreshToken(user.getUserId()); @@ -54,7 +52,7 @@ public LoginResponse login(LoginRequest request) { log.info("RefreshToken 저장 완료: userId={}", user.getUserId()); - return LoginResponse.builder() + return AuthResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) .userId(user.getUserId()) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/CustomUserDetailsService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/CustomUserDetailsService.java index a44be946..57b4ed08 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/service/CustomUserDetailsService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/CustomUserDetailsService.java @@ -22,18 +22,13 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { - UUID uuid; try { - uuid = UUID.fromString(userId); + UUID uuidUserId = UUID.fromString(userId); + User findUser = userRepository.findById(uuidUserId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + return new CustomUserDetails(findUser); } catch (IllegalArgumentException e) { - throw new CustomException(ErrorCode.INVALID_ACCESS_TOKEN); + throw new CustomException(ErrorCode.INVALID_INPUT); } - User findUser = userRepository.findById(uuid).orElseThrow( - () -> new CustomException(ErrorCode.USER_NOT_FOUND) - ); - - return new CustomUserDetails(findUser); - } - } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java index fd31208f..d7cc614a 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java @@ -1,21 +1,109 @@ package org.sejongisc.backend.common.auth.service; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.common.auth.entity.RefreshToken; +import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; +import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; +import org.sejongisc.backend.common.exception.CustomException; +import org.sejongisc.backend.common.exception.ErrorCode; +import org.sejongisc.backend.user.repository.UserRepository; +import org.sejongisc.backend.user.entity.User; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Date; +import java.util.HashMap; import java.util.Map; import java.util.UUID; -public interface RefreshTokenService { - - /** - * Refresh Token을 검증하고 새로운 Access Token을 재발급합니다. - * Refresh Token의 만료가 임박하면 새 Refresh Token도 함께 반환합니다. - * - * @param refreshToken 클라이언트의 Refresh Token - * @return Map { - * "accessToken": 새 Access Token, - * "refreshToken": (선택적) 새 Refresh Token - * } - */ - Map reissueTokens(String refreshToken); - void deleteByUserId(UUID userId); - void saveOrUpdateToken(UUID userId, String refreshToken); +@Slf4j +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + + private final RefreshTokenRepository refreshTokenRepository; + private final UserRepository userRepository; + private final JwtProvider jwtProvider; + private final TokenEncryptor tokenEncryptor; + + @Transactional + public Map reissueTokens(String encryptedRefreshToken) { + if (encryptedRefreshToken == null || encryptedRefreshToken.isEmpty()) { + throw new CustomException(ErrorCode.MISSING_REFRESH_TOKEN); + } + try { + // 전달받은 refreshToken 복호화 + String rawRefreshToken = tokenEncryptor.decrypt(encryptedRefreshToken); + + // refreshToken에서 userId 추출 + UUID userId = UUID.fromString(jwtProvider.getUserIdFromToken(rawRefreshToken)); + + // DB에서 저장된 refreshToken 확인 + RefreshToken saved = refreshTokenRepository.findByUserId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); + + String savedRawToken = tokenEncryptor.decrypt(saved.getToken()); + if (!MessageDigest.isEqual( + rawRefreshToken.getBytes(StandardCharsets.UTF_8), + savedRawToken.getBytes(StandardCharsets.UTF_8))) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + // User 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 새 Access Token 발급 + String newAccessToken = jwtProvider.createToken( + user.getUserId(), user.getRole(), user.getEmail()); + + // Refresh Token 만료 임박 시 새로 발급 + Date expiration = jwtProvider.getExpiration(rawRefreshToken); + long remainingMillis = expiration.getTime() - System.currentTimeMillis(); + String newRefreshToken = null; + + // 예: 남은 기간이 3일 미만이면 refreshToken도 갱신 + if (remainingMillis < (3L * 24 * 60 * 60 * 1000)) { + newRefreshToken = jwtProvider.createRefreshToken(user.getUserId()); + saved.setToken(newRefreshToken); + refreshTokenRepository.save(saved); + log.info("RefreshToken 재발급 완료: userId={}", userId); + } + + // 결과 반환 + Map tokens = new HashMap<>(); + tokens.put("accessToken", newAccessToken); + if (newRefreshToken != null) tokens.put("refreshToken", newRefreshToken); + + log.info("AccessToken 재발급 완료: userId={}", userId); + return tokens; + + } catch (CustomException e) { + throw e; // 커스텀 예외는 그대로 던짐 + } catch (Exception e) { + log.warn("AccessToken 재발급 실패: {}", e.getMessage()); + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + } + + @Transactional + public void deleteByUserId(UUID userId) { + refreshTokenRepository.deleteByUserId(userId); + log.info("RefreshToken deleted for userId={}", userId); + } + + @Transactional + public void saveOrUpdateToken(UUID userId, String refreshToken) { + refreshTokenRepository.findByUserId(userId) + .ifPresentOrElse( + existing -> existing.setToken(refreshToken), + () -> refreshTokenRepository.save(new RefreshToken(userId, refreshToken)) + ); + log.info("RefreshToken 저장 또는 갱신 완료: userId={}", userId); + } + } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenServiceImpl.java deleted file mode 100644 index 793bf2eb..00000000 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenServiceImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.sejongisc.backend.common.auth.service; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.common.auth.entity.RefreshToken; -import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; -import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; -import org.sejongisc.backend.common.exception.CustomException; -import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.user.entity.User; -import org.springframework.stereotype.Service; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -public class RefreshTokenServiceImpl implements RefreshTokenService { - - private final RefreshTokenRepository refreshTokenRepository; - private final UserRepository userRepository; - private final JwtProvider jwtProvider; - private final TokenEncryptor tokenEncryptor; - - @Override - @Transactional - public Map reissueTokens(String encryptedRefreshToken) { - try { - // 전달받은 refreshToken 복호화 - String rawRefreshToken = tokenEncryptor.decrypt(encryptedRefreshToken); - - // refreshToken에서 userId 추출 - UUID userId = UUID.fromString(jwtProvider.getUserIdFromToken(rawRefreshToken)); - - // DB에서 저장된 refreshToken 확인 - RefreshToken saved = refreshTokenRepository.findByUserId(userId) - .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); - - String savedRawToken = tokenEncryptor.decrypt(saved.getToken()); - if (!MessageDigest.isEqual( - rawRefreshToken.getBytes(StandardCharsets.UTF_8), - savedRawToken.getBytes(StandardCharsets.UTF_8))) { - throw new CustomException(ErrorCode.UNAUTHORIZED); - } - - // User 조회 - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 새 Access Token 발급 - String newAccessToken = jwtProvider.createToken( - user.getUserId(), user.getRole(), user.getEmail()); - - // Refresh Token 만료 임박 시 새로 발급 - Date expiration = jwtProvider.getExpiration(rawRefreshToken); - long remainingMillis = expiration.getTime() - System.currentTimeMillis(); - String newRefreshToken = null; - - // 예: 남은 기간이 3일 미만이면 refreshToken도 갱신 - if (remainingMillis < (3L * 24 * 60 * 60 * 1000)) { - newRefreshToken = jwtProvider.createRefreshToken(user.getUserId()); - saved.setToken(newRefreshToken); - refreshTokenRepository.save(saved); - log.info("RefreshToken 재발급 완료: userId={}", userId); - } - - // 결과 반환 - Map tokens = new HashMap<>(); - tokens.put("accessToken", newAccessToken); - if (newRefreshToken != null) tokens.put("refreshToken", newRefreshToken); - - log.info("AccessToken 재발급 완료: userId={}", userId); - return tokens; - - } catch (CustomException e) { - throw e; // 커스텀 예외는 그대로 던짐 - } catch (Exception e) { - log.warn("AccessToken 재발급 실패: {}", e.getMessage()); - throw new CustomException(ErrorCode.UNAUTHORIZED); - } - } - - @Override - @Transactional - public void deleteByUserId(UUID userId) { - refreshTokenRepository.deleteByUserId(userId); - log.info("RefreshToken deleted for userId={}", userId); - } - - @Override - @Transactional - public void saveOrUpdateToken(UUID userId, String refreshToken) { - refreshTokenRepository.findByUserId(userId) - .ifPresentOrElse( - existing -> existing.setToken(refreshToken), - () -> refreshTokenRepository.save(new RefreshToken(userId, refreshToken)) - ); - log.info("RefreshToken 저장 또는 갱신 완료: userId={}", userId); - } - -} diff --git a/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java b/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java index 49ab2216..538b09a4 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java +++ b/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java @@ -54,6 +54,8 @@ public enum ErrorCode { MISSING_AUTH_TOKEN(HttpStatus.UNAUTHORIZED, "인증 토큰이 필요합니다."), + MISSING_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 필요합니다."), + INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 엑세스 토큰입니다."), // EMAIL diff --git a/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java b/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java index 6d1fc5da..e09f9c5e 100644 --- a/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java +++ b/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java @@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.point.dto.PointHistoryResponse; import org.sejongisc.backend.point.service.PointHistoryService; import org.springframework.data.domain.PageRequest; diff --git a/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java b/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java index e8faeced..46ecd842 100644 --- a/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java +++ b/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.template.dto.TemplateRequest; import org.sejongisc.backend.template.dto.TemplateResponse; import org.sejongisc.backend.template.service.TemplateService; diff --git a/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java b/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java index 3bbef25e..27ba4586 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java +++ b/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java @@ -8,7 +8,6 @@ import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.common.auth.controller.AuthCookieHelper; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.common.auth.dto.SignupRequest; import org.sejongisc.backend.common.auth.dto.SignupResponse; import org.sejongisc.backend.common.auth.service.RefreshTokenService; @@ -21,7 +20,6 @@ import org.springframework.web.bind.annotation.*; import java.util.Map; -import java.util.UUID; @RestController @RequiredArgsConstructor @@ -41,42 +39,42 @@ public ResponseEntity signup(@Valid @RequestBody SignupRequest r return ResponseEntity.status(HttpStatus.CREATED).body(userService.signup(request)); } - @Operation(summary = "회원 탈퇴", description = "리프레시 토큰을 삭제합니다.") + @Operation(summary = "회원 탈퇴", description = "UserStatus.OUT 으로 변경하여 softDelete 처리 후, 리프레시 토큰을 삭제합니다.") @DeleteMapping("/withdraw") - public ResponseEntity withdraw(@AuthenticationPrincipal CustomUserDetails user) { - userService.deleteUserWithOauth(user.getUserId()); + public ResponseEntity withdraw(@AuthenticationPrincipal CustomUserDetails user) { + userService.deleteUserSoftDelete(user.getUserId()); refreshTokenService.deleteByUserId(user.getUserId()); - return ResponseEntity.ok() + return ResponseEntity.noContent() .header(HttpHeaders.SET_COOKIE, authCookieHelper.deleteCookie("refresh").toString()) - .body(Map.of("message", "회원 탈퇴가 완료되었습니다.")); + .build(); } @Operation(summary = "내 정보 조회") @GetMapping("/details") - public ResponseEntity getUserInfo(@AuthenticationPrincipal CustomUserDetails user) { + public ResponseEntity getUserInfo(@AuthenticationPrincipal CustomUserDetails user) { return ResponseEntity.ok(new UserInfoResponse(user.getUserId(), user.getName(), user.getEmail(), user.getPhoneNumber(), user.getPoint(), user.getRole().name(), user.getAuthorities())); } @Operation(summary = "내 정보 수정") - @PatchMapping("/{userId}") - public ResponseEntity updateUser( - @PathVariable UUID userId, - @RequestBody @Valid UserUpdateRequest request, - @AuthenticationPrincipal CustomUserDetails customUserDetails - ) { - userService.updateUser(userId, request); - return ResponseEntity.ok("회원 정보가 수정되었습니다."); + @PatchMapping("/details") + public ResponseEntity updateUser(@RequestBody @Valid UserUpdateRequest request, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + userService.updateUser(customUserDetails.getUserId(), request); + return ResponseEntity.ok().build(); } + /* @Operation(summary = "아이디 찾기") @PostMapping("/id/find") public ResponseEntity findUserID(@RequestBody @Valid UserIdFindRequest request) { String email = userService.findEmailByNameAndPhone(request.name(), request.phoneNumber()); return ResponseEntity.ok(Map.of("email", email)); } + */ - @Operation(summary = "비밀번호 재설정") + // TODO : 비밀번호 재설정 시 학번 입력 고려 + @Operation(summary = "비밀번호 재설정 : 이메일로 인증코드를 전송합니다.") @PostMapping("/password/reset/send") public ResponseEntity sendReset(@RequestBody @Valid PasswordResetSendRequest req){ userService.passwordReset(req.email().trim()); diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java index 2defc4e7..7ed65b8b 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java @@ -6,7 +6,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.user.entity.User; @Getter diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java b/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java index 964067d2..75cabfc8 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java @@ -1,7 +1,8 @@ package org.sejongisc.backend.user.dto; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; @@ -10,10 +11,10 @@ @Setter @Schema( name = "UserUpdateRequest", - description = "회원정보 수정 요청 객체 (이름, 전화번호, 비밀번호 중 수정할 항목만 입력)" + description = "회원정보 수정 시, 이메일/비밀번호 중 수정할 항목만 입력" ) public class UserUpdateRequest { - +/* @Schema( description = "변경할 이름 (선택 입력)", example = "홍길동" @@ -30,11 +31,23 @@ public class UserUpdateRequest { message = "전화번호는 숫자만 10~11자리로 입력해주세요." ) private String phoneNumber; + */ + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "유효한 이메일 형식이 아닙니다.") + @Schema(description = "비밀번호 재설정용 이메일", example = "sira@sejong.ac.kr") + private String email; @Schema( - description = "변경할 비밀번호 (선택 입력, 변경 시에만 포함)", - example = "Newpassword123!" + description = "기존 비밀번호 (변경 시에만 포함)", + example = "password123!" ) @Size(min = 8, message = "비밀번호는 최소 8자 이상 입력해야 합니다.") private String password; + + @Schema( + description = "변경할 비밀번호 (변경 시에만 포함)", + example = "Newpassword123!" + ) + @Size(min = 8, message = "비밀번호는 최소 8자 이상 입력해야 합니다.") + private String newPassword; } diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/Role.java b/backend/src/main/java/org/sejongisc/backend/user/entity/Role.java index 047674de..85b7b2e2 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/Role.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/Role.java @@ -1,8 +1,11 @@ package org.sejongisc.backend.user.entity; public enum Role { - PRESIDENT, // 회장 - VICE_PRESIDENT, // 부회장 - TEAM_LEADER, // 팀장 - TEAM_MEMBER // 부원 + SYSTEM_ADMIN, // 시스템 관리자 + PRESIDENT, // 회장 + VICE_PRESIDENT, // 부회장 + TEAM_LEADER, // 팀장 + TEAM_MEMBER, // 부원 + PENDING_MEMBER; // 대기회원 (회장이 승인 전 상태) + // 추가 가능 : SENIOR (선배/OB): 게시물 열람 위주 (포인트 활동 등은 제한 가능) } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java index 92c57916..7b372086 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.*; +import org.sejongisc.backend.common.auth.dto.SignupRequest; import org.sejongisc.backend.common.auth.entity.UserOauthAccount; import org.sejongisc.backend.common.entity.postgres.BasePostgresEntity; @@ -26,9 +27,9 @@ public class User extends BasePostgresEntity{ @Column(name = "user_id", columnDefinition = "uuid") private UUID userId; - //OAuth 전용 계정 대비 nullable 허용 가능 - @Column(columnDefinition = "citext", unique = true, nullable = true) - private String email; + // 로그인 시 이메일이 아닌 학번 입력 + @Column(name = "student_id", unique = true, nullable = false) + private String studentId; // 학번: 엑셀 매칭 및 계정 식별의 핵심 키 @Column(name = "password_hash") private String passwordHash; @@ -36,9 +37,6 @@ public class User extends BasePostgresEntity{ @Column(nullable = false) private String name; - @Column(name = "student_id", unique = true, nullable = false) - private String studentId; // 학번: 엑셀 매칭 및 계정 식별의 핵심 키 - @Column(name = "phone_number") private String phoneNumber; @@ -51,20 +49,24 @@ public class User extends BasePostgresEntity{ @Enumerated(EnumType.STRING) private Gender gender; // 성별 - @Column(nullable = false) + @Column(name = "is_new_member", nullable = false) private boolean isNewMember; // 신규 여부 (포인트나 이벤트 대상자 선정용) @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - @Builder.Default + @Column(name = "position_name") // 엑셀의 '직위' 컬럼 데이터 그대로 저장 + private String positionName; + + //OAuth 전용 계정 대비 nullable 허용 가능 + @Column(columnDefinition = "citext", unique = true, nullable = true) + private String email; // 추후 비밀번호 찾기용 및 공지 발송용 + + @Enumerated(EnumType.STRING) // 새 장부 업로드 시: 기존에 ACTIVE한 모든 인원을 INACTIVE로 일괄 업데이트 + @Column(nullable = false) // 새 엑셀에 있는 studentId을 대조하여, 명단에 있는 사람만 다시 ACTIVE로 바꾸고 + @Builder.Default // generation(기수)과 positionName(직위)을 최신화 private UserStatus status = UserStatus.ACTIVE; // 활동 상태 (ACTIVE, INACTIVE, GRADUATED, OUT 등) - // 새 장부 업로드 시: 기존에 ACTIVE한 모든 인원을 INACTIVE로 일괄 업데이트 - // 새 엑셀에 있는 studentId을 대조하여, 명단에 있는 사람만 다시 ACTIVE로 바꾸고 - // generation(기수)과 positionName(직위)을 최신화 @Column(columnDefinition = "integer default 0",nullable = false) private Integer point; @@ -79,9 +81,6 @@ public class User extends BasePostgresEntity{ @JsonIgnore private List oauthAccounts = new ArrayList<>(); - @Column(name = "position_name") // 엑셀의 '직위' 컬럼 데이터 그대로 저장 - private String positionName; - // 권한 확인용 편의 메서드 public boolean isManagerPosition() { if (this.positionName == null) return false; @@ -95,7 +94,7 @@ public boolean isManagerPosition() { @PrePersist public void prePersist() { if (this.role == null) { - this.role = Role.TEAM_MEMBER; + this.role = Role.PENDING_MEMBER; } if (this.point == null) { this.point = 0; @@ -104,4 +103,23 @@ public void prePersist() { public void updatePoint(int amount) { this.point += amount; } + + public static User createUserWithSignupAndPending(SignupRequest request, String encodedPw) { + return User.builder() + .role(Role.PENDING_MEMBER) + .studentId(request.getStudentId()) + .name(request.getName()) + .passwordHash(encodedPw) + .phoneNumber(request.getPhoneNumber()) + .email(request.getEmail()) + .gender(request.getGender()) + .college(request.getCollege()) // 단과대 + .department(request.getDepartment()) // 학과 + .generation(request.getGeneration()) // + .teamName(request.getTeamName()) // 소속 팀명 + .isNewMember(true) // 신규 가입자 + .point(0) + .status(UserStatus.ACTIVE) // 기본 활동 상태 + .build(); + } } diff --git a/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java index c7f88e34..d22ea3c9 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java @@ -1,5 +1,8 @@ package org.sejongisc.backend.user.repository; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import org.sejongisc.backend.user.entity.User; import org.sejongisc.backend.user.service.projection.UserIdNameProjection; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,8 +14,9 @@ public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); - boolean existsByPhoneNumber(String phoneNumber); + boolean existsByEmailOrStudentId(String email, String studentId); + boolean existsByStudentId(String studentId); Optional findUserByEmail(String email); diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java index a70ee3a3..753bb589 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java @@ -4,15 +4,9 @@ import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.common.auth.dto.SignupRequest; import org.sejongisc.backend.common.auth.dto.SignupResponse; -import org.sejongisc.backend.common.auth.entity.AuthProvider; -import org.sejongisc.backend.common.auth.entity.UserOauthAccount; -import org.sejongisc.backend.common.auth.dto.oauth.OauthUserInfo; -import org.sejongisc.backend.common.auth.repository.UserOauthAccountRepository; import org.sejongisc.backend.common.auth.service.EmailService; import org.sejongisc.backend.common.auth.service.RefreshTokenService; -import org.sejongisc.backend.common.auth.service.oauth2.OauthUnlinkService; import org.sejongisc.backend.common.annotation.OptimisticRetry; -import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.point.dto.AccountEntry; @@ -22,8 +16,8 @@ import org.sejongisc.backend.point.service.AccountService; import org.sejongisc.backend.point.service.PointLedgerService; import org.sejongisc.backend.user.dto.UserUpdateRequest; -import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.entity.UserStatus; import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.util.PasswordPolicyValidator; import org.springframework.dao.DataIntegrityViolationException; @@ -43,10 +37,7 @@ public class UserService { private final UserRepository userRepository; - private final UserOauthAccountRepository oauthAccountRepository; - private final OauthUnlinkService oauthUnlinkService; private final PasswordEncoder passwordEncoder; - private final TokenEncryptor tokenEncryptor; private final EmailService emailService; private final RedisTemplate redisTemplate; private final RefreshTokenService refreshTokenService; @@ -57,20 +48,14 @@ public class UserService { @Transactional @OptimisticRetry - public SignupResponse signup(SignupRequest dto) { - validateDuplicateUser(dto.getEmail(), dto.getPhoneNumber()); - - String trimmedPassword = validateAndGetTrimmedPassword(dto.getPassword()); - PasswordPolicyValidator.validate(trimmedPassword); - - User user = User.builder() - .name(dto.getName()) - .email(dto.getEmail()) - .passwordHash(passwordEncoder.encode(trimmedPassword)) - .role(dto.getRole() != null ? dto.getRole() : Role.TEAM_MEMBER) - .point(0) - .phoneNumber(dto.getPhoneNumber()) - .build(); + public SignupResponse signup(SignupRequest request) { + if (userRepository.existsByEmailOrStudentId(request.getEmail(), request.getStudentId())) { + if (userRepository.existsByStudentId(request.getStudentId())) throw new CustomException(ErrorCode.DUPLICATE_USER); + throw new CustomException(ErrorCode.DUPLICATE_PHONE); + } + String trimmedPassword = PasswordPolicyValidator.getValidatedPassword(request.getPassword()); + String encodedPw = passwordEncoder.encode(trimmedPassword); + User user = User.createUserWithSignupAndPending(request, encodedPw); try { User saved = userRepository.save(user); @@ -90,35 +75,29 @@ public SignupResponse signup(SignupRequest dto) { @Transactional public void updateUser(UUID userId, UserUpdateRequest request) { - User user = findUserById(userId); - - if (isNotBlank(request.getName())) { - user.setName(request.getName().trim()); + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + if (request.getEmail() != null) { + user.setEmail(request.getEmail().trim()); } - if (isNotBlank(request.getPhoneNumber())) { - user.setPhoneNumber(request.getPhoneNumber().trim()); - } + // 비밀번호 변경 로직 (새 비밀번호가 입력된 경우에만 실행) + if (request.getNewPassword() != null && !request.getNewPassword().isBlank()) { + if (!passwordEncoder.matches(request.getNewPassword(), request.getNewPassword())) { + throw new CustomException(ErrorCode.INVALID_INPUT); // 비밀번호 불일치 에러 + } + // 새 비밀번호 정제 및 정책 검증 + String cleanNewPassword = PasswordPolicyValidator.getValidatedPassword(request.getNewPassword()); - if (request.getPassword() != null) { - String trimmedPassword = validateAndGetTrimmedPassword(request.getPassword()); - PasswordPolicyValidator.validate(trimmedPassword); - user.setPasswordHash(passwordEncoder.encode(trimmedPassword)); - } + // 새 비밀번호 인코딩 및 설정 + user.setPasswordHash(passwordEncoder.encode(cleanNewPassword)); + // 비밀번호 변경 시 모든 기기 로그아웃 처리 (선택 사항) + refreshTokenService.deleteByUserId(user.getUserId()); + } log.info("회원 정보 수정 완료: userId={}", userId); } - - public String findEmailByNameAndPhone(String name, String phone) { - String nName = validateNotBlank(name, "이름"); - String nPhone = validateNotBlank(phone, "전화번호"); - - return userRepository.findByNameAndPhoneNumber(nName, nPhone) - .map(User::getEmail) - .orElse(null); - } - public void passwordReset(String email) { String nEmail = validateNotBlank(email, "이메일"); @@ -148,8 +127,7 @@ public void resetPasswordByToken(String resetToken, String newPassword) { User user = userRepository.findUserByEmail(email) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - String trimmedPassword = validateAndGetTrimmedPassword(newPassword); - PasswordPolicyValidator.validate(trimmedPassword); + String trimmedPassword = PasswordPolicyValidator.getValidatedPassword(newPassword); user.setPasswordHash(passwordEncoder.encode(trimmedPassword)); @@ -163,23 +141,6 @@ public List findAllUsersMissingAccount() { // --- 내부 헬퍼 메서드 --- - private User findUserById(UUID userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - } - - private void validateDuplicateUser(String email, String phone) { - if (userRepository.existsByEmail(email)) throw new CustomException(ErrorCode.DUPLICATE_EMAIL); - if (userRepository.existsByPhoneNumber(phone)) throw new CustomException(ErrorCode.DUPLICATE_PHONE); - } - - private String validateAndGetTrimmedPassword(String password) { - if (password == null || password.trim().isEmpty()) { - throw new CustomException(ErrorCode.INVALID_INPUT); - } - return password.trim(); - } - private String validateNotBlank(String value, String fieldName) { if (value == null || value.trim().isEmpty()) { throw new CustomException(ErrorCode.INVALID_INPUT); @@ -187,14 +148,7 @@ private String validateNotBlank(String value, String fieldName) { return value.trim(); } - private boolean isNotBlank(String value) { - return value != null && !value.trim().isEmpty(); - } - - private void completeSignup(User user) { - - } - + // TODO : RedisService로 분리 고려 private void saveResetTokenToRedis(String token, String email) { try { redisTemplate.opsForValue().set("PASSWORD_RESET:" + token, email, Duration.ofMinutes(10)); @@ -223,8 +177,17 @@ private void deleteResetTokenFromRedis(String token) { } } + public void deleteUserSoftDelete(UUID userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + user.setStatus(UserStatus.OUT); + refreshTokenService.deleteByUserId(userId); + log.info("회원 softdelete 처리 완료: userId={}", userId); + } + // ------------------------ (비활성화) OAuth2 관련 로직 ------------------------ + /* @Transactional public User upsertOAuthUser(String provider, String providerUid, String email, String name) { AuthProvider authProvider = AuthProvider.valueOf(provider.toUpperCase()); @@ -277,4 +240,5 @@ public void deleteUserWithOauth(UUID userId) { userRepository.delete(user); log.info("회원 탈퇴 완료: userId={}", userId); } + */ } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/user/util/PasswordPolicyValidator.java b/backend/src/main/java/org/sejongisc/backend/user/util/PasswordPolicyValidator.java index 4809eb2c..2cbd3901 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/util/PasswordPolicyValidator.java +++ b/backend/src/main/java/org/sejongisc/backend/user/util/PasswordPolicyValidator.java @@ -11,12 +11,20 @@ public class PasswordPolicyValidator { private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+=\\-{};:'\",.<>/?]).{8,20}$"); - public static void validate(String password) { - if (password == null || password.trim().isEmpty()) { + public static String getValidatedPassword(String password) { + String trimmed = sanitize(password); + + if (!PASSWORD_PATTERN.matcher(trimmed).matches()) { throw new CustomException(ErrorCode.INVALID_INPUT); } - if (!PASSWORD_PATTERN.matcher(password).matches()) { + + return trimmed; + } + + private static String sanitize(String password) { + if (password == null || password.isBlank()) { throw new CustomException(ErrorCode.INVALID_INPUT); } + return password.trim(); } } diff --git a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java index a19fd760..fd1eea97 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java @@ -113,8 +113,8 @@ public ResponseEntity> handle(MethodArgumentNotValidExceptio @Test @DisplayName("POST /api/auth/login - 로그인 성공 시 200 OK") void login_success() throws Exception { - LoginRequest req = new LoginRequest("hong@example.com", "Password123!"); - LoginResponse resp = LoginResponse.builder() + AuthRequest req = new AuthRequest("hong@example.com", "Password123!"); + AuthResponse resp = AuthResponse.builder() .accessToken("mockAccessToken") .refreshToken("mockRefreshToken") .userId(UUID.randomUUID()) @@ -124,7 +124,7 @@ void login_success() throws Exception { .point(100) .build(); - when(authService.login(any(LoginRequest.class))).thenReturn(resp); + when(authService.login(any(AuthRequest.class))).thenReturn(resp); mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON) @@ -141,10 +141,10 @@ void login_success() throws Exception { @Test @DisplayName("POST /api/auth/login - 존재하지 않는 사용자면 404 반환") void login_userNotFound() throws Exception { - when(authService.login(any(LoginRequest.class))) + when(authService.login(any(AuthRequest.class))) .thenThrow(new CustomException(ErrorCode.USER_NOT_FOUND)); - LoginRequest req = new LoginRequest("notfound@example.com", "Password123!"); + AuthRequest req = new AuthRequest("notfound@example.com", "Password123!"); mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) @@ -156,10 +156,10 @@ void login_userNotFound() throws Exception { @Test @DisplayName("POST /api/auth/login - 비밀번호 틀리면 401 반환") void login_wrongPassword() throws Exception { - when(authService.login(any(LoginRequest.class))) + when(authService.login(any(AuthRequest.class))) .thenThrow(new CustomException(ErrorCode.UNAUTHORIZED)); - LoginRequest req = new LoginRequest("hong@example.com", "WrongPassword!"); + AuthRequest req = new AuthRequest("hong@example.com", "WrongPassword!"); mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java index b175110a..56f08bed 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java @@ -13,8 +13,8 @@ import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.repository.UserRepository; -import org.sejongisc.backend.common.auth.dto.LoginRequest; -import org.sejongisc.backend.common.auth.dto.LoginResponse; +import org.sejongisc.backend.common.auth.dto.AuthRequest; +import org.sejongisc.backend.common.auth.dto.AuthResponse; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.springframework.security.crypto.password.PasswordEncoder; @@ -67,8 +67,8 @@ void login_success() { .point(100) .build(); - LoginRequest request = new LoginRequest(); - request.setEmail("test@example.com"); + AuthRequest request = new AuthRequest(); + request.setStudentNumber("test@example.com"); request.setPassword(rawPassword); given(userRepository.findUserByEmail("test@example.com")) @@ -78,7 +78,7 @@ void login_success() { .willReturn("mocked-jwt-token"); // when - LoginResponse response = authService.login(request); + AuthResponse response = authService.login(request); // then assertThat(response).isNotNull(); @@ -93,8 +93,8 @@ void login_success() { @DisplayName("이메일이 존재하지 않으면 USER_NOT_FOUND 예외 발생") void login_userNotFound() { // given - LoginRequest request = new LoginRequest(); - request.setEmail("notfound@example.com"); + AuthRequest request = new AuthRequest(); + request.setStudentNumber("notfound@example.com"); request.setPassword("password"); given(userRepository.findUserByEmail("notfound@example.com")) @@ -122,8 +122,8 @@ void login_wrongPassword() { .point(50) .build(); - LoginRequest request = new LoginRequest(); - request.setEmail("test@example.com"); + AuthRequest request = new AuthRequest(); + request.setStudentNumber("test@example.com"); request.setPassword("wrongPassword"); given(userRepository.findUserByEmail("test@example.com")) @@ -152,8 +152,8 @@ void login_nullPassword() { .point(0) .build(); - LoginRequest request = new LoginRequest(); - request.setEmail("test@example.com"); + AuthRequest request = new AuthRequest(); + request.setStudentNumber("test@example.com"); request.setPassword("somePassword"); given(userRepository.findUserByEmail("test@example.com")) diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceTest.java similarity index 98% rename from backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java rename to backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceTest.java index d36fbabf..d869aacd 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceTest.java @@ -9,7 +9,7 @@ import org.sejongisc.backend.common.auth.entity.RefreshToken; import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; import org.sejongisc.backend.common.auth.jwt.JwtProvider; -import org.sejongisc.backend.common.auth.service.RefreshTokenServiceImpl; +import org.sejongisc.backend.common.auth.service.RefreshTokenService; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.repository.UserRepository; @@ -23,7 +23,7 @@ @ExtendWith(MockitoExtension.class) -class RefreshTokenServiceImplTest { +class RefreshTokenServiceTest { @Mock private RefreshTokenRepository refreshTokenRepository; @@ -35,7 +35,7 @@ class RefreshTokenServiceImplTest { private JwtProvider jwtProvider; @InjectMocks - private RefreshTokenServiceImpl refreshTokenService; + private RefreshTokenService refreshTokenService; private UUID userId; private String refreshToken; diff --git a/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java b/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java index 71cbbcc9..64e9bdf3 100644 --- a/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; import org.sejongisc.backend.common.config.SecurityConfig; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.common.auth.jwt.JwtParser; import org.sejongisc.backend.template.dto.TemplateRequest; import org.sejongisc.backend.template.dto.TemplateResponse; @@ -32,7 +32,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -64,7 +63,7 @@ class TemplateControllerTest { .userId(uid).name("tester").email("test@example.com") .role(Role.TEAM_MEMBER).point(0).build(); - LoginResponse.CustomUserDetails customUserDetails = new LoginResponse.CustomUserDetails(domainUser); + CustomUserDetails customUserDetails = new CustomUserDetails(domainUser); // SecurityConfig 에서 hasRole("TEAM_MEMBER") 라면 ROLE_ 접두어 필요 return new UsernamePasswordAuthenticationToken(customUserDetails, "", List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER"))); diff --git a/backend/src/test/java/org/sejongisc/backend/user/controller/UserControllerTest.java b/backend/src/test/java/org/sejongisc/backend/user/controller/UserControllerTest.java index d5f02b12..2b8f3cd8 100644 --- a/backend/src/test/java/org/sejongisc/backend/user/controller/UserControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/user/controller/UserControllerTest.java @@ -9,7 +9,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.dto.LoginResponse; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; import org.sejongisc.backend.user.service.UserService; diff --git a/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java b/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java index f1409e7b..7c58b464 100644 --- a/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java @@ -288,42 +288,7 @@ void findOrCreateUser_newUser() { verify(oauthAccountRepository).save(any(UserOauthAccount.class)); } - @Test - @DisplayName("회원정보 수정 성공: 이름, 전화번호, 비밀번호 변경") - void updateUser_success() { - // given - UUID userId = UUID.randomUUID(); - User existingUser = User.builder() - .userId(userId) - .name("기존이름") - .phoneNumber("010-1111-1111") - .passwordHash("OLD_HASH") - .role(Role.TEAM_MEMBER) - .build(); - when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser)); - when(passwordEncoder.encode("newPassword123")).thenReturn("NEW_HASH"); - - // 수정 요청 DTO - var request = new org.sejongisc.backend.user.dto.UserUpdateRequest(); - request.setName("새이름"); - request.setPhoneNumber("010-2222-3333"); - request.setPassword("newPassword123"); - - // when - userService.updateUser(userId, request); - - // then - assertAll( - () -> assertThat(existingUser.getName()).isEqualTo("새이름"), - () -> assertThat(existingUser.getPhoneNumber()).isEqualTo("010-2222-3333"), - () -> assertThat(existingUser.getPasswordHash()).isEqualTo("NEW_HASH") - ); - - verify(userRepository).findById(userId); - verify(passwordEncoder).encode("newPassword123"); - verify(userRepository).save(existingUser); - } @Test @DisplayName("회원정보 수정 실패: 존재하지 않는 사용자일 경우 예외 발생") From 751ba5f467e0c37ed0d643915ca8c88c3ec23ab4 Mon Sep 17 00:00:00 2001 From: Wi Seungjae <132977754+discipline24@users.noreply.github.com> Date: Sat, 31 Jan 2026 01:04:42 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[BE]=20[FEAT]=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9C=A0=EC=A7=80=EB=B3=B4=EC=88=98=EC=84=B1=20=ED=96=A5?= =?UTF-8?q?=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin 관련 컨트롤러 스켈레톤 추가 - 개발자 어드민 페이지 위한 설정 추가 - 인증 관련 URL 클래스화 - TODO, 설명 주석 추가 - --- backend/build.gradle | 36 ++++++--- .../sejongisc/backend/BackendApplication.java | 2 + .../controller/AdminAttendanceController.java | 4 + .../controller/AdminBoardController.java | 4 + .../controller/AdminDashboardController.java | 4 + .../controller/AdminPointController.java | 4 + .../admin/controller/AdminUserController.java | 24 ++++-- .../auth/filter/JwtAuthenticationFilter.java | 32 +------- .../{ => db}/PrimaryDataSourceConfig.java | 2 +- .../config/{ => db}/RestTemplateConfig.java | 2 +- .../{ => db}/StockDataSourceConfig.java | 2 +- .../config/{ => security}/SecurityConfig.java | 77 +++++++++---------- .../config/security/SecurityConstants.java | 38 +++++++++ .../config/{ => swagger}/OpenApiConfig.java | 2 +- .../config/{ => swagger}/SwaggerConfig.java | 2 +- .../sejongisc/backend/user/entity/Role.java | 2 + .../auth/controller/AuthControllerTest.java | 2 +- .../controller/TemplateControllerTest.java | 2 +- .../backend/user/service/UserServiceTest.java | 8 +- 19 files changed, 152 insertions(+), 97 deletions(-) create mode 100644 backend/src/main/java/org/sejongisc/backend/admin/controller/AdminAttendanceController.java create mode 100644 backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java create mode 100644 backend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.java create mode 100644 backend/src/main/java/org/sejongisc/backend/admin/controller/AdminPointController.java rename backend/src/main/java/org/sejongisc/backend/common/config/{ => db}/PrimaryDataSourceConfig.java (98%) rename backend/src/main/java/org/sejongisc/backend/common/config/{ => db}/RestTemplateConfig.java (93%) rename backend/src/main/java/org/sejongisc/backend/common/config/{ => db}/StockDataSourceConfig.java (98%) rename backend/src/main/java/org/sejongisc/backend/common/config/{ => security}/SecurityConfig.java (74%) create mode 100644 backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java rename backend/src/main/java/org/sejongisc/backend/common/config/{ => swagger}/OpenApiConfig.java (90%) rename backend/src/main/java/org/sejongisc/backend/common/config/{ => swagger}/SwaggerConfig.java (96%) diff --git a/backend/build.gradle b/backend/build.gradle index 472159ba..f4361485 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -31,29 +31,45 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // OAuth2 Client implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // Test Dependencies + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'io.projectreactor:reactor-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Lombok compileOnly 'org.projectlombok:lombok' - runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - + + // PostgreSQL Driver + runtimeOnly 'org.postgresql:postgresql' + + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + // OkHttp MockWebServer testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' - implementation 'org.springframework.boot:spring-boot-starter-actuator' // Mockito + AssertJ testImplementation "org.mockito:mockito-junit-jupiter:5.12.0" testImplementation "org.assertj:assertj-core:3.26.3" - // GreenMail: SMTP 가짜 서버 - testImplementation "com.icegreen:greenmail:1.6.14" + // mail + testImplementation "com.icegreen:greenmail:1.6.14" // GreenMail: SMTP 가짜 서버 + implementation 'org.springframework.boot:spring-boot-starter-mail' + + // Spring Boot Admin Server & Client + implementation 'de.codecentric:spring-boot-admin-starter-server:3.2.1' + implementation 'de.codecentric:spring-boot-admin-starter-client:3.2.1' + // Actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' // spring-retry implementation 'org.springframework.retry:spring-retry' @@ -64,16 +80,12 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' - implementation 'org.springframework.boot:spring-boot-starter-validation' - // backtesting library implementation 'org.ta4j:ta4j-core:0.15' - // mail - implementation 'org.springframework.boot:spring-boot-starter-mail' - //validation implementation 'commons-validator:commons-validator:1.10.0' + implementation 'org.springframework.boot:spring-boot-starter-validation' // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/backend/src/main/java/org/sejongisc/backend/BackendApplication.java b/backend/src/main/java/org/sejongisc/backend/BackendApplication.java index 2e9b11af..878432de 100644 --- a/backend/src/main/java/org/sejongisc/backend/BackendApplication.java +++ b/backend/src/main/java/org/sejongisc/backend/BackendApplication.java @@ -1,5 +1,6 @@ package org.sejongisc.backend; +import de.codecentric.boot.admin.server.config.EnableAdminServer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @@ -9,6 +10,7 @@ @EnableScheduling @EnableJpaAuditing @EnableRetry +@EnableAdminServer @SpringBootApplication public class BackendApplication { diff --git a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminAttendanceController.java b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminAttendanceController.java new file mode 100644 index 00000000..07842fbd --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminAttendanceController.java @@ -0,0 +1,4 @@ +package org.sejongisc.backend.admin.controller; + +public class AdminAttendanceController { +} diff --git a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java new file mode 100644 index 00000000..f1a371b7 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java @@ -0,0 +1,4 @@ +package org.sejongisc.backend.admin.controller; + +public class AdminBoardController { +} diff --git a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.java b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.java new file mode 100644 index 00000000..50449fd4 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.java @@ -0,0 +1,4 @@ +package org.sejongisc.backend.admin.controller; + +public class AdminDashboardController { +} diff --git a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminPointController.java b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminPointController.java new file mode 100644 index 00000000..e7642a27 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminPointController.java @@ -0,0 +1,4 @@ +package org.sejongisc.backend.admin.controller; + +public class AdminPointController { +} diff --git a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java index 7ffa162c..2e733a27 100644 --- a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java +++ b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java @@ -8,9 +8,11 @@ import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.UserStatus; import org.sejongisc.backend.user.service.UserService; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.Map; @@ -18,7 +20,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/admin") +@RequestMapping("/api/admin/users") @Tag(name = "관리자 API", description = "운영진 및 개발자용 회원 관리 API") public class AdminUserController { @@ -26,16 +28,26 @@ public class AdminUserController { // --- [회장/운영진용] 회원 관리 API --- + @Operation(summary = "엑셀 명단 업로드 및 동기화", description = "엑셀 파일을 업로드하여 신규 회원을 등록하고, 기존 회원의 기수/직위를 갱신합니다.") + @PostMapping(value = "/upload-excel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") // 회장, 개발자만 가능 + public ResponseEntity uploadMemberExcel(@RequestPart("file") MultipartFile file) { + // 엑셀 파싱 및 DB 동기화 결과 반환 + //ExcelSyncResultDto result = adminUserService.syncMembersFromExcel(file); + //return ResponseEntity.ok(result); + return null; + } + @Operation(summary = "전체 회원 목록 조회", description = "모든 회원의 정보를 조회합니다. (회장/관리자용)") - @GetMapping("/users") + @GetMapping("") @PreAuthorize("hasAnyRole('SYSTEM_ADMIN', 'MANAGER')") public ResponseEntity> getAllUsers(@RequestBody AdminUserRequest request) { - //return ResponseEntity.ok(userService.findAllUsers()); // TODO : 전체 조회, 기수별 조회, 이름 검색 등 기능 추가 + //return ResponseEntity.ok(userService.findAllUsers()); // TODO : 전체 조회, 기수별 조회, 이름 검색 등 기능 추가 (페이징은 추후 고려) return null; } @Operation(summary = "회원 활동 상태 변경", description = "ACTIVE, INACTIVE, GRADUATED 등으로 상태를 변경합니다.") - @PatchMapping("/users/{userId}/status") + @PatchMapping("/{userId}/status") @PreAuthorize("hasAnyRole('SYSTEM_ADMIN', 'MANAGER')") public ResponseEntity updateUserStatus( @PathVariable UUID userId, @@ -47,7 +59,7 @@ public ResponseEntity updateUserStatus( // --- [시스템 관리자용 or 회장용] 권한 및 계정 제어 API --- // TODO : 회장 권한 논의 필요 @Operation(summary = "회원 권한 변경", description = "특정 유저의 Role(PRESIDENT, VICE_PRESIDENT, TEAM_LEADER)을 변경합니다.)") - @PatchMapping("/users/{userId}/role") + @PatchMapping("/{userId}/role") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ResponseEntity updateUserRole( @PathVariable UUID userId, @@ -57,7 +69,7 @@ public ResponseEntity updateUserRole( } @Operation(summary = "회원 강제 탈퇴", description = "시스템에서 유저를 완전히 삭제합니다. (시스템 관리자용)") - @DeleteMapping("/users/{userId}") + @DeleteMapping("/{userId}") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ResponseEntity forceDeleteUser(@PathVariable UUID userId) { //userService.deleteUserWithOauth(userId); diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java index ed325b38..77264785 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java @@ -1,8 +1,6 @@ package org.sejongisc.backend.common.auth.filter; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -11,11 +9,12 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotNull; import java.io.IOException; -import java.util.List; +import java.util.Arrays; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.common.auth.jwt.JwtParser; +import org.sejongisc.backend.common.config.security.SecurityConstants; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.common.exception.ErrorResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -36,22 +35,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final ObjectMapper objectMapper; - // TODO : 인증 제외 경로 클래스화 필요 (securityConfig와 중복) + JWTParser, JWTProvider 코드 중복 개선 필요 - private static final List EXCLUDE_PATTERNS = List.of( - "/api/auth/signup", - "/api/auth/login", - "/api/auth/login/**", - "/api/auth/logout", - "/api/auth/reissue", - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui/index.html", - "/swagger-resources/**", - "/webjars/**", - "/login/**", - "/oauth2/**" - ); - @Override protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @@ -68,7 +51,6 @@ protected void doFilterInternal(@NotNull HttpServletRequest request, try { String token = resolveTokenFromHeader(request); - if (token == null) { token = resolveTokenFromCookie(request); } @@ -99,7 +81,7 @@ protected boolean shouldNotFilter(HttpServletRequest request) { return true; } - boolean excluded = EXCLUDE_PATTERNS.stream() + boolean excluded = Arrays.stream(SecurityConstants.WHITELIST_URLS) .anyMatch(pattern -> pathMatcher.match(pattern, path)); // 어떤 요청이 필터 예외로 분류됐는지 콘솔에 표시 @@ -132,14 +114,6 @@ private String resolveTokenFromCookie(HttpServletRequest request) { return cookie.getValue(); } } - return null; } - - private String toJson(ErrorResponse errorResponse) throws JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - return mapper.writeValueAsString(errorResponse); - } - } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java similarity index 98% rename from backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java index 3787f14c..2b505ac0 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.config; +package org.sejongisc.backend.common.config.db; import jakarta.persistence.EntityManagerFactory; import org.springframework.beans.factory.annotation.Qualifier; diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/RestTemplateConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/db/RestTemplateConfig.java similarity index 93% rename from backend/src/main/java/org/sejongisc/backend/common/config/RestTemplateConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/db/RestTemplateConfig.java index bf9252b6..db3ced53 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/RestTemplateConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/db/RestTemplateConfig.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.config; +package org.sejongisc.backend.common.config.db; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/db/StockDataSourceConfig.java similarity index 98% rename from backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/db/StockDataSourceConfig.java index 552b4d81..d46d05ff 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/db/StockDataSourceConfig.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.config; +package org.sejongisc.backend.common.config.db; import jakarta.persistence.EntityManagerFactory; import org.springframework.beans.factory.annotation.Qualifier; diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/SecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java similarity index 74% rename from backend/src/main/java/org/sejongisc/backend/common/config/SecurityConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java index c4d8ccb6..343f3af3 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/SecurityConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java @@ -1,14 +1,17 @@ -package org.sejongisc.backend.common.config; +package org.sejongisc.backend.common.config.security; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.common.auth.service.oauth2.GithubServiceImpl; import org.sejongisc.backend.common.exception.controller.JwtAccessDeniedHandler; import org.sejongisc.backend.common.exception.controller.JwtAuthenticationEntryPoint; import org.sejongisc.backend.common.auth.filter.JwtAuthenticationFilter; +import org.sejongisc.backend.user.entity.Role; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -42,17 +45,23 @@ public class SecurityConfig { private final Environment env; - private boolean isProd() { - return List.of(env.getActiveProfiles()).contains("prod"); - } - private boolean isDev() { - return List.of(env.getActiveProfiles()).contains("dev"); - } - @Bean public AuthorizationRequestRepository authorizationRequestRepository() { return new HttpSessionOAuth2AuthorizationRequestRepository(); } + + // 계층적 권한 설정 + @Bean + public RoleHierarchy roleHierarchy() { + return RoleHierarchyImpl.withDefaultRolePrefix() + .role(Role.SYSTEM_ADMIN.name()).implies(Role.PRESIDENT.name()) + .role(Role.PRESIDENT.name()).implies(Role.VICE_PRESIDENT.name()) + .role(Role.VICE_PRESIDENT.name()).implies(Role.TEAM_LEADER.name()) + .role(Role.TEAM_LEADER.name()).implies(Role.TEAM_MEMBER.name()) + // PENDING_MEMBER는 계층에 포함시키지 않음 (접근 불가) + .build(); + } + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -87,38 +96,23 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ) .authorizeHttpRequests(auth -> { - auth - .requestMatchers( - "/api/auth/signup", - "/api/auth/login", - "/api/auth/login/**", - "/actuator", - "/actuator/**", - "/api/auth/logout", - "/api/auth/reissue", - "/v3/api-docs/**", - "/swagger-ui/**", - "/api/user/id/find", - "/api/user/password/reset/**", - "/api/email/**", - "/swagger-resources/**", - "/webjars/**", - "/login/**", - "/oauth2/**" - ).permitAll(); - - auth.requestMatchers( - "/api/user/**", - "/api/user-bets/**" - ).authenticated(); - - auth.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() -// .anyRequest().authenticated(); - .anyRequest().permitAll(); + // 모두 접근 가능한 API + auth.requestMatchers(SecurityConstants.WHITELIST_URLS).permitAll(); + // 관리자 전용 API + auth.requestMatchers(SecurityConstants.ADMIN_ONLY_URLS).hasAnyRole(Role.PRESIDENT.name(), Role.SYSTEM_ADMIN.name()); + // 일반 서비스 API (정회원 이상만 접근 가능, PENDING_MEMBER 자동 차단) + // RoleHierarchy 덕분에 TEAM_MEMBER만 적어도 상위 직급은 다 통과됨 + auth.requestMatchers(SecurityConstants.MEMBER_ONLY_URLS).hasRole(Role.TEAM_MEMBER.name()); + + auth.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + + .anyRequest().authenticated(); + //.anyRequest().permitAll(); }) + //꼭 필요할 때만(OAuth 로그인 과정 등) 세션 생성 + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)); + // TODO : OAUTH2를 쿠키에 저장 시 OR OAUTH2 를 안쓸 시 STATELESS로 변경 고려 //.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - .sessionManagement(session -> - session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)); if(jwtAuthenticationFilter != null) { http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); @@ -146,7 +140,12 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } - + private boolean isProd() { + return List.of(env.getActiveProfiles()).contains("prod"); + } + private boolean isDev() { + return List.of(env.getActiveProfiles()).contains("dev"); + } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java new file mode 100644 index 00000000..dbf1e0a2 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java @@ -0,0 +1,38 @@ +package org.sejongisc.backend.common.config.security; + +public class SecurityConstants { + public static final String[] WHITELIST_URLS = { + "/api/auth/signup", + "/api/auth/login", + "/api/auth/login/**", + "/api/auth/logout", + "/api/auth/reissue", + "/api/user/password/reset/**", + "/api/email/**", + "/actuator/**", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/swagger-resources/**", + "/webjars/**", + "/login/**", + "/oauth2/**", + "/favicon.ico", + "/error" + }; + + public static final String[] ADMIN_ONLY_URLS = { + "/api/admin/**" + }; + + // TODO : URL 추가 필요 + public static final String[] MEMBER_ONLY_URLS = { + "/api/user/**", + "/api/user-bets/**", + //"/api/board/**", + //"/api/backtest/**", + //"/api/quant-bot/**", + //"/api/attendance/**" + + }; +} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/OpenApiConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/swagger/OpenApiConfig.java similarity index 90% rename from backend/src/main/java/org/sejongisc/backend/common/config/OpenApiConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/swagger/OpenApiConfig.java index 1239b2ec..068b8116 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/OpenApiConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/swagger/OpenApiConfig.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.config; +package org.sejongisc.backend.common.config.swagger; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.servers.Server; diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/SwaggerConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/swagger/SwaggerConfig.java similarity index 96% rename from backend/src/main/java/org/sejongisc/backend/common/config/SwaggerConfig.java rename to backend/src/main/java/org/sejongisc/backend/common/config/swagger/SwaggerConfig.java index fe289edc..99a71c8d 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/SwaggerConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/swagger/SwaggerConfig.java @@ -1,4 +1,4 @@ -package org.sejongisc.backend.common.config; +package org.sejongisc.backend.common.config.swagger; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/Role.java b/backend/src/main/java/org/sejongisc/backend/user/entity/Role.java index 85b7b2e2..590b5a20 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/Role.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/Role.java @@ -1,5 +1,7 @@ package org.sejongisc.backend.user.entity; +// 일반 회원가입 : 회장 승인이 있어야만 설정 가능 +// 엑셀 회원가입 : 회장 승인 없이 설정 가능 (user.isManagerPosition 으로 판단) public enum Role { SYSTEM_ADMIN, // 시스템 관리자 PRESIDENT, // 회장 diff --git a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java index fd1eea97..c3b8de4a 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java @@ -292,7 +292,7 @@ void signup_success() throws Exception { .name("홍길동") .email("hong@example.com") .password("Password123!") - .role(Role.TEAM_MEMBER) + //.role(Role.TEAM_MEMBER) .phoneNumber("01012345678") .build(); diff --git a/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java b/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java index 64e9bdf3..fc69ba0a 100644 --- a/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.config.SecurityConfig; +import org.sejongisc.backend.common.config.security.SecurityConfig; import org.sejongisc.backend.common.auth.jwt.JwtParser; import org.sejongisc.backend.template.dto.TemplateRequest; import org.sejongisc.backend.template.dto.TemplateResponse; diff --git a/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java b/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java index 7c58b464..fa8792aa 100644 --- a/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java @@ -67,7 +67,7 @@ void signup_success() { .name("홍길동") .email("hong@example.com") .password("Password123!") - .role(Role.TEAM_MEMBER) + //.role(Role.TEAM_MEMBER) .phoneNumber("01012345678") .build(); @@ -115,7 +115,7 @@ void signup_duplicateEmail_throws() { .name("홍길동") .email("dup@example.com") .password("Password123!") - .role(Role.TEAM_MEMBER) + //.role(Role.TEAM_MEMBER) .phoneNumber("01012345678") .build(); @@ -175,7 +175,7 @@ void signup_duplicatePhone_throws() { .name("성춘향") .email("spring@example.com") .password("Password!123") - .role(Role.TEAM_MEMBER) + //.role(Role.TEAM_MEMBER) .phoneNumber("01011112222") .build(); @@ -202,7 +202,7 @@ void signup_dataIntegrityViolation_throws() { .name("임꺽정") .email("im@example.com") .password("Pw123456!") - .role(Role.TEAM_MEMBER) + //.role(Role.TEAM_MEMBER) .phoneNumber("01077778888") .build(); From 08294040db55b6259ee04b758ce2bfa5e9b04083 Mon Sep 17 00:00:00 2001 From: Wi Seungjae <132977754+discipline24@users.noreply.github.com> Date: Sat, 31 Jan 2026 01:44:13 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[BE]=20[FIX]=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sejongisc/backend/common/auth/dto/AuthRequest.java | 2 +- .../backend/common/auth/service/AuthService.java | 2 +- .../backend/common/config/db/PrimaryDataSourceConfig.java | 2 +- .../backend/common/config/security/SecurityConstants.java | 2 +- .../main/java/org/sejongisc/backend/user/entity/User.java | 2 +- .../sejongisc/backend/user/repository/UserRepository.java | 4 ++-- .../sejongisc/backend/auth/service/AuthServiceTest.java | 8 ++++---- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthRequest.java b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthRequest.java index 9cc03400..125caf4c 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/dto/AuthRequest.java @@ -19,7 +19,7 @@ public class AuthRequest { requiredMode = Schema.RequiredMode.REQUIRED ) @NotBlank(message = "학번은 필수 입력값입니다.") - private String studentNumber; + private String studentId; @Schema( description = "사용자 비밀번호 (8자 이상, 특수문자 포함)", diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java index 75246ee1..aba8304f 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java @@ -31,7 +31,7 @@ public class AuthService { @Transactional public AuthResponse login(AuthRequest request) { - User user = userRepository.findUserByEmail(request.getStudentNumber()) + User user = userRepository.findByStudentId(request.getStudentId()) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); if (user.getPasswordHash() == null || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { throw new CustomException(ErrorCode.UNAUTHORIZED); diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java index 2b505ac0..46dc6804 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java @@ -74,7 +74,7 @@ public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory( Map jpaProps = new HashMap<>(); jpaProps.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); - jpaProps.put("hibernate.hbm2ddl.auto", "update"); + jpaProps.put("hibernate.hbm2ddl.auto", "create"); return builder .dataSource(dataSource) diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java index dbf1e0a2..565e8b4c 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java @@ -2,7 +2,7 @@ public class SecurityConstants { public static final String[] WHITELIST_URLS = { - "/api/auth/signup", + "/api/user/signup", "/api/auth/login", "/api/auth/login/**", "/api/auth/logout", diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java index 7b372086..446b2601 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java @@ -106,7 +106,7 @@ public void updatePoint(int amount) { public static User createUserWithSignupAndPending(SignupRequest request, String encodedPw) { return User.builder() - .role(Role.PENDING_MEMBER) + .role(Role.TEAM_MEMBER) // TODO : 운영진 승인 로직 추가 후 PENDING_MEMBER로 변경 필요 .studentId(request.getStudentId()) .name(request.getName()) .passwordHash(encodedPw) diff --git a/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java index d22ea3c9..bb3c1cf8 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java @@ -20,11 +20,11 @@ public interface UserRepository extends JpaRepository { Optional findUserByEmail(String email); - Optional findByNameAndPhoneNumber(String name, String phoneNumber); - @Query( "SELECT u FROM User u " + "LEFT JOIN Account a ON u.userId = a.ownerId " + "WHERE a.accountId IS NULL") List findAllUsersMissingAccount(); + + Optional findByStudentId(String studentId); } diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java index 56f08bed..ddaecbf5 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java @@ -68,7 +68,7 @@ void login_success() { .build(); AuthRequest request = new AuthRequest(); - request.setStudentNumber("test@example.com"); + request.setStudentId("test@example.com"); request.setPassword(rawPassword); given(userRepository.findUserByEmail("test@example.com")) @@ -94,7 +94,7 @@ void login_success() { void login_userNotFound() { // given AuthRequest request = new AuthRequest(); - request.setStudentNumber("notfound@example.com"); + request.setStudentId("notfound@example.com"); request.setPassword("password"); given(userRepository.findUserByEmail("notfound@example.com")) @@ -123,7 +123,7 @@ void login_wrongPassword() { .build(); AuthRequest request = new AuthRequest(); - request.setStudentNumber("test@example.com"); + request.setStudentId("test@example.com"); request.setPassword("wrongPassword"); given(userRepository.findUserByEmail("test@example.com")) @@ -153,7 +153,7 @@ void login_nullPassword() { .build(); AuthRequest request = new AuthRequest(); - request.setStudentNumber("test@example.com"); + request.setStudentId("test@example.com"); request.setPassword("somePassword"); given(userRepository.findUserByEmail("test@example.com")) From 8125c9dd20c9e35f85560d2439bee5ac44895e63 Mon Sep 17 00:00:00 2001 From: Wi Seungjae <132977754+discipline24@users.noreply.github.com> Date: Sat, 31 Jan 2026 02:12:00 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[BE]=20[FIX]=20=ED=86=A0=EB=81=BC=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 5 ++--- .../org/sejongisc/backend/common/auth/jwt/JwtParser.java | 2 +- .../sejongisc/backend/common/auth/service/AuthService.java | 4 +++- .../sejongisc/backend/user/controller/UserController.java | 3 --- .../org/sejongisc/backend/user/dto/UserUpdateRequest.java | 2 +- .../sejongisc/backend/user/repository/UserRepository.java | 5 +---- .../java/org/sejongisc/backend/user/service/UserService.java | 3 ++- 7 files changed, 10 insertions(+), 14 deletions(-) diff --git a/backend/build.gradle b/backend/build.gradle index f4361485..8baebbee 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -28,7 +28,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webflux' @@ -65,8 +64,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-mail' // Spring Boot Admin Server & Client - implementation 'de.codecentric:spring-boot-admin-starter-server:3.2.1' - implementation 'de.codecentric:spring-boot-admin-starter-client:3.2.1' + implementation 'de.codecentric:spring-boot-admin-starter-server:3.5.7' + implementation 'de.codecentric:spring-boot-admin-starter-client:3.5.7' // Actuator implementation 'org.springframework.boot:spring-boot-starter-actuator' diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java index d68aec6b..0c6bfca3 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java @@ -46,7 +46,7 @@ public boolean validationToken(String token) { // Authentication 생성 public UsernamePasswordAuthenticationToken getAuthentication(String token) { Claims claims = parseClaims(token); - // TODO : 유지보수성을 위해 클레임 키 상수화 고려 및 + // TODO : 유지보수성을 위해 클레임 키 상수화 고려 String userId = claims.get("uid", String.class); String roleStr = claims.get("role", String.class); if (roleStr == null) { diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java index aba8304f..3950c2ad 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java @@ -13,6 +13,7 @@ import org.sejongisc.backend.common.auth.dto.AuthRequest; import org.sejongisc.backend.common.auth.dto.AuthResponse; import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.util.PasswordPolicyValidator; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -33,7 +34,8 @@ public class AuthService { public AuthResponse login(AuthRequest request) { User user = userRepository.findByStudentId(request.getStudentId()) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - if (user.getPasswordHash() == null || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { + String trimmedPassword = PasswordPolicyValidator.getValidatedPassword(request.getPassword()); + if (user.getPasswordHash() == null || !passwordEncoder.matches(trimmedPassword, user.getPasswordHash())) { throw new CustomException(ErrorCode.UNAUTHORIZED); } String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail()); diff --git a/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java b/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java index 27ba4586..7c1616f3 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java +++ b/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java @@ -29,7 +29,6 @@ public class UserController { private final UserService userService; - private final RefreshTokenService refreshTokenService; private final AuthCookieHelper authCookieHelper; @Operation(summary = "회원 가입", description = "회장이 승인하기 전까지 PENDING 상태가 유지되며, 웹사이트를 사용할 수 없습니다.") @@ -43,8 +42,6 @@ public ResponseEntity signup(@Valid @RequestBody SignupRequest r @DeleteMapping("/withdraw") public ResponseEntity withdraw(@AuthenticationPrincipal CustomUserDetails user) { userService.deleteUserSoftDelete(user.getUserId()); - refreshTokenService.deleteByUserId(user.getUserId()); - return ResponseEntity.noContent() .header(HttpHeaders.SET_COOKIE, authCookieHelper.deleteCookie("refresh").toString()) .build(); diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java b/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java index 75cabfc8..8673fd32 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java @@ -42,7 +42,7 @@ public class UserUpdateRequest { example = "password123!" ) @Size(min = 8, message = "비밀번호는 최소 8자 이상 입력해야 합니다.") - private String password; + private String currentPassword; @Schema( description = "변경할 비밀번호 (변경 시에만 포함)", diff --git a/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java index bb3c1cf8..56e366a6 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java @@ -1,10 +1,7 @@ package org.sejongisc.backend.user.repository; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; + import org.sejongisc.backend.user.entity.User; -import org.sejongisc.backend.user.service.projection.UserIdNameProjection; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java index 753bb589..7fff227b 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java @@ -83,7 +83,7 @@ public void updateUser(UUID userId, UserUpdateRequest request) { // 비밀번호 변경 로직 (새 비밀번호가 입력된 경우에만 실행) if (request.getNewPassword() != null && !request.getNewPassword().isBlank()) { - if (!passwordEncoder.matches(request.getNewPassword(), request.getNewPassword())) { + if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPasswordHash())) { throw new CustomException(ErrorCode.INVALID_INPUT); // 비밀번호 불일치 에러 } // 새 비밀번호 정제 및 정책 검증 @@ -177,6 +177,7 @@ private void deleteResetTokenFromRedis(String token) { } } + @Transactional public void deleteUserSoftDelete(UUID userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); From 870405bbdb59e69ccfbe0f3bee78da60ba539e14 Mon Sep 17 00:00:00 2001 From: Wi Seungjae <132977754+discipline24@users.noreply.github.com> Date: Sat, 31 Jan 2026 02:25:32 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[BE]=20[FIX]=20=ED=86=A0=EB=81=BC=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sejongisc/backend/user/dto/UserUpdateRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java b/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java index 8673fd32..45d3bf8e 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java @@ -32,7 +32,6 @@ public class UserUpdateRequest { ) private String phoneNumber; */ - @NotBlank(message = "이메일은 필수입니다.") @Email(message = "유효한 이메일 형식이 아닙니다.") @Schema(description = "비밀번호 재설정용 이메일", example = "sira@sejong.ac.kr") private String email;