diff --git a/Dockerfile b/Dockerfile
index 9660585..a972f50 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,6 +18,6 @@ RUN apt-get update \
COPY --from=build /app/build/libs/user-0.0.1-SNAPSHOT.jar .
-EXPOSE 8080
+EXPOSE 8081
ENTRYPOINT ["java", "-jar", "user-0.0.1-SNAPSHOT.jar"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..61dd6ea
--- /dev/null
+++ b/README.md
@@ -0,0 +1,236 @@
+# π FlipNote β User Service
+
+**FlipNote μλΉμ€μ μ μ λλ©μΈ λ°±μλ λ ν¬μ§ν 리μ
λλ€.**
+
+
+
+
+
+
+
+---
+
+## π λͺ©μ°¨
+
+- [μμνκΈ°](#μμνκΈ°)
+- [νκ²½ λ³μ](#νκ²½-λ³μ)
+- [μ€ν λ° λ°°ν¬](#μ€ν-λ°-λ°°ν¬)
+- [νλ‘μ νΈ κ΅¬μ‘°](#νλ‘μ νΈ-ꡬ쑰)
+
+---
+
+
+
+## π μμνκΈ°
+
+### μ¬μ μꡬμ¬ν
+
+- **Java** 21 μ΄μ
+- **Gradle** 8 μ΄μ
+- **MySQL** 8 μ΄μ
+- **Redis** 6 μ΄μ
+- Google OAuth2 ν΄λΌμ΄μΈνΈ μμ± λ° API ν€ λ°κΈ
+- Resend κ³μ μμ± λ° API ν€ λ°κΈ
+
+### μ€μΉ
+
+```bash
+# μμ‘΄μ± μ€μΉ λ° λΉλ
+./gradlew build -x test
+```
+
+---
+
+
+
+## π νκ²½ λ³μ
+
+`application.yml`μμ μ°Έμ‘°νλ νκ²½ λ³μ λͺ©λ‘μ
λλ€. λ‘컬 μ€ν μ `.env` λλ IDE μ€ν ꡬμ±μ μλ λ³μλ₯Ό μ€μ ν©λλ€.
+
+```text
+# βββ 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:8081 \
+ -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μ λ±λ‘ν΄μΌ ν©λλ€.
+
+---
+
+
+
+## π νλ‘μ νΈ κ΅¬μ‘°
+
+- κ°λ΅ν λ²μ
+
+ ```text
+ src/main/java/flipnote/user/
+ βββ domain/ # λλ©μΈ λ μ΄μ΄ (μν°ν°, λ ν¬μ§ν 리, μλ¬μ½λ, μ΄λ²€νΈ)
+ βββ application/ # μ ν리μΌμ΄μ
λ μ΄μ΄ (μλΉμ€)
+ βββ infrastructure/ # μΈνλΌ λ μ΄μ΄ (JWT, Redis, λ©μΌ, OAuth, μ€μ )
+ βββ interfaces/ # μΈν°νμ΄μ€ λ μ΄μ΄ (HTTP, gRPC μ§μ
μ )
+ ```
+
+```text
+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
diff --git a/build.gradle.kts b/build.gradle.kts
index aa6c6c5..b1caa57 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -23,13 +23,6 @@ repositories {
extra["springGrpcVersion"] = "1.0.2"
-
-dependencyManagement {
- imports {
- mavenBom("org.springframework.grpc:spring-grpc-dependencies:1.0.2")
- }
-}
-
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
diff --git a/src/main/java/flipnote/user/application/AuthService.java b/src/main/java/flipnote/user/application/AuthService.java
new file mode 100644
index 0000000..acfdd4e
--- /dev/null
+++ b/src/main/java/flipnote/user/application/AuthService.java
@@ -0,0 +1,232 @@
+package flipnote.user.application;
+
+import java.util.List;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import flipnote.user.application.command.ChangePasswordCommand;
+import flipnote.user.application.command.LoginCommand;
+import flipnote.user.application.command.SignupCommand;
+import flipnote.user.application.result.SocialLinksResult;
+import flipnote.user.application.result.TokenValidateResult;
+import flipnote.user.application.result.UserRegisterResult;
+import flipnote.user.domain.AuthErrorCode;
+import flipnote.user.domain.TokenClaims;
+import flipnote.user.domain.TokenPair;
+import flipnote.user.domain.UserErrorCode;
+import flipnote.user.domain.common.BizException;
+import flipnote.user.domain.entity.OAuthLink;
+import flipnote.user.domain.entity.User;
+import flipnote.user.domain.event.EmailVerificationSendEvent;
+import flipnote.user.domain.event.PasswordResetCreateEvent;
+import flipnote.user.domain.repository.OAuthLinkRepository;
+import flipnote.user.domain.repository.UserRepository;
+import flipnote.user.infrastructure.config.ClientProperties;
+import flipnote.user.infrastructure.jwt.JwtProvider;
+import flipnote.user.infrastructure.mail.PasswordResetTokenGenerator;
+import flipnote.user.infrastructure.mail.VerificationCodeGenerator;
+import flipnote.user.infrastructure.redis.EmailVerificationRepository;
+import flipnote.user.infrastructure.redis.PasswordResetRepository;
+import flipnote.user.infrastructure.redis.SessionInvalidationRepository;
+import flipnote.user.infrastructure.redis.TokenBlacklistRepository;
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class AuthService {
+
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final JwtProvider jwtProvider;
+ private final TokenBlacklistRepository tokenBlacklistRepository;
+ private final EmailVerificationRepository emailVerificationRepository;
+ private final PasswordResetRepository passwordResetRepository;
+ private final OAuthLinkRepository oAuthLinkRepository;
+ private final SessionInvalidationRepository sessionInvalidationRepository;
+ private final VerificationCodeGenerator verificationCodeGenerator;
+ private final PasswordResetTokenGenerator passwordResetTokenGenerator;
+ private final ClientProperties clientProperties;
+ private final ApplicationEventPublisher eventPublisher;
+
+ @Transactional
+ public UserRegisterResult register(SignupCommand command) {
+ if (!emailVerificationRepository.isVerified(command.email())) {
+ throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL);
+ }
+
+ if (userRepository.existsByEmail(command.email())) {
+ throw new BizException(AuthErrorCode.EMAIL_ALREADY_EXISTS);
+ }
+
+ User user = User.builder()
+ .email(command.email())
+ .password(passwordEncoder.encode(command.password()))
+ .name(command.name())
+ .nickname(command.nickname())
+ .phone(command.phone())
+ .smsAgree(command.smsAgree())
+ .build();
+
+ User savedUser = userRepository.save(user);
+ return UserRegisterResult.from(savedUser);
+ }
+
+ public TokenPair login(LoginCommand command) {
+ User user = userRepository.findByEmailAndStatus(command.email(), User.Status.ACTIVE)
+ .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS));
+
+ if (!passwordEncoder.matches(command.password(), user.getPassword())) {
+ throw new BizException(AuthErrorCode.INVALID_CREDENTIALS);
+ }
+
+ return jwtProvider.generateTokenPair(user);
+ }
+
+ public void logout(String refreshToken) {
+ if (refreshToken != null && jwtProvider.isTokenValid(refreshToken)) {
+ long remaining = jwtProvider.getRemainingExpiration(refreshToken);
+ if (remaining > 0) {
+ tokenBlacklistRepository.add(refreshToken, remaining);
+ }
+ }
+ }
+
+ public TokenPair refreshToken(String refreshToken) {
+ if (refreshToken == null || !jwtProvider.isTokenValid(refreshToken)) {
+ throw new BizException(AuthErrorCode.INVALID_TOKEN);
+ }
+
+ if (tokenBlacklistRepository.isBlacklisted(refreshToken)) {
+ throw new BizException(AuthErrorCode.BLACKLISTED_TOKEN);
+ }
+
+ TokenClaims claims = jwtProvider.extractClaims(refreshToken);
+
+ sessionInvalidationRepository.getInvalidatedAtMillis(claims.userId()).ifPresent(invalidatedAtMillis -> {
+ if (jwtProvider.getIssuedAt(refreshToken).getTime() < invalidatedAtMillis) {
+ throw new BizException(AuthErrorCode.INVALIDATED_SESSION);
+ }
+ });
+
+ User user = findActiveUser(claims.userId());
+
+ long remaining = jwtProvider.getRemainingExpiration(refreshToken);
+ if (remaining > 0) {
+ tokenBlacklistRepository.add(refreshToken, remaining);
+ }
+
+ return jwtProvider.generateTokenPair(user);
+ }
+
+ @Transactional
+ public void changePassword(Long userId, ChangePasswordCommand command) {
+ User user = findActiveUser(userId);
+
+ if (!passwordEncoder.matches(command.currentPassword(), user.getPassword())) {
+ throw new BizException(AuthErrorCode.PASSWORD_MISMATCH);
+ }
+
+ user.changePassword(passwordEncoder.encode(command.newPassword()));
+ sessionInvalidationRepository.invalidate(user.getId(), jwtProvider.getRefreshTokenExpiration());
+ }
+
+ public TokenValidateResult validateToken(String token) {
+ if (!jwtProvider.isTokenValid(token)) {
+ throw new BizException(AuthErrorCode.INVALID_TOKEN);
+ }
+
+ if (tokenBlacklistRepository.isBlacklisted(token)) {
+ throw new BizException(AuthErrorCode.BLACKLISTED_TOKEN);
+ }
+
+ TokenClaims claims = jwtProvider.extractClaims(token);
+
+ sessionInvalidationRepository.getInvalidatedAtMillis(claims.userId()).ifPresent(invalidatedAtMillis -> {
+ if (jwtProvider.getIssuedAt(token).getTime() < invalidatedAtMillis) {
+ throw new BizException(AuthErrorCode.INVALIDATED_SESSION);
+ }
+ });
+
+ findActiveUser(claims.userId());
+
+ return new TokenValidateResult(claims.userId(), claims.email(), claims.role());
+ }
+
+ public void sendEmailVerificationCode(String email) {
+ if (emailVerificationRepository.hasCode(email)) {
+ throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE);
+ }
+
+ String code = verificationCodeGenerator.generate();
+ emailVerificationRepository.saveCode(email, code);
+ eventPublisher.publishEvent(new EmailVerificationSendEvent(email, code));
+ }
+
+ public void verifyEmail(String email, String code) {
+ if (!emailVerificationRepository.hasCode(email)) {
+ throw new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE);
+ }
+
+ String savedCode = emailVerificationRepository.getCode(email);
+ if (!code.equals(savedCode)) {
+ throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE);
+ }
+
+ emailVerificationRepository.deleteCode(email);
+ emailVerificationRepository.markVerified(email);
+ }
+
+ public void requestPasswordReset(String email) {
+ if (!userRepository.existsByEmail(email)) {
+ return;
+ }
+
+ if (passwordResetRepository.hasToken(email)) {
+ throw new BizException(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK);
+ }
+
+ String token = passwordResetTokenGenerator.generate();
+ passwordResetRepository.save(token, email);
+
+ String link = clientProperties.getUrl() + clientProperties.getPaths().getPasswordReset()
+ + "?token=" + token;
+ eventPublisher.publishEvent(new PasswordResetCreateEvent(email, link));
+ }
+
+ @Transactional
+ public void resetPassword(String token, String newPassword) {
+ String email = passwordResetRepository.findEmailByToken(token);
+ if (email == null) {
+ throw new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN);
+ }
+
+ User user = userRepository.findByEmailAndStatus(email, User.Status.ACTIVE)
+ .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND));
+
+ user.changePassword(passwordEncoder.encode(newPassword));
+ sessionInvalidationRepository.invalidate(user.getId(), jwtProvider.getRefreshTokenExpiration());
+ passwordResetRepository.delete(token, email);
+ }
+
+ public SocialLinksResult getSocialLinks(Long userId) {
+ List links = oAuthLinkRepository.findByUser_Id(userId);
+ return SocialLinksResult.from(links);
+ }
+
+ @Transactional
+ public void deleteSocialLink(Long userId, Long socialLinkId) {
+ if (!oAuthLinkRepository.existsByIdAndUser_Id(socialLinkId, userId)) {
+ throw new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT);
+ }
+ oAuthLinkRepository.deleteById(socialLinkId);
+ }
+
+ 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/application/OAuthService.java b/src/main/java/flipnote/user/application/OAuthService.java
new file mode 100644
index 0000000..b6aabbd
--- /dev/null
+++ b/src/main/java/flipnote/user/application/OAuthService.java
@@ -0,0 +1,113 @@
+package flipnote.user.application;
+
+import java.util.Map;
+import java.util.UUID;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import flipnote.user.domain.AuthErrorCode;
+import flipnote.user.domain.TokenPair;
+import flipnote.user.domain.UserErrorCode;
+import flipnote.user.domain.common.BizException;
+import flipnote.user.domain.entity.OAuthLink;
+import flipnote.user.domain.entity.User;
+import flipnote.user.domain.repository.OAuthLinkRepository;
+import flipnote.user.domain.repository.UserRepository;
+import flipnote.user.infrastructure.jwt.JwtProvider;
+import flipnote.user.infrastructure.oauth.OAuth2UserInfo;
+import flipnote.user.infrastructure.oauth.OAuthApiClient;
+import flipnote.user.infrastructure.oauth.OAuthProperties;
+import flipnote.user.infrastructure.oauth.PkceUtil;
+import flipnote.user.infrastructure.redis.SocialLinkTokenRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@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, String codeVerifier) {
+ }
+
+ 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);
+
+ return new AuthorizationRedirect(authorizeUri, codeVerifier);
+ }
+
+ 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.existsByProviderAndProviderId(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/application/UserService.java b/src/main/java/flipnote/user/application/UserService.java
new file mode 100644
index 0000000..6d33ab1
--- /dev/null
+++ b/src/main/java/flipnote/user/application/UserService.java
@@ -0,0 +1,122 @@
+package flipnote.user.application;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import flipnote.image.grpc.v1.ActivateImageRequest;
+import flipnote.image.grpc.v1.ActivateImageResponse;
+import flipnote.image.grpc.v1.ChangeImageRequest;
+import flipnote.image.grpc.v1.ChangeImageResponse;
+import flipnote.image.grpc.v1.ImageCommandServiceGrpc;
+import flipnote.image.grpc.v1.Type;
+import flipnote.user.application.command.UpdateProfileCommand;
+import flipnote.user.application.result.MyInfoResult;
+import flipnote.user.application.result.UserInfoResult;
+import flipnote.user.application.result.UserResult;
+import flipnote.user.application.result.UserUpdateResult;
+import flipnote.user.domain.AuthErrorCode;
+import flipnote.user.domain.ImageErrorCode;
+import flipnote.user.domain.TokenClaims;
+import flipnote.user.domain.UserErrorCode;
+import flipnote.user.domain.common.BizException;
+import flipnote.user.domain.entity.User;
+import flipnote.user.domain.repository.UserRepository;
+import flipnote.user.infrastructure.jwt.JwtProvider;
+import flipnote.user.infrastructure.redis.SessionInvalidationRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class UserService {
+
+ private final UserRepository userRepository;
+ private final SessionInvalidationRepository sessionInvalidationRepository;
+ private final JwtProvider jwtProvider;
+ private final ImageCommandServiceGrpc.ImageCommandServiceBlockingStub imageCommandServiceStub;
+
+ public MyInfoResult getMyInfo(Long userId) {
+ User user = findActiveUser(userId);
+ return MyInfoResult.from(user);
+ }
+
+ public UserInfoResult getUserInfo(Long userId) {
+ User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE)
+ .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND));
+ return UserInfoResult.from(user);
+ }
+
+ @Transactional
+ public UserUpdateResult updateProfile(Long userId, UpdateProfileCommand command) {
+ User user = findActiveUser(userId);
+
+ String profileImageUrl = null;
+ if (command.imageRefId() != null) {
+ try {
+ if (User.DEFAULT_PROFILE_IMAGE_URL.equals(user.getProfileImageUrl())) {
+ ActivateImageResponse activateImageResponse = imageCommandServiceStub.activateImage(
+ ActivateImageRequest.newBuilder()
+ .setReferenceType(Type.USER)
+ .setReferenceId(userId)
+ .setImageRefId(command.imageRefId())
+ .build());
+ profileImageUrl = activateImageResponse.getUrl();
+ } else {
+ ChangeImageResponse changeImageResponse = imageCommandServiceStub.changeImage(
+ ChangeImageRequest.newBuilder()
+ .setReferenceType(Type.USER)
+ .setReferenceId(userId)
+ .setImageRefId(command.imageRefId())
+ .build());
+ profileImageUrl = changeImageResponse.getUrl();
+ }
+ } catch (Exception ex) {
+ log.error("updateProfile", ex);
+ throw new BizException(ImageErrorCode.IMAGE_SERVICE_ERROR);
+ }
+ }
+
+ user.updateProfile(command.nickname(), command.phone(), Boolean.TRUE.equals(command.smsAgree()), profileImageUrl);
+ return UserUpdateResult.from(user, command.imageRefId());
+ }
+
+ @Transactional
+ public void withdraw(Long userId) {
+ User user = findActiveUser(userId);
+ user.withdraw();
+ sessionInvalidationRepository.invalidate(userId, jwtProvider.getRefreshTokenExpiration());
+ }
+
+ public Optional findActiveUserById(Long userId) {
+ return userRepository.findByIdAndStatus(userId, User.Status.ACTIVE)
+ .map(UserResult::from);
+ }
+
+ public List findActiveUsersByIds(List userIds) {
+ return userRepository.findByIdInAndStatus(userIds, User.Status.ACTIVE)
+ .stream().map(UserResult::from).toList();
+ }
+
+ public Optional findActiveUserByEmail(String email) {
+ return userRepository.findByEmailAndStatus(email, User.Status.ACTIVE)
+ .map(UserResult::from);
+ }
+
+ public UserResult findUserByToken(String token) {
+ if (!jwtProvider.isTokenValid(token)) {
+ throw new BizException(AuthErrorCode.INVALID_TOKEN);
+ }
+ TokenClaims claims = jwtProvider.extractClaims(token);
+ return UserResult.from(findActiveUser(claims.userId()));
+ }
+
+ 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/application/command/ChangePasswordCommand.java b/src/main/java/flipnote/user/application/command/ChangePasswordCommand.java
new file mode 100644
index 0000000..ecc1ae6
--- /dev/null
+++ b/src/main/java/flipnote/user/application/command/ChangePasswordCommand.java
@@ -0,0 +1,3 @@
+package flipnote.user.application.command;
+
+public record ChangePasswordCommand(String currentPassword, String newPassword) {}
diff --git a/src/main/java/flipnote/user/application/command/LoginCommand.java b/src/main/java/flipnote/user/application/command/LoginCommand.java
new file mode 100644
index 0000000..f824ee9
--- /dev/null
+++ b/src/main/java/flipnote/user/application/command/LoginCommand.java
@@ -0,0 +1,3 @@
+package flipnote.user.application.command;
+
+public record LoginCommand(String email, String password) {}
diff --git a/src/main/java/flipnote/user/application/command/SignupCommand.java b/src/main/java/flipnote/user/application/command/SignupCommand.java
new file mode 100644
index 0000000..024f3f7
--- /dev/null
+++ b/src/main/java/flipnote/user/application/command/SignupCommand.java
@@ -0,0 +1,10 @@
+package flipnote.user.application.command;
+
+public record SignupCommand(
+ String email,
+ String password,
+ String name,
+ String nickname,
+ String phone,
+ boolean smsAgree
+) {}
diff --git a/src/main/java/flipnote/user/application/command/UpdateProfileCommand.java b/src/main/java/flipnote/user/application/command/UpdateProfileCommand.java
new file mode 100644
index 0000000..218635f
--- /dev/null
+++ b/src/main/java/flipnote/user/application/command/UpdateProfileCommand.java
@@ -0,0 +1,8 @@
+package flipnote.user.application.command;
+
+public record UpdateProfileCommand(
+ String nickname,
+ String phone,
+ Boolean smsAgree,
+ Long imageRefId
+) {}
diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java b/src/main/java/flipnote/user/application/result/MyInfoResult.java
similarity index 68%
rename from src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java
rename to src/main/java/flipnote/user/application/result/MyInfoResult.java
index 0798946..90fc081 100644
--- a/src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java
+++ b/src/main/java/flipnote/user/application/result/MyInfoResult.java
@@ -1,7 +1,6 @@
-package flipnote.user.user.presentation.dto.response;
+package flipnote.user.application.result;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import flipnote.user.user.domain.User;
+import flipnote.user.domain.entity.User;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -9,7 +8,7 @@
@Getter
@AllArgsConstructor
-public class MyInfoResponse {
+public class MyInfoResult {
private Long userId;
private String email;
@@ -19,15 +18,11 @@ public class MyInfoResponse {
private Boolean smsAgree;
private String profileImageUrl;
private Long imageRefId;
-
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
-
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime modifiedAt;
- public static MyInfoResponse from(User user) {
- return new MyInfoResponse(
+ public static MyInfoResult from(User user) {
+ return new MyInfoResult(
user.getId(),
user.getEmail(),
user.getNickname(),
diff --git a/src/main/java/flipnote/user/application/result/SocialLinkResult.java b/src/main/java/flipnote/user/application/result/SocialLinkResult.java
new file mode 100644
index 0000000..e5e088e
--- /dev/null
+++ b/src/main/java/flipnote/user/application/result/SocialLinkResult.java
@@ -0,0 +1,20 @@
+package flipnote.user.application.result;
+
+import flipnote.user.domain.entity.OAuthLink;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@AllArgsConstructor
+public class SocialLinkResult {
+
+ private Long socialLinkId;
+ private String provider;
+ private LocalDateTime linkedAt;
+
+ public static SocialLinkResult from(OAuthLink link) {
+ return new SocialLinkResult(link.getId(), link.getProvider(), link.getLinkedAt());
+ }
+}
diff --git a/src/main/java/flipnote/user/application/result/SocialLinksResult.java b/src/main/java/flipnote/user/application/result/SocialLinksResult.java
new file mode 100644
index 0000000..f2c71d0
--- /dev/null
+++ b/src/main/java/flipnote/user/application/result/SocialLinksResult.java
@@ -0,0 +1,18 @@
+package flipnote.user.application.result;
+
+import flipnote.user.domain.entity.OAuthLink;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.List;
+
+@Getter
+@AllArgsConstructor
+public class SocialLinksResult {
+
+ private List socialLinks;
+
+ public static SocialLinksResult from(List links) {
+ return new SocialLinksResult(links.stream().map(SocialLinkResult::from).toList());
+ }
+}
diff --git a/src/main/java/flipnote/user/application/result/TokenValidateResult.java b/src/main/java/flipnote/user/application/result/TokenValidateResult.java
new file mode 100644
index 0000000..8a35b85
--- /dev/null
+++ b/src/main/java/flipnote/user/application/result/TokenValidateResult.java
@@ -0,0 +1,3 @@
+package flipnote.user.application.result;
+
+public record TokenValidateResult(Long userId, String email, String role) {}
diff --git a/src/main/java/flipnote/user/application/result/UserInfoResult.java b/src/main/java/flipnote/user/application/result/UserInfoResult.java
new file mode 100644
index 0000000..e9db900
--- /dev/null
+++ b/src/main/java/flipnote/user/application/result/UserInfoResult.java
@@ -0,0 +1,19 @@
+package flipnote.user.application.result;
+
+import flipnote.user.domain.entity.User;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class UserInfoResult {
+
+ private Long userId;
+ private String nickname;
+ private String profileImageUrl;
+ private Long imageRefId;
+
+ public static UserInfoResult from(User user) {
+ return new UserInfoResult(user.getId(), user.getNickname(), user.getProfileImageUrl(), null);
+ }
+}
diff --git a/src/main/java/flipnote/user/application/result/UserRegisterResult.java b/src/main/java/flipnote/user/application/result/UserRegisterResult.java
new file mode 100644
index 0000000..fc2ef22
--- /dev/null
+++ b/src/main/java/flipnote/user/application/result/UserRegisterResult.java
@@ -0,0 +1,10 @@
+package flipnote.user.application.result;
+
+import flipnote.user.domain.entity.User;
+
+public record UserRegisterResult(Long userId) {
+
+ public static UserRegisterResult from(User user) {
+ return new UserRegisterResult(user.getId());
+ }
+}
diff --git a/src/main/java/flipnote/user/application/result/UserResult.java b/src/main/java/flipnote/user/application/result/UserResult.java
new file mode 100644
index 0000000..ea23b5f
--- /dev/null
+++ b/src/main/java/flipnote/user/application/result/UserResult.java
@@ -0,0 +1,16 @@
+package flipnote.user.application.result;
+
+import flipnote.user.domain.entity.User;
+
+public record UserResult(Long id, String email, String nickname, String profileImageUrl, String role) {
+
+ public static UserResult from(User user) {
+ return new UserResult(
+ user.getId(),
+ user.getEmail(),
+ user.getNickname(),
+ user.getProfileImageUrl() != null ? user.getProfileImageUrl() : "",
+ user.getRole().name()
+ );
+ }
+}
diff --git a/src/main/java/flipnote/user/application/result/UserUpdateResult.java b/src/main/java/flipnote/user/application/result/UserUpdateResult.java
new file mode 100644
index 0000000..8c9e7c6
--- /dev/null
+++ b/src/main/java/flipnote/user/application/result/UserUpdateResult.java
@@ -0,0 +1,28 @@
+package flipnote.user.application.result;
+
+import flipnote.user.domain.entity.User;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class UserUpdateResult {
+
+ private Long userId;
+ private String nickname;
+ private String phone;
+ private Boolean smsAgree;
+ private String profileImageUrl;
+ private Long imageRefId;
+
+ public static UserUpdateResult from(User user, Long imageRefId) {
+ return new UserUpdateResult(
+ user.getId(),
+ user.getNickname(),
+ user.getPhone(),
+ user.isSmsAgree(),
+ user.getProfileImageUrl(),
+ imageRefId
+ );
+ }
+}
diff --git a/src/main/java/flipnote/user/auth/application/AuthService.java b/src/main/java/flipnote/user/auth/application/AuthService.java
deleted file mode 100644
index 0b81523..0000000
--- a/src/main/java/flipnote/user/auth/application/AuthService.java
+++ /dev/null
@@ -1,232 +0,0 @@
-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;
-import lombok.RequiredArgsConstructor;
-import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.List;
-
-@Service
-@RequiredArgsConstructor
-@Transactional(readOnly = true)
-public class AuthService {
-
- private final UserRepository userRepository;
- private final PasswordEncoder passwordEncoder;
- private final JwtProvider jwtProvider;
- private final TokenBlacklistRepository tokenBlacklistRepository;
- private final EmailVerificationRepository emailVerificationRepository;
- private final PasswordResetRepository passwordResetRepository;
- private final OAuthLinkRepository oAuthLinkRepository;
- private final SessionInvalidationRepository sessionInvalidationRepository;
- private final VerificationCodeGenerator verificationCodeGenerator;
- private final PasswordResetTokenGenerator passwordResetTokenGenerator;
- private final ClientProperties clientProperties;
- private final ApplicationEventPublisher eventPublisher;
-
- @Transactional
- public UserResponse register(SignupRequest request) {
- if (!emailVerificationRepository.isVerified(request.getEmail())) {
- throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL);
- }
-
- if (userRepository.existsByEmail(request.getEmail())) {
- throw new BizException(AuthErrorCode.EMAIL_ALREADY_EXISTS);
- }
-
- User user = User.builder()
- .email(request.getEmail())
- .password(passwordEncoder.encode(request.getPassword()))
- .name(request.getName())
- .nickname(request.getNickname())
- .phone(request.getPhone())
- .smsAgree(Boolean.TRUE.equals(request.getSmsAgree()))
- .build();
-
- User savedUser = userRepository.save(user);
- return UserResponse.from(savedUser);
- }
-
- public TokenPair login(LoginRequest request) {
- User user = userRepository.findByEmailAndStatus(request.getEmail(), User.Status.ACTIVE)
- .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS));
-
- if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
- throw new BizException(AuthErrorCode.INVALID_CREDENTIALS);
- }
-
- return jwtProvider.generateTokenPair(user);
- }
-
- public void logout(String refreshToken) {
- if (refreshToken != null && jwtProvider.isTokenValid(refreshToken)) {
- long remaining = jwtProvider.getRemainingExpiration(refreshToken);
- if (remaining > 0) {
- tokenBlacklistRepository.add(refreshToken, remaining);
- }
- }
- }
-
- public TokenPair refreshToken(String refreshToken) {
- if (refreshToken == null || !jwtProvider.isTokenValid(refreshToken)) {
- throw new BizException(AuthErrorCode.INVALID_TOKEN);
- }
-
- if (tokenBlacklistRepository.isBlacklisted(refreshToken)) {
- throw new BizException(AuthErrorCode.BLACKLISTED_TOKEN);
- }
-
- TokenClaims claims = jwtProvider.extractClaims(refreshToken);
-
- sessionInvalidationRepository.getInvalidatedAtMillis(claims.userId()).ifPresent(invalidatedAtMillis -> {
- if (jwtProvider.getIssuedAt(refreshToken).getTime() < invalidatedAtMillis) {
- throw new BizException(AuthErrorCode.INVALIDATED_SESSION);
- }
- });
-
- User user = findActiveUser(claims.userId());
-
- long remaining = jwtProvider.getRemainingExpiration(refreshToken);
- if (remaining > 0) {
- tokenBlacklistRepository.add(refreshToken, remaining);
- }
-
- return jwtProvider.generateTokenPair(user);
- }
-
- @Transactional
- public void changePassword(Long userId, ChangePasswordRequest request) {
- User user = findActiveUser(userId);
-
- if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) {
- throw new BizException(AuthErrorCode.PASSWORD_MISMATCH);
- }
-
- user.changePassword(passwordEncoder.encode(request.getNewPassword()));
- sessionInvalidationRepository.invalidate(user.getId(), jwtProvider.getRefreshTokenExpiration());
- }
-
- public TokenValidateResponse validateToken(String token) {
- if (!jwtProvider.isTokenValid(token)) {
- throw new BizException(AuthErrorCode.INVALID_TOKEN);
- }
-
- if (tokenBlacklistRepository.isBlacklisted(token)) {
- throw new BizException(AuthErrorCode.BLACKLISTED_TOKEN);
- }
-
- TokenClaims claims = jwtProvider.extractClaims(token);
-
- sessionInvalidationRepository.getInvalidatedAtMillis(claims.userId()).ifPresent(invalidatedAtMillis -> {
- if (jwtProvider.getIssuedAt(token).getTime() < invalidatedAtMillis) {
- throw new BizException(AuthErrorCode.INVALIDATED_SESSION);
- }
- });
-
- findActiveUser(claims.userId());
-
- return new TokenValidateResponse(claims.userId(), claims.email(), claims.role());
- }
-
- public void sendEmailVerificationCode(String email) {
- if (emailVerificationRepository.hasCode(email)) {
- throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE);
- }
-
- String code = verificationCodeGenerator.generate();
- emailVerificationRepository.saveCode(email, code);
- eventPublisher.publishEvent(new EmailVerificationSendEvent(email, code));
- }
-
- public void verifyEmail(String email, String code) {
- if (!emailVerificationRepository.hasCode(email)) {
- throw new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE);
- }
-
- String savedCode = emailVerificationRepository.getCode(email);
- if (!code.equals(savedCode)) {
- throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE);
- }
-
- emailVerificationRepository.deleteCode(email);
- emailVerificationRepository.markVerified(email);
- }
-
- public void requestPasswordReset(String email) {
- // μ¬μ©μκ° μμ΄λ μ μ λ°ν (μ΄λ©μΌ μ‘΄μ¬ μ¬λΆ λ
ΈμΆ λ°©μ§)
- if (!userRepository.existsByEmail(email)) {
- return;
- }
-
- if (passwordResetRepository.hasToken(email)) {
- throw new BizException(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK);
- }
-
- String token = passwordResetTokenGenerator.generate();
- passwordResetRepository.save(token, email);
-
- String link = clientProperties.getUrl() + clientProperties.getPaths().getPasswordReset()
- + "?token=" + token;
- eventPublisher.publishEvent(new PasswordResetCreateEvent(email, link));
- }
-
- @Transactional
- public void resetPassword(String token, String newPassword) {
- String email = passwordResetRepository.findEmailByToken(token);
- if (email == null) {
- throw new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN);
- }
-
- User user = userRepository.findByEmailAndStatus(email, User.Status.ACTIVE)
- .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND));
-
- user.changePassword(passwordEncoder.encode(newPassword));
- sessionInvalidationRepository.invalidate(user.getId(), jwtProvider.getRefreshTokenExpiration());
- passwordResetRepository.delete(token, email);
- }
-
- public SocialLinksResponse getSocialLinks(Long userId) {
- List links = oAuthLinkRepository.findByUser_Id(userId);
- return SocialLinksResponse.from(links);
- }
-
- @Transactional
- public void deleteSocialLink(Long userId, Long socialLinkId) {
- if (!oAuthLinkRepository.existsByIdAndUser_Id(socialLinkId, userId)) {
- throw new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT);
- }
- oAuthLinkRepository.deleteById(socialLinkId);
- }
-
- 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/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/domain/AuthErrorCode.java b/src/main/java/flipnote/user/auth/domain/AuthErrorCode.java
deleted file mode 100644
index 6ff29e5..0000000
--- a/src/main/java/flipnote/user/auth/domain/AuthErrorCode.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package flipnote.user.auth.domain;
-
-import flipnote.user.global.error.ErrorCode;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.HttpStatus;
-
-@Getter
-@RequiredArgsConstructor
-public enum AuthErrorCode implements ErrorCode {
-
- INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_001", "μ΄λ©μΌ λλ λΉλ°λ²νΈκ° μ¬λ°λ₯΄μ§ μμ΅λλ€."),
- ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.TOO_MANY_REQUESTS, "AUTH_002", "μ΄λ―Έ μΈμ¦μ½λκ° λ°μ‘λμμ΅λλ€. μ μ ν λ€μ μλν΄ μ£ΌμΈμ."),
- NOT_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_003", "μΈμ¦μ½λκ° λ°μ‘λμ§ μμμ΅λλ€."),
- INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_004", "μΈμ¦μ½λκ° μ¬λ°λ₯΄μ§ μμ΅λλ€."),
- EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "AUTH_005", "μ΄λ―Έ μ¬μ© μ€μΈ μ΄λ©μΌμ
λλ€."),
- INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_006", "μ ν¨νμ§ μμ ν ν°μ
λλ€."),
- UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_007", "μ΄λ©μΌ μΈμ¦μ΄ μλ£λμ§ μμμ΅λλ€."),
- ALREADY_SENT_PASSWORD_RESET_LINK(HttpStatus.TOO_MANY_REQUESTS, "AUTH_008", "μ΄λ―Έ λΉλ°λ²νΈ μ¬μ€μ λ§ν¬κ° λ°μ‘λμμ΅λλ€. μ μ ν λ€μ μλν΄ μ£ΌμΈμ."),
- INVALID_PASSWORD_RESET_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_009", "μ ν¨νμ§ μμ λΉλ°λ²νΈ μ¬μ€μ ν ν°μ
λλ€."),
- INVALID_SOCIAL_LINK_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_010", "μ ν¨νμ§ μμ μμ
μ°λ ν ν°μ
λλ€."),
- ALREADY_LINKED_SOCIAL_ACCOUNT(HttpStatus.CONFLICT, "AUTH_011", "μ΄λ―Έ μ°κ²°λ μμ
κ³μ μ
λλ€."),
- NOT_REGISTERED_SOCIAL_ACCOUNT(HttpStatus.NOT_FOUND, "AUTH_012", "μ°κ²°λ μμ
κ³μ μ΄ μμ΅λλ€."),
- INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH_013", "μ§μνμ§ μλ OAuth μ 곡μμ
λλ€."),
- BLACKLISTED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_015", "무ν¨νλ ν ν°μ
λλ€."),
- INVALIDATED_SESSION(HttpStatus.UNAUTHORIZED, "AUTH_016", "μΈμ
μ΄ λ¬΄ν¨νλμμ΅λλ€. λ€μ λ‘κ·ΈμΈν΄ μ£ΌμΈμ."),
- PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_017", "νμ¬ λΉλ°λ²νΈκ° μΌμΉνμ§ μμ΅λλ€.");
-
- private final HttpStatus httpStatus;
- private final String code;
- private final String message;
-
- @Override
- public int getStatus() {
- return httpStatus.value();
- }
-}
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/dto/response/SocialLinkResponse.java b/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinkResponse.java
deleted file mode 100644
index 1c7f250..0000000
--- a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinkResponse.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package flipnote.user.auth.presentation.dto.response;
-
-import com.fasterxml.jackson.annotation.JsonFormat;
-import flipnote.user.user.domain.OAuthLink;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-import java.time.LocalDateTime;
-
-@Getter
-@AllArgsConstructor
-public class SocialLinkResponse {
-
- private Long socialLinkId;
- private String provider;
-
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
- private LocalDateTime linkedAt;
-
- public static SocialLinkResponse from(OAuthLink link) {
- return new SocialLinkResponse(link.getId(), link.getProvider(), link.getLinkedAt());
- }
-}
diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java b/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java
deleted file mode 100644
index d5f3bbd..0000000
--- a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package flipnote.user.auth.presentation.dto.response;
-
-import flipnote.user.user.domain.OAuthLink;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-import java.util.List;
-
-@Getter
-@AllArgsConstructor
-public class SocialLinksResponse {
-
- private List socialLinks;
-
- public static SocialLinksResponse from(List links) {
- List socialLinks = links.stream()
- .map(SocialLinkResponse::from)
- .toList();
- return new SocialLinksResponse(socialLinks);
- }
-}
diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java b/src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java
deleted file mode 100644
index 6b799b5..0000000
--- a/src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package flipnote.user.auth.presentation.dto.response;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-@Getter
-@AllArgsConstructor
-public class TokenValidateResponse {
-
- private Long userId;
- private String email;
- private String role;
-}
diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java b/src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java
deleted file mode 100644
index 66a9541..0000000
--- a/src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package flipnote.user.auth.presentation.dto.response;
-
-import flipnote.user.user.domain.User;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-@Getter
-@AllArgsConstructor
-public class UserResponse {
-
- private Long userId;
-
- public static UserResponse from(User user) {
- return new UserResponse(user.getId());
- }
-}
diff --git a/src/main/java/flipnote/user/domain/AuthErrorCode.java b/src/main/java/flipnote/user/domain/AuthErrorCode.java
new file mode 100644
index 0000000..479b460
--- /dev/null
+++ b/src/main/java/flipnote/user/domain/AuthErrorCode.java
@@ -0,0 +1,39 @@
+package flipnote.user.domain;
+
+import org.springframework.http.HttpStatus;
+
+import flipnote.user.domain.common.ErrorCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum AuthErrorCode implements ErrorCode {
+
+ INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_001", "μ΄λ©μΌ λλ λΉλ°λ²νΈκ° μ¬λ°λ₯΄μ§ μμ΅λλ€."),
+ ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.TOO_MANY_REQUESTS, "AUTH_002", "μ΄λ―Έ μΈμ¦μ½λκ° λ°μ‘λμμ΅λλ€. μ μ ν λ€μ μλν΄ μ£ΌμΈμ."),
+ NOT_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_003", "μΈμ¦μ½λκ° λ°μ‘λμ§ μμμ΅λλ€."),
+ INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_004", "μΈμ¦μ½λκ° μ¬λ°λ₯΄μ§ μμ΅λλ€."),
+ EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "AUTH_005", "μ΄λ―Έ μ¬μ© μ€μΈ μ΄λ©μΌμ
λλ€."),
+ INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_006", "μ ν¨νμ§ μμ ν ν°μ
λλ€."),
+ UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_007", "μ΄λ©μΌ μΈμ¦μ΄ μλ£λμ§ μμμ΅λλ€."),
+ ALREADY_SENT_PASSWORD_RESET_LINK(HttpStatus.TOO_MANY_REQUESTS, "AUTH_008", "μ΄λ―Έ λΉλ°λ²νΈ μ¬μ€μ λ§ν¬κ° λ°μ‘λμμ΅λλ€. μ μ ν λ€μ μλν΄ μ£ΌμΈμ."),
+ INVALID_PASSWORD_RESET_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_009", "μ ν¨νμ§ μμ λΉλ°λ²νΈ μ¬μ€μ ν ν°μ
λλ€."),
+ INVALID_SOCIAL_LINK_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_010", "μ ν¨νμ§ μμ μμ
μ°λ ν ν°μ
λλ€."),
+ ALREADY_LINKED_SOCIAL_ACCOUNT(HttpStatus.CONFLICT, "AUTH_011", "μ΄λ―Έ μ°κ²°λ μμ
κ³μ μ
λλ€."),
+ NOT_REGISTERED_SOCIAL_ACCOUNT(HttpStatus.NOT_FOUND, "AUTH_012", "μ°κ²°λ μμ
κ³μ μ΄ μμ΅λλ€."),
+ INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH_013", "μ§μνμ§ μλ OAuth μ 곡μμ
λλ€."),
+ OAUTH_COMMUNICATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_014", "OAuth μλ²μμ ν΅μ μ€ μ€λ₯κ° λ°μνμ΅λλ€."),
+ BLACKLISTED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_015", "무ν¨νλ ν ν°μ
λλ€."),
+ INVALIDATED_SESSION(HttpStatus.UNAUTHORIZED, "AUTH_016", "μΈμ
μ΄ λ¬΄ν¨νλμμ΅λλ€. λ€μ λ‘κ·ΈμΈν΄ μ£ΌμΈμ."),
+ PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_017", "νμ¬ λΉλ°λ²νΈκ° μΌμΉνμ§ μμ΅λλ€.");
+
+ private final HttpStatus httpStatus;
+ private final String code;
+ private final String message;
+
+ @Override
+ public int getStatus() {
+ return httpStatus.value();
+ }
+}
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 70%
rename from src/main/java/flipnote/user/auth/domain/VerificationConstants.java
rename to src/main/java/flipnote/user/domain/VerificationConstants.java
index d92bc97..cc8d5d3 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;
@@ -7,4 +7,5 @@
public final class VerificationConstants {
public static final int CODE_TTL_MINUTES = 5;
+ public static final int VERIFIED_TTL_MINUTES = 10;
}
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 81%
rename from src/main/java/flipnote/user/user/domain/OAuthLinkRepository.java
rename to src/main/java/flipnote/user/domain/repository/OAuthLinkRepository.java
index fbc55c8..1286797 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("""
@@ -19,7 +21,7 @@ Optional findByProviderAndProviderIdWithUser(
@Param("providerId") String providerId
);
- boolean existsByUser_IdAndProviderAndProviderId(Long userId, String provider, String providerId);
+ boolean existsByProviderAndProviderId(String provider, String providerId);
boolean existsByIdAndUser_Id(Long id, Long userId);
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/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/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..393d530 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,8 @@
-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 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/auth/infrastructure/redis/PasswordResetTokenGenerator.java b/src/main/java/flipnote/user/infrastructure/mail/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/mail/PasswordResetTokenGenerator.java
index 1e8797f..53074b5 100644
--- a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetTokenGenerator.java
+++ b/src/main/java/flipnote/user/infrastructure/mail/PasswordResetTokenGenerator.java
@@ -1,4 +1,4 @@
-package flipnote.user.auth.infrastructure.redis;
+package flipnote.user.infrastructure.mail;
import org.springframework.stereotype.Component;
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/redis/VerificationCodeGenerator.java b/src/main/java/flipnote/user/infrastructure/mail/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/mail/VerificationCodeGenerator.java
index 301156d..bc61620 100644
--- a/src/main/java/flipnote/user/auth/infrastructure/redis/VerificationCodeGenerator.java
+++ b/src/main/java/flipnote/user/infrastructure/mail/VerificationCodeGenerator.java
@@ -1,4 +1,4 @@
-package flipnote.user.auth.infrastructure.redis;
+package flipnote.user.infrastructure.mail;
import org.springframework.stereotype.Component;
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..a3f8c28
--- /dev/null
+++ b/src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java
@@ -0,0 +1,101 @@
+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.util.UriComponentsBuilder;
+
+import flipnote.user.domain.AuthErrorCode;
+import flipnote.user.domain.common.BizException;
+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 BizException(AuthErrorCode.OAUTH_COMMUNICATION_ERROR);
+ }
+ }
+
+ 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 BizException(AuthErrorCode.OAUTH_COMMUNICATION_ERROR);
+ }
+ }
+
+ public OAuth2UserInfo createUserInfo(String providerName, Map attributes) {
+ return switch (providerName.toLowerCase()) {
+ case "google" -> new GoogleUserInfo(attributes);
+ default -> throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER);
+ };
+ }
+
+ 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())
+ .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..5b0260c
--- /dev/null
+++ b/src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java
@@ -0,0 +1,46 @@
+package flipnote.user.infrastructure.oauth;
+
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+@Validated
+@Getter
+@RequiredArgsConstructor
+@ConfigurationProperties(prefix = "app.oauth2")
+public class OAuthProperties {
+
+ @NotBlank
+ private final String baseUrl;
+
+ @Valid
+ private final Map providers;
+
+ @Getter
+ @Setter
+ public static class Provider {
+ @NotBlank
+ private String clientId;
+ @NotBlank
+ private String clientSecret;
+ @NotBlank
+ private String redirectUri;
+ @NotBlank
+ private String authorizationUri;
+ @NotBlank
+ private String tokenUri;
+ @NotBlank
+ private String userInfoUri;
+ @NotEmpty
+ 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 86%
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..e3cf08c 100644
--- a/src/main/java/flipnote/user/auth/infrastructure/redis/EmailVerificationRepository.java
+++ b/src/main/java/flipnote/user/infrastructure/redis/EmailVerificationRepository.java
@@ -1,5 +1,6 @@
-package flipnote.user.auth.infrastructure.redis;
+package flipnote.user.infrastructure.redis;
+import flipnote.user.domain.VerificationConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
@@ -12,8 +13,6 @@ public class EmailVerificationRepository {
private static final String CODE_KEY_PREFIX = "email:verification:code:";
private static final String VERIFIED_KEY_PREFIX = "email:verification:verified:";
- private static final long CODE_TTL_MINUTES = 5;
- private static final long VERIFIED_TTL_MINUTES = 10;
private final StringRedisTemplate redisTemplate;
@@ -21,7 +20,7 @@ public void saveCode(String email, String code) {
redisTemplate.opsForValue().set(
CODE_KEY_PREFIX + email,
code,
- CODE_TTL_MINUTES,
+ VerificationConstants.CODE_TTL_MINUTES,
TimeUnit.MINUTES
);
}
@@ -42,7 +41,7 @@ public void markVerified(String email) {
redisTemplate.opsForValue().set(
VERIFIED_KEY_PREFIX + email,
"verified",
- VERIFIED_TTL_MINUTES,
+ VerificationConstants.VERIFIED_TTL_MINUTES,
TimeUnit.MINUTES
);
}
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 85%
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..0d636f7 100644
--- a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetRepository.java
+++ b/src/main/java/flipnote/user/infrastructure/redis/PasswordResetRepository.java
@@ -1,5 +1,6 @@
-package flipnote.user.auth.infrastructure.redis;
+package flipnote.user.infrastructure.redis;
+import flipnote.user.domain.PasswordResetConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
@@ -12,7 +13,6 @@ public class PasswordResetRepository {
private static final String TOKEN_KEY_PREFIX = "password:reset:token:";
private static final String EMAIL_KEY_PREFIX = "password:reset:email:";
- private static final long TTL_MINUTES = 30;
private final StringRedisTemplate redisTemplate;
@@ -24,13 +24,13 @@ public void save(String token, String email) {
redisTemplate.opsForValue().set(
TOKEN_KEY_PREFIX + token,
email,
- TTL_MINUTES,
+ PasswordResetConstants.TOKEN_TTL_MINUTES,
TimeUnit.MINUTES
);
redisTemplate.opsForValue().set(
EMAIL_KEY_PREFIX + email,
token,
- TTL_MINUTES,
+ PasswordResetConstants.TOKEN_TTL_MINUTES,
TimeUnit.MINUTES
);
}
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/interfaces/grpc/GrpcExceptionHandlerImpl.java b/src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java
new file mode 100644
index 0000000..fc6c241
--- /dev/null
+++ b/src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java
@@ -0,0 +1,51 @@
+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 StatusException e) {
+ log.warn("gRPC StatusException: status={}, description={}",
+ e.getStatus().getCode(), e.getStatus().getDescription());
+ return e;
+ }
+ if (t instanceof StatusRuntimeException e) {
+ log.warn("gRPC StatusRuntimeException: status={}, description={}",
+ e.getStatus().getCode(), e.getStatus().getDescription());
+ return e.getStatus().asException(e.getTrailers());
+ }
+ 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..20bfcf9
--- /dev/null
+++ b/src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java
@@ -0,0 +1,93 @@
+package flipnote.user.interfaces.grpc;
+
+import java.util.List;
+
+import org.springframework.stereotype.Service;
+
+import flipnote.user.application.UserService;
+import flipnote.user.application.result.UserResult;
+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;
+
+@Service
+@RequiredArgsConstructor
+public class GrpcUserQueryService extends UserQueryServiceGrpc.UserQueryServiceImplBase {
+
+ private final UserService userService;
+
+ @Override
+ public void getUser(GetUserRequest request, StreamObserver responseObserver) {
+ UserResult user = userService.findActiveUserById(request.getUserId())
+ .orElseThrow(() -> Status.NOT_FOUND
+ .withDescription("μ¬μ©μλ₯Ό μ°Ύμ μ μμ΅λλ€.")
+ .asRuntimeException());
+
+ responseObserver.onNext(toUserResponse(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::toUserResponse).toList())
+ .build();
+
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ }
+
+ @Override
+ public void getUserByEmail(GetUserByEmailRequest request, StreamObserver responseObserver) {
+ userService.findActiveUserByEmail(request.getEmail())
+ .ifPresentOrElse(
+ user -> responseObserver.onNext(
+ GetUserByEmailResponse.newBuilder()
+ .setExists(true)
+ .setUser(toUserResponse(user))
+ .build()
+ ),
+ () -> responseObserver.onNext(
+ GetUserByEmailResponse.newBuilder()
+ .setExists(false)
+ .build()
+ )
+ );
+
+ responseObserver.onCompleted();
+ }
+
+ @Override
+ public void getUserByToken(GetUserByTokenRequest request, StreamObserver responseObserver) {
+ UserResult user = userService.findUserByToken(request.getAccessToken());
+
+ responseObserver.onNext(
+ GetUserByTokenResponse.newBuilder()
+ .setUserId(user.id())
+ .setNickname(user.nickname())
+ .build()
+ );
+ responseObserver.onCompleted();
+ }
+
+ private GetUserResponse toUserResponse(UserResult user) {
+ return GetUserResponse.newBuilder()
+ .setId(user.id())
+ .setEmail(user.email())
+ .setNickname(user.nickname())
+ .setProfileImageUrl(user.profileImageUrl())
+ .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 54%
rename from src/main/java/flipnote/user/auth/presentation/AuthController.java
rename to src/main/java/flipnote/user/interfaces/http/AuthController.java
index 060f8d6..aa44fc9 100644
--- a/src/main/java/flipnote/user/auth/presentation/AuthController.java
+++ b/src/main/java/flipnote/user/interfaces/http/AuthController.java
@@ -1,27 +1,36 @@
-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.http;
+
+import flipnote.user.application.AuthService;
+import flipnote.user.application.result.SocialLinksResult;
+import flipnote.user.application.result.TokenValidateResult;
+import flipnote.user.application.result.UserRegisterResult;
+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 jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.CookieValue;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/v1/auth")
@@ -32,15 +41,16 @@ public class AuthController {
private final JwtProvider jwtProvider;
@PostMapping("/register")
- public ResponseEntity register(@Valid @RequestBody SignupRequest request) {
- UserResponse response = authService.register(request);
- return ResponseEntity.status(HttpStatus.CREATED).body(response);
+ public ResponseEntity register(@Valid @RequestBody SignupRequest request) {
+ return ResponseEntity.status(HttpStatus.CREATED).body(authService.register(request.toCommand()));
}
@PostMapping("/login")
- public ResponseEntity login(@Valid @RequestBody LoginRequest request,
- HttpServletResponse response) {
- TokenPair tokenPair = authService.login(request);
+ public ResponseEntity login(
+ @Valid @RequestBody LoginRequest request,
+ HttpServletResponse response
+ ) {
+ TokenPair tokenPair = authService.login(request.toCommand());
setTokenCookies(response, tokenPair);
return ResponseEntity.ok().build();
}
@@ -48,7 +58,8 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest request,
@PostMapping("/logout")
public ResponseEntity logout(
@CookieValue(name = HttpConstants.REFRESH_TOKEN_COOKIE, required = false) String refreshToken,
- HttpServletResponse response) {
+ HttpServletResponse response
+ ) {
authService.logout(refreshToken);
CookieUtil.deleteCookie(response, HttpConstants.ACCESS_TOKEN_COOKIE);
CookieUtil.deleteCookie(response, HttpConstants.REFRESH_TOKEN_COOKIE);
@@ -58,47 +69,44 @@ public ResponseEntity logout(
@PostMapping("/token/refresh")
public ResponseEntity refreshToken(
@CookieValue(name = HttpConstants.REFRESH_TOKEN_COOKIE) String refreshToken,
- HttpServletResponse response) {
+ HttpServletResponse response
+ ) {
TokenPair tokenPair = authService.refreshToken(refreshToken);
setTokenCookies(response, tokenPair);
return ResponseEntity.ok().build();
}
@PostMapping("/token/validate")
- public ResponseEntity validateToken(
- @Valid @RequestBody TokenValidateRequest request) {
- TokenValidateResponse result = authService.validateToken(request.getToken());
- return ResponseEntity.ok(result);
+ public ResponseEntity validateToken(@Valid @RequestBody TokenValidateRequest request) {
+ return ResponseEntity.ok(authService.validateToken(request.getToken()));
}
@PatchMapping("/password")
public ResponseEntity changePassword(
@RequestHeader(HttpConstants.USER_ID_HEADER) Long userId,
@Valid @RequestBody ChangePasswordRequest request,
- HttpServletResponse response) {
- authService.changePassword(userId, request);
+ HttpServletResponse response
+ ) {
+ authService.changePassword(userId, request.toCommand());
CookieUtil.deleteCookie(response, HttpConstants.ACCESS_TOKEN_COOKIE);
CookieUtil.deleteCookie(response, HttpConstants.REFRESH_TOKEN_COOKIE);
return ResponseEntity.noContent().build();
}
@PostMapping("/email-verification/request")
- public ResponseEntity sendEmailVerification(
- @Valid @RequestBody EmailVerificationRequest request) {
+ public ResponseEntity sendEmailVerification(@Valid @RequestBody EmailVerificationRequest request) {
authService.sendEmailVerificationCode(request.getEmail());
return ResponseEntity.ok().build();
}
@PostMapping("/email-verification")
- public ResponseEntity verifyEmail(
- @Valid @RequestBody EmailVerifyRequest request) {
+ public ResponseEntity verifyEmail(@Valid @RequestBody EmailVerifyRequest request) {
authService.verifyEmail(request.getEmail(), request.getCode());
return ResponseEntity.ok().build();
}
@PostMapping("/password-reset/request")
- public ResponseEntity requestPasswordReset(
- @Valid @RequestBody PasswordResetCreateRequest request) {
+ public ResponseEntity requestPasswordReset(@Valid @RequestBody PasswordResetCreateRequest request) {
authService.requestPasswordReset(request.getEmail());
return ResponseEntity.noContent().build();
}
@@ -106,7 +114,8 @@ public ResponseEntity requestPasswordReset(
@PostMapping("/password-reset")
public ResponseEntity resetPassword(
@Valid @RequestBody PasswordResetRequest request,
- HttpServletResponse response) {
+ HttpServletResponse response
+ ) {
authService.resetPassword(request.getToken(), request.getPassword());
CookieUtil.deleteCookie(response, HttpConstants.ACCESS_TOKEN_COOKIE);
CookieUtil.deleteCookie(response, HttpConstants.REFRESH_TOKEN_COOKIE);
@@ -114,16 +123,16 @@ public ResponseEntity resetPassword(
}
@GetMapping("/social-links")
- public ResponseEntity getSocialLinks(
+ public ResponseEntity getSocialLinks(
@RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) {
- SocialLinksResponse response = authService.getSocialLinks(userId);
- return ResponseEntity.ok(response);
+ return ResponseEntity.ok(authService.getSocialLinks(userId));
}
@DeleteMapping("/social-links/{socialLinkId}")
public ResponseEntity deleteSocialLink(
@RequestHeader(HttpConstants.USER_ID_HEADER) Long userId,
- @PathVariable Long socialLinkId) {
+ @PathVariable Long socialLinkId
+ ) {
authService.deleteSocialLink(userId, socialLinkId);
return ResponseEntity.noContent().build();
}
diff --git a/src/main/java/flipnote/user/auth/presentation/OAuthController.java b/src/main/java/flipnote/user/interfaces/http/OAuthController.java
similarity index 80%
rename from src/main/java/flipnote/user/auth/presentation/OAuthController.java
rename to src/main/java/flipnote/user/interfaces/http/OAuthController.java
index e34b016..8c92703 100644
--- a/src/main/java/flipnote/user/auth/presentation/OAuthController.java
+++ b/src/main/java/flipnote/user/interfaces/http/OAuthController.java
@@ -1,18 +1,19 @@
-package flipnote.user.auth.presentation;
+package flipnote.user.interfaces.http;
-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 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.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@@ -28,14 +29,24 @@ public class OAuthController {
private final JwtProvider jwtProvider;
private final ClientProperties clientProperties;
+ private static final int VERIFIER_COOKIE_MAX_AGE = 180;
+
@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);
+ ResponseCookie verifierCookie = ResponseCookie.from(HttpConstants.OAUTH_VERIFIER_COOKIE, redirect.codeVerifier())
+ .httpOnly(true)
+ .secure(true)
+ .path("/")
+ .maxAge(VERIFIER_COOKIE_MAX_AGE)
+ .sameSite("Lax")
+ .build();
+
return ResponseEntity.status(HttpStatus.FOUND)
- .header(HttpHeaders.SET_COOKIE, redirect.verifierCookie().toString())
+ .header(HttpHeaders.SET_COOKIE, verifierCookie.toString())
.location(URI.create(redirect.authorizeUri()))
.build();
}
@@ -76,8 +87,7 @@ private ResponseEntity handleSocialLogin(String provider, String code, Str
}
}
- private ResponseEntity handleSocialLink(String provider, String code, String state,
- String codeVerifier) {
+ private ResponseEntity handleSocialLink(String provider, String code, String state, String codeVerifier) {
try {
oAuthService.linkSocialAccount(provider, code, state, codeVerifier);
return ResponseEntity.status(HttpStatus.FOUND)
diff --git a/src/main/java/flipnote/user/interfaces/http/UserController.java b/src/main/java/flipnote/user/interfaces/http/UserController.java
new file mode 100644
index 0000000..361110c
--- /dev/null
+++ b/src/main/java/flipnote/user/interfaces/http/UserController.java
@@ -0,0 +1,52 @@
+package flipnote.user.interfaces.http;
+
+import flipnote.user.application.UserService;
+import flipnote.user.application.result.MyInfoResult;
+import flipnote.user.application.result.UserInfoResult;
+import flipnote.user.application.result.UserUpdateResult;
+import flipnote.user.interfaces.http.common.HttpConstants;
+import flipnote.user.interfaces.http.dto.request.UpdateProfileRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/v1/users")
+@RequiredArgsConstructor
+public class UserController {
+
+ private final UserService userService;
+
+ @GetMapping("/me")
+ public ResponseEntity getMyInfo(
+ @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) {
+ return ResponseEntity.ok(userService.getMyInfo(userId));
+ }
+
+ @GetMapping("/{userId}")
+ public ResponseEntity getUserInfo(@PathVariable Long userId) {
+ return ResponseEntity.ok(userService.getUserInfo(userId));
+ }
+
+ @PutMapping
+ public ResponseEntity updateProfile(
+ @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId,
+ @Valid @RequestBody UpdateProfileRequest request) {
+ return ResponseEntity.ok(userService.updateProfile(userId, request.toCommand()));
+ }
+
+ @DeleteMapping
+ public ResponseEntity withdraw(
+ @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) {
+ userService.withdraw(userId);
+ return ResponseEntity.noContent().build();
+ }
+}
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 68%
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..f4c2513 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,5 +1,6 @@
-package flipnote.user.auth.presentation.dto.request;
+package flipnote.user.interfaces.http.dto.request;
+import flipnote.user.application.command.ChangePasswordCommand;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
@@ -15,4 +16,8 @@ public class ChangePasswordRequest {
@NotBlank(message = "μ λΉλ°λ²νΈλ νμμ
λλ€")
@Size(min = 8, max = 20, message = "λΉλ°λ²νΈλ 8μ μ΄μ 20μ μ΄νμ¬μΌ ν©λλ€")
private String newPassword;
+
+ public ChangePasswordCommand toCommand() {
+ return new ChangePasswordCommand(currentPassword, newPassword);
+ }
}
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 68%
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..f812b75 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,5 +1,6 @@
-package flipnote.user.auth.presentation.dto.request;
+package flipnote.user.interfaces.http.dto.request;
+import flipnote.user.application.command.LoginCommand;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
@@ -15,4 +16,8 @@ public class LoginRequest {
@NotBlank(message = "λΉλ°λ²νΈλ νμμ
λλ€")
private String password;
+
+ public LoginCommand toCommand() {
+ return new LoginCommand(email, password);
+ }
}
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 81%
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..1a0008c 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,5 +1,6 @@
-package flipnote.user.auth.presentation.dto.request;
+package flipnote.user.interfaces.http.dto.request;
+import flipnote.user.application.command.SignupCommand;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -32,4 +33,8 @@ public class SignupRequest {
@Pattern(regexp = "^01[0-9]{8,9}$", message = "μ¬λ°λ₯Έ μ νλ²νΈ νμμ΄ μλλλ€")
private String phone;
+
+ public SignupCommand toCommand() {
+ return new SignupCommand(email, password, name, nickname, phone, Boolean.TRUE.equals(smsAgree));
+ }
}
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 71%
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..86bf9aa 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,5 +1,6 @@
-package flipnote.user.user.presentation.dto.request;
+package flipnote.user.interfaces.http.dto.request;
+import flipnote.user.application.command.UpdateProfileCommand;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
@@ -20,4 +21,8 @@ public class UpdateProfileRequest {
private Boolean smsAgree;
private Long imageRefId;
+
+ public UpdateProfileCommand toCommand() {
+ return new UpdateProfileCommand(nickname, phone, smsAgree, imageRefId);
+ }
}
diff --git a/src/main/java/flipnote/user/interfaces/http/dto/response/.gitkeep b/src/main/java/flipnote/user/interfaces/http/dto/response/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/java/flipnote/user/user/application/UserService.java b/src/main/java/flipnote/user/user/application/UserService.java
deleted file mode 100644
index 220bb07..0000000
--- a/src/main/java/flipnote/user/user/application/UserService.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package flipnote.user.user.application;
-
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import flipnote.image.grpc.v1.ActivateImageRequest;
-import flipnote.image.grpc.v1.ActivateImageResponse;
-import flipnote.image.grpc.v1.ChangeImageRequest;
-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 lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-@Service
-@RequiredArgsConstructor
-@Transactional(readOnly = true)
-public class UserService {
-
- private final UserRepository userRepository;
- private final SessionInvalidationRepository sessionInvalidationRepository;
- private final JwtProvider jwtProvider;
- private final ImageCommandServiceGrpc.ImageCommandServiceBlockingStub imageCommandServiceStub;
-
- public MyInfoResponse getMyInfo(Long userId) {
- User user = findActiveUser(userId);
- return MyInfoResponse.from(user);
- }
-
- public UserInfoResponse getUserInfo(Long userId) {
- User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE)
- .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND));
- return UserInfoResponse.from(user);
- }
-
- @Transactional
- public UserUpdateResponse updateProfile(Long userId, UpdateProfileRequest request) {
- User user = findActiveUser(userId);
-
- String profileImageUrl = null;
- if (request.getImageRefId() != null) {
- try {
- if (User.DEFAULT_PROFILE_IMAGE_URL.equals(user.getProfileImageUrl())) {
- ActivateImageResponse activateImageResponse = imageCommandServiceStub.activateImage(
- ActivateImageRequest.newBuilder()
- .setReferenceType(Type.USER)
- .setReferenceId(userId)
- .setImageRefId(request.getImageRefId())
- .build());
-
- profileImageUrl = activateImageResponse.getUrl();
- } else {
- ChangeImageResponse changeImageResponse = imageCommandServiceStub.changeImage(
- ChangeImageRequest.newBuilder()
- .setReferenceType(Type.USER)
- .setReferenceId(userId)
- .setImageRefId(request.getImageRefId())
- .build());
-
- profileImageUrl = changeImageResponse.getUrl();
- }
- } catch (Exception ex) {
- log.error("updateProfile", ex);
- throw new BizException(ImageErrorCode.IMAGE_SERVICE_ERROR);
- }
- }
-
- user.updateProfile(request.getNickname(), request.getPhone(), request.getSmsAgree(), profileImageUrl);
- return UserUpdateResponse.from(user, request.getImageRefId());
- }
-
- @Transactional
- public void withdraw(Long userId) {
- User user = findActiveUser(userId);
- user.withdraw();
- sessionInvalidationRepository.invalidate(userId, jwtProvider.getRefreshTokenExpiration());
- }
-
- 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/user/presentation/UserController.java b/src/main/java/flipnote/user/user/presentation/UserController.java
deleted file mode 100644
index fc53b14..0000000
--- a/src/main/java/flipnote/user/user/presentation/UserController.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package flipnote.user.user.presentation;
-
-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 jakarta.validation.Valid;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-@RestController
-@RequestMapping("/v1/users")
-@RequiredArgsConstructor
-public class UserController {
-
- private final UserService userService;
-
- @GetMapping("/me")
- public ResponseEntity getMyInfo(
- @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) {
- MyInfoResponse response = userService.getMyInfo(userId);
- return ResponseEntity.ok(response);
- }
-
- @GetMapping("/{userId}")
- public ResponseEntity getUserInfo(@PathVariable Long userId) {
- UserInfoResponse response = userService.getUserInfo(userId);
- return ResponseEntity.ok(response);
- }
-
- @PutMapping
- public ResponseEntity updateProfile(
- @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId,
- @Valid @RequestBody UpdateProfileRequest request) {
- UserUpdateResponse response = userService.updateProfile(userId, request);
- return ResponseEntity.ok(response);
- }
-
- @DeleteMapping
- public ResponseEntity withdraw(
- @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) {
- userService.withdraw(userId);
- return ResponseEntity.noContent().build();
- }
-}
diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java b/src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java
deleted file mode 100644
index fd08f81..0000000
--- a/src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package flipnote.user.user.presentation.dto.response;
-
-import flipnote.user.user.domain.User;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-@Getter
-@AllArgsConstructor
-public class UserInfoResponse {
-
- private Long userId;
- private String nickname;
- private String profileImageUrl;
- private Long imageRefId;
-
- public static UserInfoResponse from(User user) {
- return new UserInfoResponse(user.getId(), user.getNickname(), user.getProfileImageUrl(), null);
- }
-}
diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java b/src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java
deleted file mode 100644
index 1880087..0000000
--- a/src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package flipnote.user.user.presentation.dto.response;
-
-import flipnote.user.user.domain.User;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-@Getter
-@AllArgsConstructor
-public class UserUpdateResponse {
-
- private Long userId;
- private String nickname;
- private String phone;
- private Boolean smsAgree;
- private String profileImageUrl;
- private Long imageRefId;
-
- public static UserUpdateResponse from(User user, Long imageRefId) {
- return new UserUpdateResponse(
- user.getId(),
- user.getNickname(),
- user.getPhone(),
- user.isSmsAgree(),
- user.getProfileImageUrl(),
- imageRefId
- );
- }
-}
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();
- }
-}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index be7129b..8ed9735 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -2,6 +2,11 @@ spring:
application:
name: user
+ jackson:
+ date-format: yyyy-MM-dd HH:mm:ss
+ serialization:
+ write-dates-as-timestamps: false
+
datasource:
url: ${DB_URL:jdbc:mysql://localhost:3306/flipnote_user}
username: ${DB_USERNAME:root}