From efc88f602e1a7d67ef1cdb67439dc2a6fe7866ef Mon Sep 17 00:00:00 2001 From: dungbik Date: Thu, 2 Apr 2026 14:44:56 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Refactor:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{auth => }/application/AuthService.java | 54 ++++---- .../user/application/OAuthService.java | 129 ++++++++++++++++++ .../{user => }/application/UserService.java | 35 +++-- .../user/{auth => }/domain/AuthErrorCode.java | 7 +- .../error => domain}/ImageErrorCode.java | 6 +- .../domain/PasswordResetConstants.java | 2 +- .../user/{auth => }/domain/TokenClaims.java | 2 +- .../user/{auth => }/domain/TokenPair.java | 2 +- .../user/{user => }/domain/UserErrorCode.java | 7 +- .../domain/VerificationConstants.java | 2 +- .../common}/BizException.java | 4 +- .../common}/EmailSendException.java | 2 +- .../error => domain/common}/ErrorCode.java | 2 +- .../{global => domain}/entity/BaseEntity.java | 2 +- .../domain => domain/entity}/OAuthLink.java | 2 +- .../{user/domain => domain/entity}/User.java | 3 +- .../event/EmailVerificationSendEvent.java | 2 +- .../event/PasswordResetCreateEvent.java | 2 +- .../repository}/OAuthLinkRepository.java | 4 +- .../repository}/UserRepository.java | 4 +- .../config/AppConfig.java | 2 +- .../config/ClientProperties.java | 2 +- .../config/GrpcClientConfig.java | 2 +- .../config/JpaAuditingConfig.java | 2 +- .../config/SwaggerConfig.java | 4 +- .../jwt}/JwtProperties.java | 2 +- .../infrastructure/jwt/JwtProvider.java | 10 +- .../EmailVerificationEventListener.java | 10 +- .../listener/PasswordResetEventListener.java | 10 +- .../infrastructure/mail/MailService.java | 2 +- .../mail}/ResendConfig.java | 2 +- .../mail/ResendMailService.java | 6 +- .../mail}/ResendProperties.java | 2 +- .../infrastructure/oauth/GoogleUserInfo.java | 2 +- .../infrastructure/oauth/OAuth2UserInfo.java | 2 +- .../infrastructure/oauth/OAuthApiClient.java | 96 +++++++++++++ .../infrastructure/oauth/OAuthProperties.java | 29 ++++ .../infrastructure/oauth/PkceUtil.java | 2 +- .../redis/EmailVerificationRepository.java | 2 +- .../redis/PasswordResetRepository.java | 2 +- .../redis/PasswordResetTokenGenerator.java | 2 +- .../redis/SessionInvalidationRepository.java | 2 +- .../redis/SocialLinkTokenRepository.java | 2 +- .../redis/TokenBlacklistRepository.java | 2 +- .../redis/VerificationCodeGenerator.java | 2 +- .../grpc/GrpcExceptionHandlerImpl.java | 46 +++++++ .../interfaces/grpc/GrpcUserQueryService.java | 55 ++++++++ .../http}/AuthController.java | 36 ++--- .../user/interfaces/http/OAuthController.java | 101 ++++++++++++++ .../http}/UserController.java | 14 +- .../http/common}/ApiResponse.java | 3 +- .../http/common}/ApiResponseAdvice.java | 2 +- .../http/common}/CookieUtil.java | 2 +- .../http/common}/GlobalExceptionHandler.java | 4 +- .../http/common}/HttpConstants.java | 2 +- .../dto/request/ChangePasswordRequest.java | 2 +- .../dto/request/EmailVerificationRequest.java | 2 +- .../http}/dto/request/EmailVerifyRequest.java | 2 +- .../http}/dto/request/LoginRequest.java | 2 +- .../request/PasswordResetCreateRequest.java | 2 +- .../dto/request/PasswordResetRequest.java | 2 +- .../http}/dto/request/SignupRequest.java | 2 +- .../dto/request/TokenValidateRequest.java | 2 +- .../dto/request/UpdateProfileRequest.java | 2 +- .../http}/dto/response/MyInfoResponse.java | 4 +- .../dto/response/SocialLinkResponse.java | 4 +- .../dto/response/SocialLinksResponse.java | 4 +- .../dto/response/TokenValidateResponse.java | 2 +- .../http}/dto/response/UserInfoResponse.java | 4 +- .../http}/dto/response/UserResponse.java | 4 +- .../dto/response/UserUpdateResponse.java | 4 +- 71 files changed, 629 insertions(+), 154 deletions(-) rename src/main/java/flipnote/user/{auth => }/application/AuthService.java (84%) create mode 100644 src/main/java/flipnote/user/application/OAuthService.java rename src/main/java/flipnote/user/{user => }/application/UserService.java (74%) rename src/main/java/flipnote/user/{auth => }/domain/AuthErrorCode.java (96%) rename src/main/java/flipnote/user/{global/error => domain}/ImageErrorCode.java (86%) rename src/main/java/flipnote/user/{auth => }/domain/PasswordResetConstants.java (85%) rename src/main/java/flipnote/user/{auth => }/domain/TokenClaims.java (73%) rename src/main/java/flipnote/user/{auth => }/domain/TokenPair.java (66%) rename src/main/java/flipnote/user/{user => }/domain/UserErrorCode.java (85%) rename src/main/java/flipnote/user/{auth => }/domain/VerificationConstants.java (85%) rename src/main/java/flipnote/user/{global/exception => domain/common}/BizException.java (66%) rename src/main/java/flipnote/user/{global/exception => domain/common}/EmailSendException.java (77%) rename src/main/java/flipnote/user/{global/error => domain/common}/ErrorCode.java (73%) rename src/main/java/flipnote/user/{global => domain}/entity/BaseEntity.java (94%) rename src/main/java/flipnote/user/{user/domain => domain/entity}/OAuthLink.java (96%) rename src/main/java/flipnote/user/{user/domain => domain/entity}/User.java (96%) rename src/main/java/flipnote/user/{auth => }/domain/event/EmailVerificationSendEvent.java (68%) rename src/main/java/flipnote/user/{auth => }/domain/event/PasswordResetCreateEvent.java (67%) rename src/main/java/flipnote/user/{user/domain => domain/repository}/OAuthLinkRepository.java (90%) rename src/main/java/flipnote/user/{user/domain => domain/repository}/UserRepository.java (85%) rename src/main/java/flipnote/user/{global => infrastructure}/config/AppConfig.java (95%) rename src/main/java/flipnote/user/{global => infrastructure}/config/ClientProperties.java (93%) rename src/main/java/flipnote/user/{global => infrastructure}/config/GrpcClientConfig.java (92%) rename src/main/java/flipnote/user/{global => infrastructure}/config/JpaAuditingConfig.java (82%) rename src/main/java/flipnote/user/{global => infrastructure}/config/SwaggerConfig.java (92%) rename src/main/java/flipnote/user/{global/config => infrastructure/jwt}/JwtProperties.java (89%) rename src/main/java/flipnote/user/{auth => }/infrastructure/jwt/JwtProvider.java (93%) rename src/main/java/flipnote/user/{auth => }/infrastructure/listener/EmailVerificationEventListener.java (70%) rename src/main/java/flipnote/user/{auth => }/infrastructure/listener/PasswordResetEventListener.java (70%) rename src/main/java/flipnote/user/{auth => }/infrastructure/mail/MailService.java (77%) rename src/main/java/flipnote/user/{global/config => infrastructure/mail}/ResendConfig.java (90%) rename src/main/java/flipnote/user/{auth => }/infrastructure/mail/ResendMailService.java (93%) rename src/main/java/flipnote/user/{global/config => infrastructure/mail}/ResendProperties.java (91%) rename src/main/java/flipnote/user/{auth => }/infrastructure/oauth/GoogleUserInfo.java (92%) rename src/main/java/flipnote/user/{auth => }/infrastructure/oauth/OAuth2UserInfo.java (74%) create mode 100644 src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java create mode 100644 src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java rename src/main/java/flipnote/user/{auth => }/infrastructure/oauth/PkceUtil.java (95%) rename src/main/java/flipnote/user/{auth => }/infrastructure/redis/EmailVerificationRepository.java (97%) rename src/main/java/flipnote/user/{auth => }/infrastructure/redis/PasswordResetRepository.java (96%) rename src/main/java/flipnote/user/{auth => }/infrastructure/redis/PasswordResetTokenGenerator.java (81%) rename src/main/java/flipnote/user/{auth => }/infrastructure/redis/SessionInvalidationRepository.java (95%) rename src/main/java/flipnote/user/{auth => }/infrastructure/redis/SocialLinkTokenRepository.java (95%) rename src/main/java/flipnote/user/{auth => }/infrastructure/redis/TokenBlacklistRepository.java (94%) rename src/main/java/flipnote/user/{auth => }/infrastructure/redis/VerificationCodeGenerator.java (87%) create mode 100644 src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java create mode 100644 src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/AuthController.java (82%) create mode 100644 src/main/java/flipnote/user/interfaces/http/OAuthController.java rename src/main/java/flipnote/user/{user/presentation => interfaces/http}/UserController.java (76%) rename src/main/java/flipnote/user/{global/error => interfaces/http/common}/ApiResponse.java (95%) rename src/main/java/flipnote/user/{global/error => interfaces/http/common}/ApiResponseAdvice.java (96%) rename src/main/java/flipnote/user/{global/util => interfaces/http/common}/CookieUtil.java (95%) rename src/main/java/flipnote/user/{global/error => interfaces/http/common}/GlobalExceptionHandler.java (95%) rename src/main/java/flipnote/user/{global/constants => interfaces/http/common}/HttpConstants.java (89%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/request/ChangePasswordRequest.java (90%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/request/EmailVerificationRequest.java (87%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/request/EmailVerifyRequest.java (91%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/request/LoginRequest.java (89%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/request/PasswordResetCreateRequest.java (87%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/request/PasswordResetRequest.java (90%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/request/SignupRequest.java (95%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/request/TokenValidateRequest.java (82%) rename src/main/java/flipnote/user/{user/presentation => interfaces/http}/dto/request/UpdateProfileRequest.java (91%) rename src/main/java/flipnote/user/{user/presentation => interfaces/http}/dto/response/MyInfoResponse.java (91%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/response/SocialLinkResponse.java (83%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/response/SocialLinksResponse.java (82%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/response/TokenValidateResponse.java (78%) rename src/main/java/flipnote/user/{user/presentation => interfaces/http}/dto/response/UserInfoResponse.java (81%) rename src/main/java/flipnote/user/{auth/presentation => interfaces/http}/dto/response/UserResponse.java (72%) rename src/main/java/flipnote/user/{user/presentation => interfaces/http}/dto/response/UserUpdateResponse.java (86%) diff --git a/src/main/java/flipnote/user/auth/application/AuthService.java b/src/main/java/flipnote/user/application/AuthService.java similarity index 84% rename from src/main/java/flipnote/user/auth/application/AuthService.java rename to src/main/java/flipnote/user/application/AuthService.java index 0b81523..4e3a8ba 100644 --- a/src/main/java/flipnote/user/auth/application/AuthService.java +++ b/src/main/java/flipnote/user/application/AuthService.java @@ -1,30 +1,30 @@ -package flipnote.user.auth.application; - -import flipnote.user.auth.domain.AuthErrorCode; -import flipnote.user.auth.domain.TokenClaims; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.auth.domain.event.EmailVerificationSendEvent; -import flipnote.user.auth.domain.event.PasswordResetCreateEvent; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.auth.infrastructure.redis.EmailVerificationRepository; -import flipnote.user.auth.infrastructure.redis.PasswordResetRepository; -import flipnote.user.auth.infrastructure.redis.PasswordResetTokenGenerator; -import flipnote.user.auth.infrastructure.redis.SessionInvalidationRepository; -import flipnote.user.auth.infrastructure.redis.TokenBlacklistRepository; -import flipnote.user.auth.infrastructure.redis.VerificationCodeGenerator; -import flipnote.user.auth.presentation.dto.request.ChangePasswordRequest; -import flipnote.user.auth.presentation.dto.request.LoginRequest; -import flipnote.user.auth.presentation.dto.request.SignupRequest; -import flipnote.user.auth.presentation.dto.response.SocialLinksResponse; -import flipnote.user.auth.presentation.dto.response.TokenValidateResponse; -import flipnote.user.auth.presentation.dto.response.UserResponse; -import flipnote.user.global.config.ClientProperties; -import flipnote.user.global.exception.BizException; -import flipnote.user.user.domain.OAuthLink; -import flipnote.user.user.domain.OAuthLinkRepository; -import flipnote.user.user.domain.User; -import flipnote.user.user.domain.UserErrorCode; -import flipnote.user.user.domain.UserRepository; +package flipnote.user.application; + +import flipnote.user.domain.AuthErrorCode; +import flipnote.user.domain.entity.OAuthLink; +import flipnote.user.domain.repository.OAuthLinkRepository; +import flipnote.user.domain.TokenClaims; +import flipnote.user.domain.TokenPair; +import flipnote.user.domain.entity.User; +import flipnote.user.domain.UserErrorCode; +import flipnote.user.domain.repository.UserRepository; +import flipnote.user.domain.common.BizException; +import flipnote.user.domain.event.EmailVerificationSendEvent; +import flipnote.user.domain.event.PasswordResetCreateEvent; +import flipnote.user.infrastructure.config.ClientProperties; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.infrastructure.redis.EmailVerificationRepository; +import flipnote.user.infrastructure.redis.PasswordResetRepository; +import flipnote.user.infrastructure.redis.PasswordResetTokenGenerator; +import flipnote.user.infrastructure.redis.SessionInvalidationRepository; +import flipnote.user.infrastructure.redis.TokenBlacklistRepository; +import flipnote.user.infrastructure.redis.VerificationCodeGenerator; +import flipnote.user.interfaces.http.dto.request.ChangePasswordRequest; +import flipnote.user.interfaces.http.dto.request.LoginRequest; +import flipnote.user.interfaces.http.dto.request.SignupRequest; +import flipnote.user.interfaces.http.dto.response.SocialLinksResponse; +import flipnote.user.interfaces.http.dto.response.TokenValidateResponse; +import flipnote.user.interfaces.http.dto.response.UserResponse; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/src/main/java/flipnote/user/application/OAuthService.java b/src/main/java/flipnote/user/application/OAuthService.java new file mode 100644 index 0000000..927cabd --- /dev/null +++ b/src/main/java/flipnote/user/application/OAuthService.java @@ -0,0 +1,129 @@ +package flipnote.user.application; + +import flipnote.user.domain.AuthErrorCode; +import flipnote.user.domain.entity.OAuthLink; +import flipnote.user.domain.repository.OAuthLinkRepository; +import flipnote.user.domain.TokenPair; +import flipnote.user.domain.entity.User; +import flipnote.user.domain.UserErrorCode; +import flipnote.user.domain.repository.UserRepository; +import flipnote.user.domain.common.BizException; +import flipnote.user.infrastructure.oauth.OAuthProperties; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.infrastructure.oauth.OAuthApiClient; +import flipnote.user.infrastructure.oauth.OAuth2UserInfo; +import flipnote.user.infrastructure.oauth.PkceUtil; +import flipnote.user.infrastructure.redis.SocialLinkTokenRepository; +import flipnote.user.interfaces.http.common.HttpConstants; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class OAuthService { + + private final PkceUtil pkceUtil; + private final OAuthApiClient oAuthApiClient; + private final OAuthLinkRepository oAuthLinkRepository; + private final UserRepository userRepository; + private final SocialLinkTokenRepository socialLinkTokenRepository; + private final JwtProvider jwtProvider; + private final OAuthProperties oAuthProperties; + + public record AuthorizationRedirect(String authorizeUri, ResponseCookie verifierCookie) {} + + private static final int VERIFIER_COOKIE_MAX_AGE = 180; + + public AuthorizationRedirect getAuthorizationUri(String providerName, HttpServletRequest request, + Long userId) { + OAuthProperties.Provider provider = resolveProvider(providerName); + + String codeVerifier = pkceUtil.generateCodeVerifier(); + String codeChallenge = pkceUtil.generateCodeChallenge(codeVerifier); + + String state = null; + if (userId != null) { + state = UUID.randomUUID().toString(); + socialLinkTokenRepository.save(userId, state); + } + + String authorizeUri = oAuthApiClient.buildAuthorizeUri(request, provider, codeChallenge, state); + + ResponseCookie verifierCookie = ResponseCookie.from(HttpConstants.OAUTH_VERIFIER_COOKIE, codeVerifier) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(VERIFIER_COOKIE_MAX_AGE) + .sameSite("Lax") + .build(); + + return new AuthorizationRedirect(authorizeUri, verifierCookie); + } + + public TokenPair socialLogin(String providerName, String code, String codeVerifier, + HttpServletRequest request) { + OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier, request); + + OAuthLink oAuthLink = oAuthLinkRepository + .findByProviderAndProviderIdWithUser(userInfo.getProvider(), userInfo.getProviderId()) + .orElseThrow(() -> new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT)); + + return jwtProvider.generateTokenPair(oAuthLink.getUser()); + } + + @Transactional + public void linkSocialAccount(String providerName, String code, String state, + String codeVerifier, HttpServletRequest request) { + Long userId = socialLinkTokenRepository.findUserIdByState(state) + .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_SOCIAL_LINK_TOKEN)); + + socialLinkTokenRepository.delete(state); + + OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier, request); + + if (oAuthLinkRepository.existsByUser_IdAndProviderAndProviderId( + userId, userInfo.getProvider(), userInfo.getProviderId())) { + throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT); + } + + User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) + .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); + + OAuthLink link = OAuthLink.builder() + .provider(userInfo.getProvider()) + .providerId(userInfo.getProviderId()) + .user(user) + .build(); + oAuthLinkRepository.save(link); + } + + private OAuth2UserInfo getOAuth2UserInfo(String providerName, String code, + String codeVerifier, HttpServletRequest request) { + OAuthProperties.Provider provider = resolveProvider(providerName); + String accessToken = oAuthApiClient.requestAccessToken(provider, code, codeVerifier, request); + Map attributes = oAuthApiClient.requestUserInfo(provider, accessToken); + return oAuthApiClient.createUserInfo(providerName, attributes); + } + + private OAuthProperties.Provider resolveProvider(String providerName) { + Map providers = oAuthProperties.getProviders(); + if (providers == null) { + throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); + } + OAuthProperties.Provider provider = providers.get(providerName.toLowerCase()); + if (provider == null) { + log.warn("지원하지 않는 OAuth Provider: {}", providerName); + throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); + } + return provider; + } +} diff --git a/src/main/java/flipnote/user/user/application/UserService.java b/src/main/java/flipnote/user/application/UserService.java similarity index 74% rename from src/main/java/flipnote/user/user/application/UserService.java rename to src/main/java/flipnote/user/application/UserService.java index 220bb07..5d50448 100644 --- a/src/main/java/flipnote/user/user/application/UserService.java +++ b/src/main/java/flipnote/user/application/UserService.java @@ -1,4 +1,7 @@ -package flipnote.user.user.application; +package flipnote.user.application; + +import java.util.List; +import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -9,17 +12,17 @@ import flipnote.image.grpc.v1.ChangeImageResponse; import flipnote.image.grpc.v1.ImageCommandServiceGrpc; import flipnote.image.grpc.v1.Type; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.auth.infrastructure.redis.SessionInvalidationRepository; -import flipnote.user.global.error.ImageErrorCode; -import flipnote.user.global.exception.BizException; -import flipnote.user.user.domain.User; -import flipnote.user.user.domain.UserErrorCode; -import flipnote.user.user.domain.UserRepository; -import flipnote.user.user.presentation.dto.request.UpdateProfileRequest; -import flipnote.user.user.presentation.dto.response.MyInfoResponse; -import flipnote.user.user.presentation.dto.response.UserInfoResponse; -import flipnote.user.user.presentation.dto.response.UserUpdateResponse; +import flipnote.user.domain.ImageErrorCode; +import flipnote.user.domain.entity.User; +import flipnote.user.domain.UserErrorCode; +import flipnote.user.domain.repository.UserRepository; +import flipnote.user.domain.common.BizException; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.infrastructure.redis.SessionInvalidationRepository; +import flipnote.user.interfaces.http.dto.request.UpdateProfileRequest; +import flipnote.user.interfaces.http.dto.response.MyInfoResponse; +import flipnote.user.interfaces.http.dto.response.UserInfoResponse; +import flipnote.user.interfaces.http.dto.response.UserUpdateResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -88,6 +91,14 @@ public void withdraw(Long userId) { sessionInvalidationRepository.invalidate(userId, jwtProvider.getRefreshTokenExpiration()); } + public Optional findActiveUserById(Long userId) { + return userRepository.findByIdAndStatus(userId, User.Status.ACTIVE); + } + + public List findActiveUsersByIds(List userIds) { + return userRepository.findByIdInAndStatus(userIds, User.Status.ACTIVE); + } + private User findActiveUser(Long userId) { return userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); diff --git a/src/main/java/flipnote/user/auth/domain/AuthErrorCode.java b/src/main/java/flipnote/user/domain/AuthErrorCode.java similarity index 96% rename from src/main/java/flipnote/user/auth/domain/AuthErrorCode.java rename to src/main/java/flipnote/user/domain/AuthErrorCode.java index 6ff29e5..b93c236 100644 --- a/src/main/java/flipnote/user/auth/domain/AuthErrorCode.java +++ b/src/main/java/flipnote/user/domain/AuthErrorCode.java @@ -1,9 +1,10 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; -import flipnote.user.global.error.ErrorCode; +import org.springframework.http.HttpStatus; + +import flipnote.user.domain.common.ErrorCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor diff --git a/src/main/java/flipnote/user/global/error/ImageErrorCode.java b/src/main/java/flipnote/user/domain/ImageErrorCode.java similarity index 86% rename from src/main/java/flipnote/user/global/error/ImageErrorCode.java rename to src/main/java/flipnote/user/domain/ImageErrorCode.java index 16fc7b1..57670dd 100644 --- a/src/main/java/flipnote/user/global/error/ImageErrorCode.java +++ b/src/main/java/flipnote/user/domain/ImageErrorCode.java @@ -1,8 +1,10 @@ -package flipnote.user.global.error; +package flipnote.user.domain; +import org.springframework.http.HttpStatus; + +import flipnote.user.domain.common.ErrorCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor diff --git a/src/main/java/flipnote/user/auth/domain/PasswordResetConstants.java b/src/main/java/flipnote/user/domain/PasswordResetConstants.java similarity index 85% rename from src/main/java/flipnote/user/auth/domain/PasswordResetConstants.java rename to src/main/java/flipnote/user/domain/PasswordResetConstants.java index 94878de..9767a83 100644 --- a/src/main/java/flipnote/user/auth/domain/PasswordResetConstants.java +++ b/src/main/java/flipnote/user/domain/PasswordResetConstants.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/main/java/flipnote/user/auth/domain/TokenClaims.java b/src/main/java/flipnote/user/domain/TokenClaims.java similarity index 73% rename from src/main/java/flipnote/user/auth/domain/TokenClaims.java rename to src/main/java/flipnote/user/domain/TokenClaims.java index 52bc265..44a252a 100644 --- a/src/main/java/flipnote/user/auth/domain/TokenClaims.java +++ b/src/main/java/flipnote/user/domain/TokenClaims.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; public record TokenClaims( Long userId, diff --git a/src/main/java/flipnote/user/auth/domain/TokenPair.java b/src/main/java/flipnote/user/domain/TokenPair.java similarity index 66% rename from src/main/java/flipnote/user/auth/domain/TokenPair.java rename to src/main/java/flipnote/user/domain/TokenPair.java index 81cb9d4..48ec06a 100644 --- a/src/main/java/flipnote/user/auth/domain/TokenPair.java +++ b/src/main/java/flipnote/user/domain/TokenPair.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; public record TokenPair(String accessToken, String refreshToken) { } diff --git a/src/main/java/flipnote/user/user/domain/UserErrorCode.java b/src/main/java/flipnote/user/domain/UserErrorCode.java similarity index 85% rename from src/main/java/flipnote/user/user/domain/UserErrorCode.java rename to src/main/java/flipnote/user/domain/UserErrorCode.java index d98aa0c..dd4a612 100644 --- a/src/main/java/flipnote/user/user/domain/UserErrorCode.java +++ b/src/main/java/flipnote/user/domain/UserErrorCode.java @@ -1,9 +1,10 @@ -package flipnote.user.user.domain; +package flipnote.user.domain; -import flipnote.user.global.error.ErrorCode; +import org.springframework.http.HttpStatus; + +import flipnote.user.domain.common.ErrorCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor diff --git a/src/main/java/flipnote/user/auth/domain/VerificationConstants.java b/src/main/java/flipnote/user/domain/VerificationConstants.java similarity index 85% rename from src/main/java/flipnote/user/auth/domain/VerificationConstants.java rename to src/main/java/flipnote/user/domain/VerificationConstants.java index d92bc97..31c09db 100644 --- a/src/main/java/flipnote/user/auth/domain/VerificationConstants.java +++ b/src/main/java/flipnote/user/domain/VerificationConstants.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/main/java/flipnote/user/global/exception/BizException.java b/src/main/java/flipnote/user/domain/common/BizException.java similarity index 66% rename from src/main/java/flipnote/user/global/exception/BizException.java rename to src/main/java/flipnote/user/domain/common/BizException.java index f31adef..cfed271 100644 --- a/src/main/java/flipnote/user/global/exception/BizException.java +++ b/src/main/java/flipnote/user/domain/common/BizException.java @@ -1,6 +1,6 @@ -package flipnote.user.global.exception; +package flipnote.user.domain.common; -import flipnote.user.global.error.ErrorCode; +import flipnote.user.domain.common.ErrorCode; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/user/global/exception/EmailSendException.java b/src/main/java/flipnote/user/domain/common/EmailSendException.java similarity index 77% rename from src/main/java/flipnote/user/global/exception/EmailSendException.java rename to src/main/java/flipnote/user/domain/common/EmailSendException.java index e8a11b5..dfe342f 100644 --- a/src/main/java/flipnote/user/global/exception/EmailSendException.java +++ b/src/main/java/flipnote/user/domain/common/EmailSendException.java @@ -1,4 +1,4 @@ -package flipnote.user.global.exception; +package flipnote.user.domain.common; public class EmailSendException extends RuntimeException { diff --git a/src/main/java/flipnote/user/global/error/ErrorCode.java b/src/main/java/flipnote/user/domain/common/ErrorCode.java similarity index 73% rename from src/main/java/flipnote/user/global/error/ErrorCode.java rename to src/main/java/flipnote/user/domain/common/ErrorCode.java index d043924..a12fbc6 100644 --- a/src/main/java/flipnote/user/global/error/ErrorCode.java +++ b/src/main/java/flipnote/user/domain/common/ErrorCode.java @@ -1,4 +1,4 @@ -package flipnote.user.global.error; +package flipnote.user.domain.common; public interface ErrorCode { diff --git a/src/main/java/flipnote/user/global/entity/BaseEntity.java b/src/main/java/flipnote/user/domain/entity/BaseEntity.java similarity index 94% rename from src/main/java/flipnote/user/global/entity/BaseEntity.java rename to src/main/java/flipnote/user/domain/entity/BaseEntity.java index 61b9181..1c1ca7e 100644 --- a/src/main/java/flipnote/user/global/entity/BaseEntity.java +++ b/src/main/java/flipnote/user/domain/entity/BaseEntity.java @@ -1,4 +1,4 @@ -package flipnote.user.global.entity; +package flipnote.user.domain.entity; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; diff --git a/src/main/java/flipnote/user/user/domain/OAuthLink.java b/src/main/java/flipnote/user/domain/entity/OAuthLink.java similarity index 96% rename from src/main/java/flipnote/user/user/domain/OAuthLink.java rename to src/main/java/flipnote/user/domain/entity/OAuthLink.java index 893b542..07abe1c 100644 --- a/src/main/java/flipnote/user/user/domain/OAuthLink.java +++ b/src/main/java/flipnote/user/domain/entity/OAuthLink.java @@ -1,4 +1,4 @@ -package flipnote.user.user.domain; +package flipnote.user.domain.entity; import jakarta.persistence.*; import lombok.AccessLevel; diff --git a/src/main/java/flipnote/user/user/domain/User.java b/src/main/java/flipnote/user/domain/entity/User.java similarity index 96% rename from src/main/java/flipnote/user/user/domain/User.java rename to src/main/java/flipnote/user/domain/entity/User.java index d09f98e..01546d7 100644 --- a/src/main/java/flipnote/user/user/domain/User.java +++ b/src/main/java/flipnote/user/domain/entity/User.java @@ -1,6 +1,5 @@ -package flipnote.user.user.domain; +package flipnote.user.domain.entity; -import flipnote.user.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/flipnote/user/auth/domain/event/EmailVerificationSendEvent.java b/src/main/java/flipnote/user/domain/event/EmailVerificationSendEvent.java similarity index 68% rename from src/main/java/flipnote/user/auth/domain/event/EmailVerificationSendEvent.java rename to src/main/java/flipnote/user/domain/event/EmailVerificationSendEvent.java index 0373988..399867a 100644 --- a/src/main/java/flipnote/user/auth/domain/event/EmailVerificationSendEvent.java +++ b/src/main/java/flipnote/user/domain/event/EmailVerificationSendEvent.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain.event; +package flipnote.user.domain.event; public record EmailVerificationSendEvent( String to, diff --git a/src/main/java/flipnote/user/auth/domain/event/PasswordResetCreateEvent.java b/src/main/java/flipnote/user/domain/event/PasswordResetCreateEvent.java similarity index 67% rename from src/main/java/flipnote/user/auth/domain/event/PasswordResetCreateEvent.java rename to src/main/java/flipnote/user/domain/event/PasswordResetCreateEvent.java index b1c6daf..d0c415a 100644 --- a/src/main/java/flipnote/user/auth/domain/event/PasswordResetCreateEvent.java +++ b/src/main/java/flipnote/user/domain/event/PasswordResetCreateEvent.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain.event; +package flipnote.user.domain.event; public record PasswordResetCreateEvent( String to, diff --git a/src/main/java/flipnote/user/user/domain/OAuthLinkRepository.java b/src/main/java/flipnote/user/domain/repository/OAuthLinkRepository.java similarity index 90% rename from src/main/java/flipnote/user/user/domain/OAuthLinkRepository.java rename to src/main/java/flipnote/user/domain/repository/OAuthLinkRepository.java index fbc55c8..62ffd6a 100644 --- a/src/main/java/flipnote/user/user/domain/OAuthLinkRepository.java +++ b/src/main/java/flipnote/user/domain/repository/OAuthLinkRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.user.domain; +package flipnote.user.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -7,6 +7,8 @@ import java.util.List; import java.util.Optional; +import flipnote.user.domain.entity.OAuthLink; + public interface OAuthLinkRepository extends JpaRepository { @Query(""" diff --git a/src/main/java/flipnote/user/user/domain/UserRepository.java b/src/main/java/flipnote/user/domain/repository/UserRepository.java similarity index 85% rename from src/main/java/flipnote/user/user/domain/UserRepository.java rename to src/main/java/flipnote/user/domain/repository/UserRepository.java index e59c10d..384e675 100644 --- a/src/main/java/flipnote/user/user/domain/UserRepository.java +++ b/src/main/java/flipnote/user/domain/repository/UserRepository.java @@ -1,10 +1,12 @@ -package flipnote.user.user.domain; +package flipnote.user.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Optional; +import flipnote.user.domain.entity.User; + public interface UserRepository extends JpaRepository { Optional findByEmail(String email); diff --git a/src/main/java/flipnote/user/global/config/AppConfig.java b/src/main/java/flipnote/user/infrastructure/config/AppConfig.java similarity index 95% rename from src/main/java/flipnote/user/global/config/AppConfig.java rename to src/main/java/flipnote/user/infrastructure/config/AppConfig.java index c8b5cfe..90b7f0d 100644 --- a/src/main/java/flipnote/user/global/config/AppConfig.java +++ b/src/main/java/flipnote/user/infrastructure/config/AppConfig.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/flipnote/user/global/config/ClientProperties.java b/src/main/java/flipnote/user/infrastructure/config/ClientProperties.java similarity index 93% rename from src/main/java/flipnote/user/global/config/ClientProperties.java rename to src/main/java/flipnote/user/infrastructure/config/ClientProperties.java index 5334472..3edf7d5 100644 --- a/src/main/java/flipnote/user/global/config/ClientProperties.java +++ b/src/main/java/flipnote/user/infrastructure/config/ClientProperties.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/flipnote/user/global/config/GrpcClientConfig.java b/src/main/java/flipnote/user/infrastructure/config/GrpcClientConfig.java similarity index 92% rename from src/main/java/flipnote/user/global/config/GrpcClientConfig.java rename to src/main/java/flipnote/user/infrastructure/config/GrpcClientConfig.java index 2b5eed3..c7bc08d 100644 --- a/src/main/java/flipnote/user/global/config/GrpcClientConfig.java +++ b/src/main/java/flipnote/user/infrastructure/config/GrpcClientConfig.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; import flipnote.image.grpc.v1.ImageCommandServiceGrpc; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/flipnote/user/global/config/JpaAuditingConfig.java b/src/main/java/flipnote/user/infrastructure/config/JpaAuditingConfig.java similarity index 82% rename from src/main/java/flipnote/user/global/config/JpaAuditingConfig.java rename to src/main/java/flipnote/user/infrastructure/config/JpaAuditingConfig.java index 6e3060c..d55f976 100644 --- a/src/main/java/flipnote/user/global/config/JpaAuditingConfig.java +++ b/src/main/java/flipnote/user/infrastructure/config/JpaAuditingConfig.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/src/main/java/flipnote/user/global/config/SwaggerConfig.java b/src/main/java/flipnote/user/infrastructure/config/SwaggerConfig.java similarity index 92% rename from src/main/java/flipnote/user/global/config/SwaggerConfig.java rename to src/main/java/flipnote/user/infrastructure/config/SwaggerConfig.java index 186cc42..4bf33fe 100644 --- a/src/main/java/flipnote/user/global/config/SwaggerConfig.java +++ b/src/main/java/flipnote/user/infrastructure/config/SwaggerConfig.java @@ -1,6 +1,6 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; -import flipnote.user.global.constants.HttpConstants; +import flipnote.user.interfaces.http.common.HttpConstants; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; diff --git a/src/main/java/flipnote/user/global/config/JwtProperties.java b/src/main/java/flipnote/user/infrastructure/jwt/JwtProperties.java similarity index 89% rename from src/main/java/flipnote/user/global/config/JwtProperties.java rename to src/main/java/flipnote/user/infrastructure/jwt/JwtProperties.java index 6feaa72..a7d6f34 100644 --- a/src/main/java/flipnote/user/global/config/JwtProperties.java +++ b/src/main/java/flipnote/user/infrastructure/jwt/JwtProperties.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.jwt; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/flipnote/user/auth/infrastructure/jwt/JwtProvider.java b/src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java similarity index 93% rename from src/main/java/flipnote/user/auth/infrastructure/jwt/JwtProvider.java rename to src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java index 9cd7a6e..276ca0e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/jwt/JwtProvider.java +++ b/src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java @@ -1,9 +1,9 @@ -package flipnote.user.auth.infrastructure.jwt; +package flipnote.user.infrastructure.jwt; -import flipnote.user.auth.domain.TokenClaims; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.user.domain.User; -import flipnote.user.global.config.JwtProperties; +import flipnote.user.domain.TokenClaims; +import flipnote.user.domain.TokenPair; +import flipnote.user.domain.entity.User; +import flipnote.user.infrastructure.jwt.JwtProperties; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; diff --git a/src/main/java/flipnote/user/auth/infrastructure/listener/EmailVerificationEventListener.java b/src/main/java/flipnote/user/infrastructure/listener/EmailVerificationEventListener.java similarity index 70% rename from src/main/java/flipnote/user/auth/infrastructure/listener/EmailVerificationEventListener.java rename to src/main/java/flipnote/user/infrastructure/listener/EmailVerificationEventListener.java index d16fdd6..4dcbefe 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/listener/EmailVerificationEventListener.java +++ b/src/main/java/flipnote/user/infrastructure/listener/EmailVerificationEventListener.java @@ -1,14 +1,14 @@ -package flipnote.user.auth.infrastructure.listener; +package flipnote.user.infrastructure.listener; import org.springframework.context.event.EventListener; import org.springframework.resilience.annotation.Retryable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import flipnote.user.auth.domain.VerificationConstants; -import flipnote.user.auth.domain.event.EmailVerificationSendEvent; -import flipnote.user.auth.infrastructure.mail.MailService; -import flipnote.user.global.exception.EmailSendException; +import flipnote.user.domain.VerificationConstants; +import flipnote.user.domain.common.EmailSendException; +import flipnote.user.domain.event.EmailVerificationSendEvent; +import flipnote.user.infrastructure.mail.MailService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/flipnote/user/auth/infrastructure/listener/PasswordResetEventListener.java b/src/main/java/flipnote/user/infrastructure/listener/PasswordResetEventListener.java similarity index 70% rename from src/main/java/flipnote/user/auth/infrastructure/listener/PasswordResetEventListener.java rename to src/main/java/flipnote/user/infrastructure/listener/PasswordResetEventListener.java index 2518e79..4257c89 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/listener/PasswordResetEventListener.java +++ b/src/main/java/flipnote/user/infrastructure/listener/PasswordResetEventListener.java @@ -1,14 +1,14 @@ -package flipnote.user.auth.infrastructure.listener; +package flipnote.user.infrastructure.listener; import org.springframework.context.event.EventListener; import org.springframework.resilience.annotation.Retryable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import flipnote.user.auth.domain.PasswordResetConstants; -import flipnote.user.auth.domain.event.PasswordResetCreateEvent; -import flipnote.user.auth.infrastructure.mail.MailService; -import flipnote.user.global.exception.EmailSendException; +import flipnote.user.domain.PasswordResetConstants; +import flipnote.user.domain.common.EmailSendException; +import flipnote.user.domain.event.PasswordResetCreateEvent; +import flipnote.user.infrastructure.mail.MailService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/flipnote/user/auth/infrastructure/mail/MailService.java b/src/main/java/flipnote/user/infrastructure/mail/MailService.java similarity index 77% rename from src/main/java/flipnote/user/auth/infrastructure/mail/MailService.java rename to src/main/java/flipnote/user/infrastructure/mail/MailService.java index da8c47c..15df452 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/mail/MailService.java +++ b/src/main/java/flipnote/user/infrastructure/mail/MailService.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.mail; +package flipnote.user.infrastructure.mail; public interface MailService { diff --git a/src/main/java/flipnote/user/global/config/ResendConfig.java b/src/main/java/flipnote/user/infrastructure/mail/ResendConfig.java similarity index 90% rename from src/main/java/flipnote/user/global/config/ResendConfig.java rename to src/main/java/flipnote/user/infrastructure/mail/ResendConfig.java index ed0b74e..c52409f 100644 --- a/src/main/java/flipnote/user/global/config/ResendConfig.java +++ b/src/main/java/flipnote/user/infrastructure/mail/ResendConfig.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.mail; import com.resend.Resend; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/flipnote/user/auth/infrastructure/mail/ResendMailService.java b/src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java similarity index 93% rename from src/main/java/flipnote/user/auth/infrastructure/mail/ResendMailService.java rename to src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java index 4e57af1..4d44bc6 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/mail/ResendMailService.java +++ b/src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java @@ -1,10 +1,10 @@ -package flipnote.user.auth.infrastructure.mail; +package flipnote.user.infrastructure.mail; import com.resend.Resend; import com.resend.core.exception.ResendException; import com.resend.services.emails.model.CreateEmailOptions; -import flipnote.user.global.config.ResendProperties; -import flipnote.user.global.exception.EmailSendException; +import flipnote.user.domain.common.EmailSendException; +import flipnote.user.infrastructure.mail.ResendProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/src/main/java/flipnote/user/global/config/ResendProperties.java b/src/main/java/flipnote/user/infrastructure/mail/ResendProperties.java similarity index 91% rename from src/main/java/flipnote/user/global/config/ResendProperties.java rename to src/main/java/flipnote/user/infrastructure/mail/ResendProperties.java index b1f28c6..7ab8b09 100644 --- a/src/main/java/flipnote/user/global/config/ResendProperties.java +++ b/src/main/java/flipnote/user/infrastructure/mail/ResendProperties.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.mail; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; diff --git a/src/main/java/flipnote/user/auth/infrastructure/oauth/GoogleUserInfo.java b/src/main/java/flipnote/user/infrastructure/oauth/GoogleUserInfo.java similarity index 92% rename from src/main/java/flipnote/user/auth/infrastructure/oauth/GoogleUserInfo.java rename to src/main/java/flipnote/user/infrastructure/oauth/GoogleUserInfo.java index b9e5b01..a1e282e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/oauth/GoogleUserInfo.java +++ b/src/main/java/flipnote/user/infrastructure/oauth/GoogleUserInfo.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.oauth; +package flipnote.user.infrastructure.oauth; import java.util.Map; diff --git a/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuth2UserInfo.java b/src/main/java/flipnote/user/infrastructure/oauth/OAuth2UserInfo.java similarity index 74% rename from src/main/java/flipnote/user/auth/infrastructure/oauth/OAuth2UserInfo.java rename to src/main/java/flipnote/user/infrastructure/oauth/OAuth2UserInfo.java index 5a394a5..339581e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuth2UserInfo.java +++ b/src/main/java/flipnote/user/infrastructure/oauth/OAuth2UserInfo.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.oauth; +package flipnote.user.infrastructure.oauth; public interface OAuth2UserInfo { diff --git a/src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java b/src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java new file mode 100644 index 0000000..cc8d7e0 --- /dev/null +++ b/src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java @@ -0,0 +1,96 @@ +package flipnote.user.infrastructure.oauth; + +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; + +import flipnote.user.infrastructure.oauth.OAuthProperties; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + +@Service +@RequiredArgsConstructor +public class OAuthApiClient { + + private final RestClient restClient; + private final ObjectMapper objectMapper; + + public String requestAccessToken(OAuthProperties.Provider provider, String code, + String codeVerifier, HttpServletRequest request) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", provider.getClientId()); + params.add("client_secret", provider.getClientSecret()); + params.add("redirect_uri", buildRedirectUri(request, provider.getRedirectUri())); + params.add("code", code); + params.add("code_verifier", codeVerifier); + + try { + String responseBody = restClient.post() + .uri(provider.getTokenUri()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(params) + .retrieve() + .body(String.class); + + Map responseMap = objectMapper.readValue(responseBody, new TypeReference<>() {}); + return (String) responseMap.get("access_token"); + } catch (Exception e) { + throw new RuntimeException("Failed to get OAuth access token", e); + } + } + + public Map requestUserInfo(OAuthProperties.Provider provider, String accessToken) { + try { + String responseBody = restClient.get() + .uri(provider.getUserInfoUri()) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .retrieve() + .body(String.class); + + return objectMapper.readValue(responseBody, new TypeReference<>() {}); + } catch (Exception e) { + throw new RuntimeException("Failed to get OAuth user info", e); + } + } + + public OAuth2UserInfo createUserInfo(String providerName, Map attributes) { + return switch (providerName.toLowerCase()) { + case "google" -> new GoogleUserInfo(attributes); + default -> throw new IllegalArgumentException("Unsupported OAuth provider: " + providerName); + }; + } + + public String buildAuthorizeUri(HttpServletRequest request, OAuthProperties.Provider provider, + String codeChallenge, String state) { + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(provider.getAuthorizationUri()) + .queryParam("client_id", provider.getClientId()) + .queryParam("redirect_uri", buildRedirectUri(request, provider.getRedirectUri())) + .queryParam("response_type", "code") + .queryParam("scope", String.join(" ", provider.getScope())) + .queryParam("code_challenge", codeChallenge) + .queryParam("code_challenge_method", "S256"); + + if (state != null) { + builder.queryParam("state", state); + } + + return builder.toUriString(); + } + + private String buildRedirectUri(HttpServletRequest request, String path) { + return ServletUriComponentsBuilder.fromRequestUri(request) + .replacePath(path) + .build() + .toUriString(); + } +} diff --git a/src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java b/src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java new file mode 100644 index 0000000..bde860d --- /dev/null +++ b/src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java @@ -0,0 +1,29 @@ +package flipnote.user.infrastructure.oauth; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; +import java.util.Map; + +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "app.oauth2") +public class OAuthProperties { + + private final Map providers; + + @Getter + @Setter + public static class Provider { + private String clientId; + private String clientSecret; + private String redirectUri; + private String authorizationUri; + private String tokenUri; + private String userInfoUri; + private List scope; + } +} diff --git a/src/main/java/flipnote/user/auth/infrastructure/oauth/PkceUtil.java b/src/main/java/flipnote/user/infrastructure/oauth/PkceUtil.java similarity index 95% rename from src/main/java/flipnote/user/auth/infrastructure/oauth/PkceUtil.java rename to src/main/java/flipnote/user/infrastructure/oauth/PkceUtil.java index 544266c..cf4fe6e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/oauth/PkceUtil.java +++ b/src/main/java/flipnote/user/infrastructure/oauth/PkceUtil.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.oauth; +package flipnote.user.infrastructure.oauth; import org.springframework.stereotype.Component; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/EmailVerificationRepository.java b/src/main/java/flipnote/user/infrastructure/redis/EmailVerificationRepository.java similarity index 97% rename from src/main/java/flipnote/user/auth/infrastructure/redis/EmailVerificationRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/EmailVerificationRepository.java index 6218947..656bc7f 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/EmailVerificationRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/EmailVerificationRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetRepository.java b/src/main/java/flipnote/user/infrastructure/redis/PasswordResetRepository.java similarity index 96% rename from src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/PasswordResetRepository.java index c821d5d..547b880 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/PasswordResetRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetTokenGenerator.java b/src/main/java/flipnote/user/infrastructure/redis/PasswordResetTokenGenerator.java similarity index 81% rename from src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetTokenGenerator.java rename to src/main/java/flipnote/user/infrastructure/redis/PasswordResetTokenGenerator.java index 1e8797f..8d578d8 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetTokenGenerator.java +++ b/src/main/java/flipnote/user/infrastructure/redis/PasswordResetTokenGenerator.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import org.springframework.stereotype.Component; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/SessionInvalidationRepository.java b/src/main/java/flipnote/user/infrastructure/redis/SessionInvalidationRepository.java similarity index 95% rename from src/main/java/flipnote/user/auth/infrastructure/redis/SessionInvalidationRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/SessionInvalidationRepository.java index e494496..887147e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/SessionInvalidationRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/SessionInvalidationRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/SocialLinkTokenRepository.java b/src/main/java/flipnote/user/infrastructure/redis/SocialLinkTokenRepository.java similarity index 95% rename from src/main/java/flipnote/user/auth/infrastructure/redis/SocialLinkTokenRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/SocialLinkTokenRepository.java index 1c4222b..4fb47cd 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/SocialLinkTokenRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/SocialLinkTokenRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/TokenBlacklistRepository.java b/src/main/java/flipnote/user/infrastructure/redis/TokenBlacklistRepository.java similarity index 94% rename from src/main/java/flipnote/user/auth/infrastructure/redis/TokenBlacklistRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/TokenBlacklistRepository.java index 9a1d899..210f8ab 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/TokenBlacklistRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/TokenBlacklistRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/VerificationCodeGenerator.java b/src/main/java/flipnote/user/infrastructure/redis/VerificationCodeGenerator.java similarity index 87% rename from src/main/java/flipnote/user/auth/infrastructure/redis/VerificationCodeGenerator.java rename to src/main/java/flipnote/user/infrastructure/redis/VerificationCodeGenerator.java index 301156d..20b71d3 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/VerificationCodeGenerator.java +++ b/src/main/java/flipnote/user/infrastructure/redis/VerificationCodeGenerator.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import org.springframework.stereotype.Component; diff --git a/src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java b/src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java new file mode 100644 index 0000000..073d90b --- /dev/null +++ b/src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java @@ -0,0 +1,46 @@ +package flipnote.user.interfaces.grpc; + +import flipnote.user.domain.common.BizException; +import flipnote.user.domain.common.ErrorCode; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class GrpcExceptionHandlerImpl implements GrpcExceptionHandler { + + @Override + public StatusException handleException(Throwable t) { + if (t instanceof BizException e) { + ErrorCode errorCode = e.getErrorCode(); + log.warn("gRPC BizException: code={}, status={}, message={}", + errorCode.getCode(), errorCode.getStatus(), errorCode.getMessage()); + return toGrpcStatus(errorCode) + .withDescription(errorCode.getMessage()) + .asException(); + } + if (t instanceof StatusRuntimeException e) { + log.warn("gRPC StatusRuntimeException: status={}, description={}", + e.getStatus().getCode(), e.getStatus().getDescription()); + return e.getStatus().asException(); + } + log.error("gRPC Unhandled exception", t); + return Status.INTERNAL.withDescription("Internal server error").asException(); + } + + private Status toGrpcStatus(ErrorCode errorCode) { + return switch (errorCode.getStatus()) { + case 400 -> Status.INVALID_ARGUMENT; + case 401 -> Status.UNAUTHENTICATED; + case 403 -> Status.PERMISSION_DENIED; + case 404 -> Status.NOT_FOUND; + case 409 -> Status.ALREADY_EXISTS; + case 429 -> Status.RESOURCE_EXHAUSTED; + default -> Status.INTERNAL; + }; + } +} diff --git a/src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java b/src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java new file mode 100644 index 0000000..2baa39b --- /dev/null +++ b/src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java @@ -0,0 +1,55 @@ +package flipnote.user.interfaces.grpc; + +import flipnote.user.application.UserService; +import flipnote.user.domain.entity.User; +import flipnote.user.grpc.GetUserRequest; +import flipnote.user.grpc.GetUserResponse; +import flipnote.user.grpc.GetUsersRequest; +import flipnote.user.grpc.GetUsersResponse; +import flipnote.user.grpc.UserQueryServiceGrpc; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class GrpcUserQueryService extends UserQueryServiceGrpc.UserQueryServiceImplBase { + + private final UserService userService; + + @Override + public void getUser(GetUserRequest request, StreamObserver responseObserver) { + User user = userService.findActiveUserById(request.getUserId()) + .orElseThrow(() -> Status.NOT_FOUND + .withDescription("사용자를 찾을 수 없습니다.") + .asRuntimeException()); + + responseObserver.onNext(toResponse(user)); + responseObserver.onCompleted(); + } + + @Override + public void getUsers(GetUsersRequest request, StreamObserver responseObserver) { + List userIds = request.getUserIdsList(); + List users = userService.findActiveUsersByIds(userIds); + + GetUsersResponse response = GetUsersResponse.newBuilder() + .addAllUsers(users.stream().map(this::toResponse).toList()) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + private GetUserResponse toResponse(User user) { + return GetUserResponse.newBuilder() + .setId(user.getId()) + .setEmail(user.getEmail()) + .setNickname(user.getNickname()) + .setProfileImageUrl(user.getProfileImageUrl() != null ? user.getProfileImageUrl() : "") + .build(); + } +} diff --git a/src/main/java/flipnote/user/auth/presentation/AuthController.java b/src/main/java/flipnote/user/interfaces/http/AuthController.java similarity index 82% rename from src/main/java/flipnote/user/auth/presentation/AuthController.java rename to src/main/java/flipnote/user/interfaces/http/AuthController.java index 060f8d6..62ff975 100644 --- a/src/main/java/flipnote/user/auth/presentation/AuthController.java +++ b/src/main/java/flipnote/user/interfaces/http/AuthController.java @@ -1,21 +1,21 @@ -package flipnote.user.auth.presentation; - -import flipnote.user.auth.application.AuthService; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.auth.presentation.dto.request.ChangePasswordRequest; -import flipnote.user.auth.presentation.dto.request.EmailVerificationRequest; -import flipnote.user.auth.presentation.dto.request.EmailVerifyRequest; -import flipnote.user.auth.presentation.dto.request.LoginRequest; -import flipnote.user.auth.presentation.dto.request.PasswordResetCreateRequest; -import flipnote.user.auth.presentation.dto.request.PasswordResetRequest; -import flipnote.user.auth.presentation.dto.request.SignupRequest; -import flipnote.user.auth.presentation.dto.request.TokenValidateRequest; -import flipnote.user.auth.presentation.dto.response.SocialLinksResponse; -import flipnote.user.auth.presentation.dto.response.TokenValidateResponse; -import flipnote.user.auth.presentation.dto.response.UserResponse; -import flipnote.user.global.constants.HttpConstants; -import flipnote.user.global.util.CookieUtil; +package flipnote.user.interfaces; + +import flipnote.user.application.AuthService; +import flipnote.user.domain.TokenPair; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.interfaces.http.common.CookieUtil; +import flipnote.user.interfaces.http.common.HttpConstants; +import flipnote.user.interfaces.http.dto.request.ChangePasswordRequest; +import flipnote.user.interfaces.http.dto.request.EmailVerificationRequest; +import flipnote.user.interfaces.http.dto.request.EmailVerifyRequest; +import flipnote.user.interfaces.http.dto.request.LoginRequest; +import flipnote.user.interfaces.http.dto.request.PasswordResetCreateRequest; +import flipnote.user.interfaces.http.dto.request.PasswordResetRequest; +import flipnote.user.interfaces.http.dto.request.SignupRequest; +import flipnote.user.interfaces.http.dto.request.TokenValidateRequest; +import flipnote.user.interfaces.http.dto.response.SocialLinksResponse; +import flipnote.user.interfaces.http.dto.response.TokenValidateResponse; +import flipnote.user.interfaces.http.dto.response.UserResponse; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/flipnote/user/interfaces/http/OAuthController.java b/src/main/java/flipnote/user/interfaces/http/OAuthController.java new file mode 100644 index 0000000..ceb9758 --- /dev/null +++ b/src/main/java/flipnote/user/interfaces/http/OAuthController.java @@ -0,0 +1,101 @@ +package flipnote.user.interfaces; + +import flipnote.user.application.OAuthService; +import flipnote.user.domain.AuthErrorCode; +import flipnote.user.domain.TokenPair; +import flipnote.user.domain.common.BizException; +import flipnote.user.infrastructure.config.ClientProperties; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.interfaces.http.common.CookieUtil; +import flipnote.user.interfaces.http.common.HttpConstants; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class OAuthController { + + private final OAuthService oAuthService; + private final JwtProvider jwtProvider; + private final ClientProperties clientProperties; + + @GetMapping("/oauth2/authorization/{provider}") + public ResponseEntity redirectToProvider( + @PathVariable String provider, + @RequestHeader(value = HttpConstants.USER_ID_HEADER, required = false) Long userId, + HttpServletRequest request) { + OAuthService.AuthorizationRedirect redirect = oAuthService.getAuthorizationUri(provider, request, userId); + + return ResponseEntity.status(HttpStatus.FOUND) + .header(HttpHeaders.SET_COOKIE, redirect.verifierCookie().toString()) + .location(URI.create(redirect.authorizeUri())) + .build(); + } + + @GetMapping("/oauth2/callback/{provider}") + public ResponseEntity handleCallback( + @PathVariable String provider, + @RequestParam String code, + @RequestParam(required = false) String state, + @CookieValue(HttpConstants.OAUTH_VERIFIER_COOKIE) String codeVerifier, + HttpServletRequest request, + HttpServletResponse response) { + + CookieUtil.deleteCookie(response, HttpConstants.OAUTH_VERIFIER_COOKIE); + + boolean isSocialLinkRequest = StringUtils.hasText(state); + if (isSocialLinkRequest) { + return handleSocialLink(provider, code, state, codeVerifier, request); + } + return handleSocialLogin(provider, code, codeVerifier, request, response); + } + + private ResponseEntity handleSocialLogin(String provider, String code, String codeVerifier, + HttpServletRequest request, HttpServletResponse response) { + try { + TokenPair tokenPair = oAuthService.socialLogin(provider, code, codeVerifier, request); + CookieUtil.addCookie(response, HttpConstants.ACCESS_TOKEN_COOKIE, tokenPair.accessToken(), + jwtProvider.getAccessTokenExpiration() / 1000); + CookieUtil.addCookie(response, HttpConstants.REFRESH_TOKEN_COOKIE, tokenPair.refreshToken(), + jwtProvider.getRefreshTokenExpiration() / 1000); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLoginSuccess())) + .build(); + } catch (Exception e) { + log.warn("소셜 로그인 처리 실패. provider: {}", provider, e); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLoginFailure())) + .build(); + } + } + + private ResponseEntity handleSocialLink(String provider, String code, String state, + String codeVerifier, HttpServletRequest request) { + try { + oAuthService.linkSocialAccount(provider, code, state, codeVerifier, request); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLinkSuccess())) + .build(); + } catch (BizException e) { + log.warn("소셜 계정 연동 처리 실패. provider: {}", provider, e); + if (e.getErrorCode() == AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT) { + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLinkConflict())) + .build(); + } + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLinkFailure())) + .build(); + } + } +} diff --git a/src/main/java/flipnote/user/user/presentation/UserController.java b/src/main/java/flipnote/user/interfaces/http/UserController.java similarity index 76% rename from src/main/java/flipnote/user/user/presentation/UserController.java rename to src/main/java/flipnote/user/interfaces/http/UserController.java index fc53b14..4da91bf 100644 --- a/src/main/java/flipnote/user/user/presentation/UserController.java +++ b/src/main/java/flipnote/user/interfaces/http/UserController.java @@ -1,11 +1,11 @@ -package flipnote.user.user.presentation; +package flipnote.user.interfaces; -import flipnote.user.user.application.UserService; -import flipnote.user.user.presentation.dto.request.UpdateProfileRequest; -import flipnote.user.user.presentation.dto.response.MyInfoResponse; -import flipnote.user.user.presentation.dto.response.UserInfoResponse; -import flipnote.user.user.presentation.dto.response.UserUpdateResponse; -import flipnote.user.global.constants.HttpConstants; +import flipnote.user.application.UserService; +import flipnote.user.interfaces.http.common.HttpConstants; +import flipnote.user.interfaces.http.dto.request.UpdateProfileRequest; +import flipnote.user.interfaces.http.dto.response.MyInfoResponse; +import flipnote.user.interfaces.http.dto.response.UserInfoResponse; +import flipnote.user.interfaces.http.dto.response.UserUpdateResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/flipnote/user/global/error/ApiResponse.java b/src/main/java/flipnote/user/interfaces/http/common/ApiResponse.java similarity index 95% rename from src/main/java/flipnote/user/global/error/ApiResponse.java rename to src/main/java/flipnote/user/interfaces/http/common/ApiResponse.java index 39f2442..968d8db 100644 --- a/src/main/java/flipnote/user/global/error/ApiResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/common/ApiResponse.java @@ -1,5 +1,6 @@ -package flipnote.user.global.error; +package flipnote.user.interfaces.http.common; +import flipnote.user.domain.common.ErrorCode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/flipnote/user/global/error/ApiResponseAdvice.java b/src/main/java/flipnote/user/interfaces/http/common/ApiResponseAdvice.java similarity index 96% rename from src/main/java/flipnote/user/global/error/ApiResponseAdvice.java rename to src/main/java/flipnote/user/interfaces/http/common/ApiResponseAdvice.java index eddabf1..b2bdb3a 100644 --- a/src/main/java/flipnote/user/global/error/ApiResponseAdvice.java +++ b/src/main/java/flipnote/user/interfaces/http/common/ApiResponseAdvice.java @@ -1,4 +1,4 @@ -package flipnote.user.global.error; +package flipnote.user.interfaces.http.common; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; diff --git a/src/main/java/flipnote/user/global/util/CookieUtil.java b/src/main/java/flipnote/user/interfaces/http/common/CookieUtil.java similarity index 95% rename from src/main/java/flipnote/user/global/util/CookieUtil.java rename to src/main/java/flipnote/user/interfaces/http/common/CookieUtil.java index bfeda9b..6df476e 100644 --- a/src/main/java/flipnote/user/global/util/CookieUtil.java +++ b/src/main/java/flipnote/user/interfaces/http/common/CookieUtil.java @@ -1,4 +1,4 @@ -package flipnote.user.global.util; +package flipnote.user.interfaces.http.common; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseCookie; diff --git a/src/main/java/flipnote/user/global/error/GlobalExceptionHandler.java b/src/main/java/flipnote/user/interfaces/http/common/GlobalExceptionHandler.java similarity index 95% rename from src/main/java/flipnote/user/global/error/GlobalExceptionHandler.java rename to src/main/java/flipnote/user/interfaces/http/common/GlobalExceptionHandler.java index 5d4d142..e50545d 100644 --- a/src/main/java/flipnote/user/global/error/GlobalExceptionHandler.java +++ b/src/main/java/flipnote/user/interfaces/http/common/GlobalExceptionHandler.java @@ -1,6 +1,6 @@ -package flipnote.user.global.error; +package flipnote.user.interfaces.http.common; -import flipnote.user.global.exception.BizException; +import flipnote.user.domain.common.BizException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/flipnote/user/global/constants/HttpConstants.java b/src/main/java/flipnote/user/interfaces/http/common/HttpConstants.java similarity index 89% rename from src/main/java/flipnote/user/global/constants/HttpConstants.java rename to src/main/java/flipnote/user/interfaces/http/common/HttpConstants.java index dd85905..ef64b99 100644 --- a/src/main/java/flipnote/user/global/constants/HttpConstants.java +++ b/src/main/java/flipnote/user/interfaces/http/common/HttpConstants.java @@ -1,4 +1,4 @@ -package flipnote.user.global.constants; +package flipnote.user.interfaces.http.common; import lombok.NoArgsConstructor; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/ChangePasswordRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/ChangePasswordRequest.java similarity index 90% rename from src/main/java/flipnote/user/auth/presentation/dto/request/ChangePasswordRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/ChangePasswordRequest.java index 68fca89..1a9e6d3 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/ChangePasswordRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/ChangePasswordRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerificationRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerificationRequest.java similarity index 87% rename from src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerificationRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerificationRequest.java index 9914235..a16898a 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerificationRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerificationRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerifyRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerifyRequest.java similarity index 91% rename from src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerifyRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerifyRequest.java index add0490..0e2033d 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerifyRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerifyRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/LoginRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/LoginRequest.java similarity index 89% rename from src/main/java/flipnote/user/auth/presentation/dto/request/LoginRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/LoginRequest.java index e84a68e..fd4e8d1 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/LoginRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/LoginRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetCreateRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetCreateRequest.java similarity index 87% rename from src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetCreateRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetCreateRequest.java index 805ec9e..d166b03 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetCreateRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetCreateRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetRequest.java similarity index 90% rename from src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetRequest.java index 5bf5a9f..fd57bd8 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/SignupRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/SignupRequest.java similarity index 95% rename from src/main/java/flipnote/user/auth/presentation/dto/request/SignupRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/SignupRequest.java index a59af8e..8bea746 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/SignupRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/SignupRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/TokenValidateRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/TokenValidateRequest.java similarity index 82% rename from src/main/java/flipnote/user/auth/presentation/dto/request/TokenValidateRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/TokenValidateRequest.java index 45055ce..3586ecb 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/TokenValidateRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/TokenValidateRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.NotBlank; import lombok.Getter; diff --git a/src/main/java/flipnote/user/user/presentation/dto/request/UpdateProfileRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/UpdateProfileRequest.java similarity index 91% rename from src/main/java/flipnote/user/user/presentation/dto/request/UpdateProfileRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/UpdateProfileRequest.java index 2bcbff3..29f23f5 100644 --- a/src/main/java/flipnote/user/user/presentation/dto/request/UpdateProfileRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/UpdateProfileRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.user.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java b/src/main/java/flipnote/user/interfaces/http/dto/response/MyInfoResponse.java similarity index 91% rename from src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java rename to src/main/java/flipnote/user/interfaces/http/dto/response/MyInfoResponse.java index 0798946..9a99532 100644 --- a/src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/response/MyInfoResponse.java @@ -1,7 +1,7 @@ -package flipnote.user.user.presentation.dto.response; +package flipnote.user.interfaces.http.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; -import flipnote.user.user.domain.User; +import flipnote.user.domain.entity.User; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinkResponse.java b/src/main/java/flipnote/user/interfaces/http/dto/response/SocialLinkResponse.java similarity index 83% rename from src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinkResponse.java rename to src/main/java/flipnote/user/interfaces/http/dto/response/SocialLinkResponse.java index 1c7f250..bc68198 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinkResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/response/SocialLinkResponse.java @@ -1,7 +1,7 @@ -package flipnote.user.auth.presentation.dto.response; +package flipnote.user.interfaces.http.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; -import flipnote.user.user.domain.OAuthLink; +import flipnote.user.domain.entity.OAuthLink; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java b/src/main/java/flipnote/user/interfaces/http/dto/response/SocialLinksResponse.java similarity index 82% rename from src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java rename to src/main/java/flipnote/user/interfaces/http/dto/response/SocialLinksResponse.java index d5f3bbd..2297b90 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/response/SocialLinksResponse.java @@ -1,6 +1,6 @@ -package flipnote.user.auth.presentation.dto.response; +package flipnote.user.interfaces.http.dto.response; -import flipnote.user.user.domain.OAuthLink; +import flipnote.user.domain.entity.OAuthLink; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java b/src/main/java/flipnote/user/interfaces/http/dto/response/TokenValidateResponse.java similarity index 78% rename from src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java rename to src/main/java/flipnote/user/interfaces/http/dto/response/TokenValidateResponse.java index 6b799b5..933306f 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/response/TokenValidateResponse.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.response; +package flipnote.user.interfaces.http.dto.response; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java b/src/main/java/flipnote/user/interfaces/http/dto/response/UserInfoResponse.java similarity index 81% rename from src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java rename to src/main/java/flipnote/user/interfaces/http/dto/response/UserInfoResponse.java index fd08f81..d739b6f 100644 --- a/src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/response/UserInfoResponse.java @@ -1,6 +1,6 @@ -package flipnote.user.user.presentation.dto.response; +package flipnote.user.interfaces.http.dto.response; -import flipnote.user.user.domain.User; +import flipnote.user.domain.entity.User; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java b/src/main/java/flipnote/user/interfaces/http/dto/response/UserResponse.java similarity index 72% rename from src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java rename to src/main/java/flipnote/user/interfaces/http/dto/response/UserResponse.java index 66a9541..77b88ac 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/response/UserResponse.java @@ -1,6 +1,6 @@ -package flipnote.user.auth.presentation.dto.response; +package flipnote.user.interfaces.http.dto.response; -import flipnote.user.user.domain.User; +import flipnote.user.domain.entity.User; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java b/src/main/java/flipnote/user/interfaces/http/dto/response/UserUpdateResponse.java similarity index 86% rename from src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java rename to src/main/java/flipnote/user/interfaces/http/dto/response/UserUpdateResponse.java index 1880087..6296af8 100644 --- a/src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/response/UserUpdateResponse.java @@ -1,6 +1,6 @@ -package flipnote.user.user.presentation.dto.response; +package flipnote.user.interfaces.http.dto.response; -import flipnote.user.user.domain.User; +import flipnote.user.domain.entity.User; import lombok.AllArgsConstructor; import lombok.Getter; From 5e6c7d7088e1c3f46746bd67e663a110cb6b401f Mon Sep 17 00:00:00 2001 From: dungbik Date: Thu, 2 Apr 2026 14:45:07 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Chore:=20README=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4c8427 --- /dev/null +++ b/README.md @@ -0,0 +1,228 @@ +# 📒 FlipNote — User Service + +**FlipNote 서비스의 유저 도메인 백엔드 레포지토리입니다.** + +![Spring Boot](https://img.shields.io/badge/Spring_Boot-6DB33F?logo=springboot&logoColor=white) +![Java](https://img.shields.io/badge/Java_21-007396?logo=openjdk&logoColor=white) +![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white) +![Redis](https://img.shields.io/badge/Redis-FF4438?logo=redis&logoColor=white) +![Deploy](https://img.shields.io/badge/Deploy-GHCR%20%2B%20Docker-2496ED?logo=docker&logoColor=white) + +--- + +## 📑 목차 + +- [시작하기](#-시작하기) +- [환경 변수](#-환경-변수) +- [실행 및 배포](#-실행-및-배포) +- [프로젝트 구조](#-프로젝트-구조) + +--- + +## 🚀 시작하기 + +### 사전 요구사항 + +- **Java** 21 이상 +- **Gradle** 8 이상 +- **MySQL** 8 이상 +- **Redis** 6 이상 +- Google OAuth2 클라이언트 생성 및 API 키 발급 +- Resend 계정 생성 및 API 키 발급 + +### 설치 + +```bash +# 의존성 설치 및 빌드 +./gradlew build -x test +``` + +--- + +## 🔐 환경 변수 + +`application.yml`에서 참조하는 환경 변수 목록입니다. 로컬 실행 시 `.env` 또는 IDE 실행 구성에 아래 변수를 설정합니다. + +``` +# ─── Database ─────────────────────────────────────────── +DB_URL=jdbc:mysql://localhost:3306/flipnote_user +DB_USERNAME= +DB_PASSWORD= + +# ─── Redis ────────────────────────────────────────────── +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# ─── JPA ──────────────────────────────────────────────── +# create | create-drop | update | validate | none +DDL_AUTO=update + +# ─── gRPC ─────────────────────────────────────────────── +GRPC_PORT=9092 + +# ─── JWT ──────────────────────────────────────────────── +JWT_SECRET= +# 액세스 토큰 만료 시간 (ms), 기본값 900000 (15분) +JWT_ACCESS_EXPIRATION=900000 +# 리프레시 토큰 만료 시간 (ms), 기본값 604800000 (7일) +JWT_REFRESH_EXPIRATION=604800000 + +# ─── Email (Resend) ───────────────────────────────────── +APP_RESEND_API_KEY= + +# ─── Client ───────────────────────────────────────────── +# 프론트엔드 URL (CORS, 리다이렉트에 사용) +APP_CLIENT_URL=http://localhost:3000 + +# ─── Google OAuth2 ────────────────────────────────────── +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +``` + +> **⚠️ 주의**: 환경 변수 파일은 절대 git에 커밋하지 마세요. `.gitignore`에 포함되어 있는지 반드시 확인하세요. + +--- + +## 🖥️ 실행 및 배포 + +### 로컬 개발 서버 실행 + +```bash +./gradlew bootRun +``` + +기본적으로 `http://localhost:8081`에서 실행됩니다. +Swagger UI는 `http://localhost:8081/users/swagger-ui.html`에서 확인할 수 있습니다. + +### 프로덕션 빌드 + +```bash +./gradlew bootJar +``` + +`build/libs/user-0.0.1-SNAPSHOT.jar` 파일이 생성됩니다. + +### 테스트 실행 + +```bash +./gradlew test +``` + +### Docker 이미지 빌드 및 실행 + +```bash +# 이미지 빌드 +docker build -t flipnote-user . + +# 컨테이너 실행 +docker run -p 8081:8080 \ + -e DB_URL=... \ + -e JWT_SECRET=... \ + flipnote-user +``` + +### 배포 (GitHub Actions) + +`main` 브랜치에 push 시 GitHub Actions가 자동으로 아래 과정을 실행합니다. + +**CI** (`push` / `pull_request` → `main`) +1. JDK 21 설치 +2. `./gradlew build -x test` — 빌드 검증 +3. `./gradlew test` — 테스트 실행 +4. Dependency-Check — 취약점 분석 리포트 생성 + +**CD** (`push` → `main`) +1. GitHub Container Registry(GHCR) 로그인 +2. Docker 이미지 빌드 +3. `ghcr.io/dungbik/flipnote-user` 이미지 Push + +> 배포에 필요한 시크릿(`ORG_PAT`)은 GitHub Repository → Settings → Secrets and variables → Actions에 등록해야 합니다. + +--- + +## 📁 프로젝트 구조 + +- 간략화 버전 + + ``` + src/main/java/flipnote/user/ + ├── domain/ # 도메인 레이어 (엔티티, 레포지토리, 에러코드, 이벤트) + ├── application/ # 애플리케이션 레이어 (서비스) + ├── infrastructure/ # 인프라 레이어 (JWT, Redis, 메일, OAuth, 설정) + └── interfaces/ # 인터페이스 레이어 (HTTP, gRPC 진입점) + ``` + +``` +FlipNote-User/ +├── src/ +│ ├── main/ +│ │ ├── java/flipnote/user/ +│ │ │ ├── UserApplication.java +│ │ │ │ +│ │ │ ├── domain/ # 도메인 레이어 +│ │ │ │ ├── common/ # 도메인 공통 +│ │ │ │ │ ├── ErrorCode.java +│ │ │ │ │ ├── BizException.java +│ │ │ │ │ └── EmailSendException.java +│ │ │ │ ├── entity/ # JPA 엔티티 +│ │ │ │ │ ├── User.java +│ │ │ │ │ ├── OAuthLink.java +│ │ │ │ │ └── BaseEntity.java +│ │ │ │ ├── repository/ # 레포지토리 인터페이스 +│ │ │ │ │ ├── UserRepository.java +│ │ │ │ │ └── OAuthLinkRepository.java +│ │ │ │ ├── event/ # 도메인 이벤트 +│ │ │ │ │ ├── EmailVerificationSendEvent.java +│ │ │ │ │ └── PasswordResetCreateEvent.java +│ │ │ │ ├── AuthErrorCode.java +│ │ │ │ ├── UserErrorCode.java +│ │ │ │ ├── ImageErrorCode.java +│ │ │ │ ├── TokenClaims.java +│ │ │ │ ├── TokenPair.java +│ │ │ │ ├── PasswordResetConstants.java +│ │ │ │ └── VerificationConstants.java +│ │ │ │ +│ │ │ ├── application/ # 애플리케이션 레이어 +│ │ │ │ ├── AuthService.java +│ │ │ │ ├── OAuthService.java +│ │ │ │ └── UserService.java +│ │ │ │ +│ │ │ ├── infrastructure/ # 인프라 레이어 +│ │ │ │ ├── config/ # 범용 설정 (App, JPA, Swagger, gRPC 클라이언트) +│ │ │ │ ├── jwt/ # JWT 발급/검증 + 설정 +│ │ │ │ ├── mail/ # 메일 발송 서비스 + 설정 +│ │ │ │ ├── oauth/ # Google OAuth2 클라이언트 + 설정 +│ │ │ │ ├── redis/ # Redis 저장소 (토큰, 인증코드 등) +│ │ │ │ └── listener/ # 도메인 이벤트 리스너 +│ │ │ │ +│ │ │ └── interfaces/ # 인터페이스 레이어 +│ │ │ ├── http/ # HTTP 진입점 +│ │ │ │ ├── AuthController.java # 인증 (회원가입, 로그인, 비밀번호 등) +│ │ │ │ ├── OAuthController.java # 소셜 로그인 (Google OAuth2) +│ │ │ │ ├── UserController.java # 유저 정보 조회/수정 +│ │ │ │ ├── dto/ +│ │ │ │ │ ├── request/ # Request DTO +│ │ │ │ │ └── response/ # Response DTO +│ │ │ │ └── common/ # ApiResponse, 예외 처리, 쿠키 유틸 +│ │ │ └── grpc/ # gRPC 진입점 +│ │ │ ├── GrpcUserQueryService.java # 유저 조회 gRPC 서비스 +│ │ │ └── GrpcExceptionHandlerImpl.java # gRPC 전역 예외 처리 +│ │ │ +│ │ ├── proto/ # gRPC proto 파일 +│ │ │ ├── user_query.proto +│ │ │ └── image.proto +│ │ │ +│ │ └── resources/ +│ │ ├── application.yml +│ │ └── templates/email/ # 이메일 HTML 템플릿 (Thymeleaf) +│ │ ├── email-verification.html +│ │ └── password-reset.html +│ │ +│ └── test/ +│ └── java/flipnote/user/ +│ +├── Dockerfile +├── build.gradle.kts +└── settings.gradle.kts +``` \ No newline at end of file From 187ae6a7aa6f028a277acdd7d5210b96dfd090f6 Mon Sep 17 00:00:00 2001 From: dungbik Date: Thu, 2 Apr 2026 14:47:30 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/auth/application/OAuthService.java | 127 ---------------- .../infrastructure/oauth/OAuthApiClient.java | 96 ------------- .../auth/presentation/OAuthController.java | 98 ------------- .../user/global/config/OAuthProperties.java | 30 ---- .../grpc/GrpcUserQueryService.java | 135 ------------------ 5 files changed, 486 deletions(-) delete mode 100644 src/main/java/flipnote/user/auth/application/OAuthService.java delete mode 100644 src/main/java/flipnote/user/auth/infrastructure/oauth/OAuthApiClient.java delete mode 100644 src/main/java/flipnote/user/auth/presentation/OAuthController.java delete mode 100644 src/main/java/flipnote/user/global/config/OAuthProperties.java delete mode 100644 src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java diff --git a/src/main/java/flipnote/user/auth/application/OAuthService.java b/src/main/java/flipnote/user/auth/application/OAuthService.java deleted file mode 100644 index fb5eb6c..0000000 --- a/src/main/java/flipnote/user/auth/application/OAuthService.java +++ /dev/null @@ -1,127 +0,0 @@ -package flipnote.user.auth.application; - -import flipnote.user.auth.domain.AuthErrorCode; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.auth.infrastructure.oauth.OAuthApiClient; -import flipnote.user.auth.infrastructure.oauth.OAuth2UserInfo; -import flipnote.user.auth.infrastructure.oauth.PkceUtil; -import flipnote.user.auth.infrastructure.redis.SocialLinkTokenRepository; -import flipnote.user.global.config.OAuthProperties; -import flipnote.user.global.constants.HttpConstants; -import flipnote.user.global.exception.BizException; -import flipnote.user.user.domain.OAuthLink; -import flipnote.user.user.domain.OAuthLinkRepository; -import flipnote.user.user.domain.User; -import flipnote.user.user.domain.UserErrorCode; -import flipnote.user.user.domain.UserRepository; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseCookie; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Map; -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class OAuthService { - - private final PkceUtil pkceUtil; - private final OAuthApiClient oAuthApiClient; - private final OAuthLinkRepository oAuthLinkRepository; - private final UserRepository userRepository; - private final SocialLinkTokenRepository socialLinkTokenRepository; - private final JwtProvider jwtProvider; - private final OAuthProperties oAuthProperties; - - public record AuthorizationRedirect(String authorizeUri, ResponseCookie verifierCookie) {} - - private static final int VERIFIER_COOKIE_MAX_AGE = 180; - - public AuthorizationRedirect getAuthorizationUri(String providerName, Long userId) { - OAuthProperties.Provider provider = resolveProvider(providerName); - - String codeVerifier = pkceUtil.generateCodeVerifier(); - String codeChallenge = pkceUtil.generateCodeChallenge(codeVerifier); - - String state = null; - if (userId != null) { - state = UUID.randomUUID().toString(); - socialLinkTokenRepository.save(userId, state); - } - - String authorizeUri = oAuthApiClient.buildAuthorizeUri(provider, codeChallenge, state); - - ResponseCookie verifierCookie = ResponseCookie.from(HttpConstants.OAUTH_VERIFIER_COOKIE, codeVerifier) - .httpOnly(true) - .secure(true) - .path("/") - .maxAge(VERIFIER_COOKIE_MAX_AGE) - .sameSite("Lax") - .build(); - - return new AuthorizationRedirect(authorizeUri, verifierCookie); - } - - public TokenPair socialLogin(String providerName, String code, String codeVerifier) { - OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier); - - OAuthLink oAuthLink = oAuthLinkRepository - .findByProviderAndProviderIdWithUser(userInfo.getProvider(), userInfo.getProviderId()) - .orElseThrow(() -> new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT)); - - return jwtProvider.generateTokenPair(oAuthLink.getUser()); - } - - @Transactional - public void linkSocialAccount(String providerName, String code, String state, - String codeVerifier) { - Long userId = socialLinkTokenRepository.findUserIdByState(state) - .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_SOCIAL_LINK_TOKEN)); - - socialLinkTokenRepository.delete(state); - - OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier); - - if (oAuthLinkRepository.existsByUser_IdAndProviderAndProviderId( - userId, userInfo.getProvider(), userInfo.getProviderId())) { - throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT); - } - - User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) - .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); - - OAuthLink link = OAuthLink.builder() - .provider(userInfo.getProvider()) - .providerId(userInfo.getProviderId()) - .user(user) - .build(); - oAuthLinkRepository.save(link); - } - - private OAuth2UserInfo getOAuth2UserInfo(String providerName, String code, - String codeVerifier) { - OAuthProperties.Provider provider = resolveProvider(providerName); - String accessToken = oAuthApiClient.requestAccessToken(provider, code, codeVerifier); - Map attributes = oAuthApiClient.requestUserInfo(provider, accessToken); - return oAuthApiClient.createUserInfo(providerName, attributes); - } - - private OAuthProperties.Provider resolveProvider(String providerName) { - Map providers = oAuthProperties.getProviders(); - if (providers == null) { - throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); - } - OAuthProperties.Provider provider = providers.get(providerName.toLowerCase()); - if (provider == null) { - log.warn("지원하지 않는 OAuth Provider: {}", providerName); - throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); - } - return provider; - } -} diff --git a/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuthApiClient.java b/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuthApiClient.java deleted file mode 100644 index 4417b43..0000000 --- a/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuthApiClient.java +++ /dev/null @@ -1,96 +0,0 @@ -package flipnote.user.auth.infrastructure.oauth; - -import java.util.Map; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Service; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestClient; -import org.springframework.web.util.UriComponentsBuilder; - -import flipnote.user.global.config.OAuthProperties; - -import lombok.RequiredArgsConstructor; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.ObjectMapper; - -@Service -@RequiredArgsConstructor -public class OAuthApiClient { - - private final RestClient restClient; - private final ObjectMapper objectMapper; - private final OAuthProperties oAuthProperties; - - public String requestAccessToken(OAuthProperties.Provider provider, String code, - String codeVerifier) { - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "authorization_code"); - params.add("client_id", provider.getClientId()); - params.add("client_secret", provider.getClientSecret()); - params.add("redirect_uri", buildRedirectUri(provider.getRedirectUri())); - params.add("code", code); - params.add("code_verifier", codeVerifier); - - try { - String responseBody = restClient.post() - .uri(provider.getTokenUri()) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(params) - .retrieve() - .body(String.class); - - Map responseMap = objectMapper.readValue(responseBody, new TypeReference<>() {}); - return (String) responseMap.get("access_token"); - } catch (Exception e) { - throw new RuntimeException("Failed to get OAuth access token", e); - } - } - - public Map requestUserInfo(OAuthProperties.Provider provider, String accessToken) { - try { - String responseBody = restClient.get() - .uri(provider.getUserInfoUri()) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .retrieve() - .body(String.class); - - return objectMapper.readValue(responseBody, new TypeReference<>() {}); - } catch (Exception e) { - throw new RuntimeException("Failed to get OAuth user info", e); - } - } - - public OAuth2UserInfo createUserInfo(String providerName, Map attributes) { - return switch (providerName.toLowerCase()) { - case "google" -> new GoogleUserInfo(attributes); - default -> throw new IllegalArgumentException("Unsupported OAuth provider: " + providerName); - }; - } - - public String buildAuthorizeUri(OAuthProperties.Provider provider, - String codeChallenge, String state) { - UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(provider.getAuthorizationUri()) - .queryParam("client_id", provider.getClientId()) - .queryParam("redirect_uri", buildRedirectUri(provider.getRedirectUri())) - .queryParam("response_type", "code") - .queryParam("scope", String.join(" ", provider.getScope())) - .queryParam("code_challenge", codeChallenge) - .queryParam("code_challenge_method", "S256"); - - if (state != null) { - builder.queryParam("state", state); - } - - return builder.toUriString(); - } - - private String buildRedirectUri(String path) { - return UriComponentsBuilder.fromUriString(oAuthProperties.getBaseUrl()) - .path(path) - .build() - .toUriString(); - } -} diff --git a/src/main/java/flipnote/user/auth/presentation/OAuthController.java b/src/main/java/flipnote/user/auth/presentation/OAuthController.java deleted file mode 100644 index e34b016..0000000 --- a/src/main/java/flipnote/user/auth/presentation/OAuthController.java +++ /dev/null @@ -1,98 +0,0 @@ -package flipnote.user.auth.presentation; - -import flipnote.user.auth.application.OAuthService; -import flipnote.user.auth.domain.AuthErrorCode; -import flipnote.user.global.exception.BizException; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.global.config.ClientProperties; -import flipnote.user.global.constants.HttpConstants; -import flipnote.user.global.util.CookieUtil; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; - -import java.net.URI; - -@Slf4j -@RestController -@RequiredArgsConstructor -public class OAuthController { - - private final OAuthService oAuthService; - private final JwtProvider jwtProvider; - private final ClientProperties clientProperties; - - @GetMapping("/oauth2/authorization/{provider}") - public ResponseEntity redirectToProvider( - @PathVariable String provider, - @RequestHeader(value = HttpConstants.USER_ID_HEADER, required = false) Long userId) { - OAuthService.AuthorizationRedirect redirect = oAuthService.getAuthorizationUri(provider, userId); - - return ResponseEntity.status(HttpStatus.FOUND) - .header(HttpHeaders.SET_COOKIE, redirect.verifierCookie().toString()) - .location(URI.create(redirect.authorizeUri())) - .build(); - } - - @GetMapping("/oauth2/callback/{provider}") - public ResponseEntity handleCallback( - @PathVariable String provider, - @RequestParam String code, - @RequestParam(required = false) String state, - @CookieValue(HttpConstants.OAUTH_VERIFIER_COOKIE) String codeVerifier, - HttpServletResponse response) { - - CookieUtil.deleteCookie(response, HttpConstants.OAUTH_VERIFIER_COOKIE); - - boolean isSocialLinkRequest = StringUtils.hasText(state); - if (isSocialLinkRequest) { - return handleSocialLink(provider, code, state, codeVerifier); - } - return handleSocialLogin(provider, code, codeVerifier, response); - } - - private ResponseEntity handleSocialLogin(String provider, String code, String codeVerifier, - HttpServletResponse response) { - try { - TokenPair tokenPair = oAuthService.socialLogin(provider, code, codeVerifier); - CookieUtil.addCookie(response, HttpConstants.ACCESS_TOKEN_COOKIE, tokenPair.accessToken(), - jwtProvider.getAccessTokenExpiration() / 1000); - CookieUtil.addCookie(response, HttpConstants.REFRESH_TOKEN_COOKIE, tokenPair.refreshToken(), - jwtProvider.getRefreshTokenExpiration() / 1000); - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLoginSuccess())) - .build(); - } catch (Exception e) { - log.warn("소셜 로그인 처리 실패. provider: {}", provider, e); - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLoginFailure())) - .build(); - } - } - - private ResponseEntity handleSocialLink(String provider, String code, String state, - String codeVerifier) { - try { - oAuthService.linkSocialAccount(provider, code, state, codeVerifier); - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLinkSuccess())) - .build(); - } catch (BizException e) { - log.warn("소셜 계정 연동 처리 실패. provider: {}", provider, e); - if (e.getErrorCode() == AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT) { - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLinkConflict())) - .build(); - } - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create(clientProperties.getUrl() + clientProperties.getPaths().getSocialLinkFailure())) - .build(); - } - } -} diff --git a/src/main/java/flipnote/user/global/config/OAuthProperties.java b/src/main/java/flipnote/user/global/config/OAuthProperties.java deleted file mode 100644 index 8024ce6..0000000 --- a/src/main/java/flipnote/user/global/config/OAuthProperties.java +++ /dev/null @@ -1,30 +0,0 @@ -package flipnote.user.global.config; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.List; -import java.util.Map; - -@Getter -@RequiredArgsConstructor -@ConfigurationProperties(prefix = "app.oauth2") -public class OAuthProperties { - - private final String baseUrl; - private final Map providers; - - @Getter - @Setter - public static class Provider { - private String clientId; - private String clientSecret; - private String redirectUri; - private String authorizationUri; - private String tokenUri; - private String userInfoUri; - private List scope; - } -} diff --git a/src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java b/src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java deleted file mode 100644 index e38d3e6..0000000 --- a/src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java +++ /dev/null @@ -1,135 +0,0 @@ -package flipnote.user.user.presentation.grpc; - -import flipnote.user.auth.domain.TokenClaims; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.user.domain.User; -import flipnote.user.user.domain.UserRepository; -import flipnote.user.grpc.GetUserByEmailRequest; -import flipnote.user.grpc.GetUserByEmailResponse; -import flipnote.user.grpc.GetUserByTokenRequest; -import flipnote.user.grpc.GetUserByTokenResponse; -import flipnote.user.grpc.GetUserRequest; -import flipnote.user.grpc.GetUserResponse; -import flipnote.user.grpc.GetUsersRequest; -import flipnote.user.grpc.GetUsersResponse; -import flipnote.user.grpc.UserQueryServiceGrpc; -import io.grpc.Status; -import io.grpc.stub.StreamObserver; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class GrpcUserQueryService extends UserQueryServiceGrpc.UserQueryServiceImplBase { - - private final UserRepository userRepository; - private final JwtProvider jwtProvider; - - @Override - public void getUser(GetUserRequest request, StreamObserver responseObserver) { - try { - User user = userRepository.findByIdAndStatus(request.getUserId(), User.Status.ACTIVE) - .orElse(null); - - if (user == null) { - responseObserver.onError( - Status.NOT_FOUND.withDescription("사용자를 찾을 수 없습니다.").asRuntimeException() - ); - return; - } - - responseObserver.onNext(toResponse(user)); - responseObserver.onCompleted(); - } catch (Exception e) { - log.error("gRPC getUser error. userId: {}", request.getUserId(), e); - responseObserver.onError(Status.INTERNAL.withDescription("Internal error").asRuntimeException()); - } - } - - @Override - public void getUsers(GetUsersRequest request, StreamObserver responseObserver) { - try { - List userIds = request.getUserIdsList(); - List users = userRepository.findByIdInAndStatus(userIds, User.Status.ACTIVE); - - GetUsersResponse response = GetUsersResponse.newBuilder() - .addAllUsers(users.stream().map(this::toResponse).toList()) - .build(); - - responseObserver.onNext(response); - responseObserver.onCompleted(); - } catch (Exception e) { - log.error("gRPC getUsers error. userIds: {}", request.getUserIdsList(), e); - responseObserver.onError(Status.INTERNAL.withDescription("Internal error").asRuntimeException()); - } - } - - @Override - public void getUserByEmail(GetUserByEmailRequest request, StreamObserver responseObserver) { - try { - User user = userRepository.findByEmailAndStatus(request.getEmail(), User.Status.ACTIVE) - .orElse(null); - - GetUserByEmailResponse.Builder responseBuilder = GetUserByEmailResponse.newBuilder(); - - if (user != null) { - responseBuilder.setExists(true).setUser(toResponse(user)); - } else { - responseBuilder.setExists(false); - } - - responseObserver.onNext(responseBuilder.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - log.error("gRPC getUserByEmail error. email: {}", request.getEmail(), e); - responseObserver.onError(Status.INTERNAL.withDescription("Internal error").asRuntimeException()); - } - } - - @Override - public void getUserByToken(GetUserByTokenRequest request, StreamObserver responseObserver) { - try { - if (!jwtProvider.isTokenValid(request.getAccessToken())) { - responseObserver.onError( - Status.UNAUTHENTICATED.withDescription("유효하지 않은 토큰입니다.").asRuntimeException() - ); - return; - } - - TokenClaims claims = jwtProvider.extractClaims(request.getAccessToken()); - User user = userRepository.findByIdAndStatus(claims.userId(), User.Status.ACTIVE) - .orElse(null); - - if (user == null) { - responseObserver.onError( - Status.NOT_FOUND.withDescription("사용자를 찾을 수 없습니다.").asRuntimeException() - ); - return; - } - - GetUserByTokenResponse response = GetUserByTokenResponse.newBuilder() - .setUserId(user.getId()) - .setNickname(user.getNickname()) - .build(); - - responseObserver.onNext(response); - responseObserver.onCompleted(); - } catch (Exception e) { - log.error("gRPC getUserByToken error", e); - responseObserver.onError(Status.INTERNAL.withDescription("Internal error").asRuntimeException()); - } - } - - private GetUserResponse toResponse(User user) { - return GetUserResponse.newBuilder() - .setId(user.getId()) - .setEmail(user.getEmail()) - .setNickname(user.getNickname()) - .setProfileImageUrl(user.getProfileImageUrl() != null ? user.getProfileImageUrl() : "") - .build(); - } -}