Skip to content

[FEATURE] 애플 소셜 로그인 구현#148

Merged
parkmineum merged 9 commits intodevfrom
feature/#144_apple_login
Jan 28, 2026
Merged

[FEATURE] 애플 소셜 로그인 구현#148
parkmineum merged 9 commits intodevfrom
feature/#144_apple_login

Conversation

@parkmineum
Copy link
Member

@parkmineum parkmineum commented Jan 28, 2026

🎋 이슈 및 작업중인 브랜치

🔑 주요 내용

  • 출시를 위한 애플 소셜 로그인 추가
  • 로그아웃 및 회원 탈퇴 추가
-- 1. provider 컬럼 추가 (기존 kakao_id가 있던 위치 근처에 생성)
ALTER TABLE tb_users ADD COLUMN provider VARCHAR(20) NOT NULL DEFAULT 'KAKAO' AFTER id;

-- 2. 기존 social_id가 비어있고 kakao_id에 값이 있다면 이관 (필요한 경우만 실행)
-- UPDATE tb_users SET social_id = kakao_id WHERE social_id = '' OR social_id IS NULL;

-- 3. 기존 개별 유니크 제약 조건 삭제 (일반적으로 uk_..., 또는 컬럼명과 동일)
-- social_id가 이미 unique였다면 삭제해야 복합 키 등록 시 충돌이 안 납니다.
ALTER TABLE tb_users DROP INDEX uk_users_social_id; -- 실제 이름 확인 필요
ALTER TABLE tb_users DROP INDEX kakao_id; -- 기존 kakao_id 유니크 삭제

-- 4. 더 이상 사용하지 않는 컬럼 삭제
ALTER TABLE tb_users DROP COLUMN kakao_id;

-- 5. (provider, social_id) 복합 유니크 제약 조건 추가
ALTER TABLE tb_users ADD CONSTRAINT uk_user_social UNIQUE (provider, social_id);

Check List

  • Assignees 등록을 하였나요?
  • 라벨(Label) 등록을 하였나요?
  • PR 머지하기 전 반드시 CI가 정상적으로 작동하는지 확인해주세요!

Summary by CodeRabbit

  • New Features
    • Apple Sign-In 도입 및 Apple/Kakao 통합 로그인 지원
    • 로그아웃 API 및 회원 탈퇴 API 추가
  • Chores
    • JWT 라이브러리 중앙 버전 관리 및 의존성 추가
    • HTTP 클라이언트(RestTemplate) 및 타임아웃 설정 구성 추가
    • 인증 모델을 provider/socialId 기반으로 개편, DB 제약·엔티티 관계 정비
    • Kakao 관리키 기반 unlink 및 Apple OAuth 연동 설정 추가
  • Tests
    • 인증 서비스·컨트롤러 관련 단위/통합 테스트 신규 추가·보강

✏️ Tip: You can customize this high-level summary in your review settings.

@parkmineum parkmineum linked an issue Jan 28, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Warning

Rate limit exceeded

@parkmineum has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 20 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Apple OAuth 클라이언트·모델·설정과 Apple/Kakao 로그인 서비스가 추가되었고, User 도메인이 provider+socialId 기반으로 변경되었습니다. 인증 컨트롤러에 Apple 로그인·로그아웃·탈퇴 엔드포인트가 추가되었으며 jjwt 의존성과 RestTemplate 빈이 도입되고 관련 리포지토리·매퍼·테스트가 갱신되었습니다.

Changes

코호트 / 파일(s) 변경 요약
빌드 설정
build.gradle.kts, ssolv-api-core/build.gradle.kts, ssolv-infrastructure/build.gradle.kts, ssolv-api-common/build.gradle.kts
프로젝트 루트에 extra["jjwtVersion"] 추가 및 각 서브모듈에서 jjwtVersion 사용해 jjwt-api/jjwt-impl/jjwt-jackson 의존성 선언 추가/통일
도메인·리포지토리·매퍼
ssolv-domain/src/main/kotlin/.../AuthProvider.kt, .../User.kt, .../UserQueryRepository.kt, ssolv-infrastructure/src/main/kotlin/.../auth/UserEntity.kt, .../auth/UserJpaRepository.kt, .../auth/UserQueryRepositoryImpl.kt, .../mapper/UserMapper.kt
kakaoId 제거 → provider: AuthProvidersocialId 도입, DB 유니크 제약(uk_user_social) 추가, findByProviderAndSocialId 조회 메서드/매핑/엔티티·리포지토 변경
Apple OAuth (클라이언트·모델·설정)
ssolv-infrastructure/src/main/kotlin/.../client/AppleOAuthClient.kt, .../model/AppleResponse.kt, .../properties/AppleProperties.kt, ssolv-domain/src/main/kotlin/.../command/AppleLoginCommand.kt
Apple 토큰 요청·ID 토큰 검증(JWK), ES256 client_secret 생성, Apple 응답 모델·프로퍼티·명령 객체 추가 및 관련 예외 매핑
Apple 로그인 서비스·사용자 처리
ssolv-api-core/src/main/kotlin/.../application/login/AppleOAuthService.kt, .../CreateAppleUserService.kt
Apple 로그인 흐름 구현: 토큰/ID 토큰 파싱, optional user JSON 파싱, 사용자 생성/조회 및 토큰 발급 후 LoginResponse 반환
Kakao OAuth 리팩토링
ssolv-infrastructure/src/main/kotlin/.../client/KakaoOAuthClient.kt, .../properties/KakaoProperties.kt, ssolv-api-core/src/main/kotlin/.../application/login/CreateKakaoUserService.kt, .../KakaoLoginService.kt
RestTemplate 주입, redirectUri 처리 보강, accessCode 원본 사용, unlink(socialId) 추가, adminKey 프로퍼티 추가, provider 기반 사용자 조회로 변경 및 클래스/패키지 정리(명칭 변경)
인증 컨트롤러·엔드포인트
ssolv-api-core/src/main/kotlin/.../auth/controller/AuthController.kt
KakaoLoginServiceAppleOAuthService 주입으로 교체/추가, /apple-login POST, /logout POST, /withdraw DELETE 엔드포인트 추가 및 관련 서비스 주입 변경
로그아웃·회원탈퇴 서비스
ssolv-api-core/src/main/kotlin/.../application/common/LogoutService.kt, .../WithdrawService.kt
LogoutService 추가: refreshToken 제거 및 저장. WithdrawService 추가: 호스트 회의 검증, 트랜잭션 내 로컬 사용자 삭제, 소셜 unlink 시도(실패 로깅)
엔티티 관계·타임스탬프·설정
ssolv-infrastructure/src/main/kotlin/.../auth/UserEntity.kt, .../meeting/MeetingEntity.kt, .../common/BaseTimeEntity.kt, .../config/RestTemplateConfig.kt
UserEntity에 attendance 관계 및 cascade/orphanRemoval 설정, Meeting attendees에 cascade/orphanRemoval 추가, createdAt를 var로 변경(보호된 setter), RestTemplate Bean 추가(타임아웃 설정)
에러 코드
ssolv-global-utils/src/main/kotlin/.../exception/ErrorCode.kt
탈퇴 제한 코드 추가(CANNOT_WITHDRAW_WITH_ACTIVE_MEETINGS) 및 Apple 관련 ErrorCode 항목(O011~O019) 추가
테스트 변경·추가
ssolv-api-core/src/test/kotlin/.../CreateAppleUserServiceTest.kt, LogoutServiceTest.kt, WithdrawServiceTest.kt, KakaoLoginServiceTest.kt, AuthControllerTest.kt, UpdateTokenServiceTest.kt, ssolv-api-core/src/test/kotlin/.../util/TestDataFactory.kt, .../meeting/MeetingTestDataFactory.kt, ssolv-infrastructure/src/test/kotlin/.../KakaoOAuthClientTest.kt
Apple 관련 테스트 추가 및 기존 테스트 업데이트, Logout/Withdraw 테스트 추가, 테스트 팩토리 시그니처 kakaoIdprovider: AuthProvider 변경, Kakao client 테스트에 RestTemplate 인자 추가 및 테스트명/변수명 정리

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 분

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 Apple OAuth 기능 구현이라는 주요 변경사항을 명확하게 요약하고 있으며, PR의 핵심 의도를 잘 나타냅니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#144_apple_login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@parkmineum parkmineum self-assigned this Jan 28, 2026
@parkmineum parkmineum requested a review from sunwon12 January 28, 2026 18:23
@parkmineum parkmineum changed the title [FEATURE] 애플 소셜 로그인 기능 추가 [FEATURE] Apple OAuth 기능 구현 Jan 28, 2026
@github-actions
Copy link

github-actions bot commented Jan 28, 2026

🧪 테스트 결과

158 tests   158 ✅  26s ⏱️
 35 suites    0 💤
 35 files      0 ❌

Results for commit ff43504.

♻️ This comment has been updated with latest results.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In
`@ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/common/LogoutService.kt`:
- Around line 19-29: The logout flow sets updatedAt on the domain via
LogoutService.logout (creating loggedOutUser) but the value isn't persisted
because UserMapper.toEntity ignores domain.updatedAt; update UserMapper.toEntity
to map the domain.updatedAt into the UserEntity constructor (handle
nullability/defaults as appropriate) so the timestamp persists, and audit other
mappers/usages (e.g., CreateAppleUserService, CreateKakaoUserService) to ensure
they also pass domain.updatedAt through to their entity conversions.

In
`@ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/common/WithdrawService.kt`:
- Around line 21-35: In WithdrawService.withdraw the external
kakaoOAuthClient.unlink call occurs inside the `@Transactional` scope (after
userQueryRepository.findById and before userCommandRepository.delete); refactor
so the DB delete happens inside the transaction (call
userCommandRepository.delete within the transactional method) and then perform
kakaoOAuthClient.unlink outside that transaction (e.g., return from withdraw and
invoke an externalUnlink method, publish a domain event, or call an
async/retry-enabled unlink handler) and ensure unlink failures are logged and
retried or recorded for later reconciliation.

In
`@ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/login/AppleOAuthService.kt`:
- Line 45: The log statement in AppleOAuthService (log.info("애플 로그인 - socialId:
{}, email: {}, nickname: {}", socialId, email, nickname)) exposes PII; change it
to avoid plaintext socialId/email at INFO level by either removing them entirely
or logging only masked/hashed versions (e.g., SHA-256 or show only last 4 chars)
and drop to debug level (log.debug) for any non-essential details; ensure any
nickname is either non-identifying or similarly masked before logging and update
the log call accordingly.

In
`@ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/login/CreateAppleUserService.kt`:
- Around line 45-55: The current findOrCreateUser uses an email fallback which
can create duplicate accounts for Apple Sign In (email is only provided on first
login); change findOrCreateUser to always lookup by provider+socialId via
userQueryRepository.findByProviderAndSocialId(AuthProvider.APPLE, socialId) and
remove the email-based fallback (userQueryRepository.findByEmail) from the
lookup path; instead, when calling createNewUser(email, nickname, profileImage,
socialId) ensure that createNewUser persists the AuthProvider and socialId with
the new User and only stores the email as an attribute (not as a lookup key), or
limit email-based association to an explicit first-login flow if you have a
reliable flag—this ensures socialId is the sole trust anchor for Apple users and
prevents duplicate accounts.

In
`@ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/login/CreateKakaoUserService.kt`:
- Around line 48-51: 현재 CreateKakaoUserService의 사용자 조회
로직(userQueryRepository.findByProviderAndSocialId(...) ?:
userQueryRepository.findByEmail(email))은 이메일 기반 폴백으로 인해 다른 소셜 프로바이더 계정과 잘못 연결될 수
있으니, 이메일 폴백을 제거하고 provider+socialId 조회만 사용하도록 수정하거나(즉 existingUser =
userQueryRepository.findByProviderAndSocialId(AuthProvider.KAKAO, socialId)만 사용)
이메일이 이미 다른 프로바이더로 존재하는 경우에는 createNewUser(...)를 바로 호출하지 말고 계정 연동 확인 또는 예외를 발생시키는
흐름을 추가하라; 참조 심볼: CreateKakaoUserService,
userQueryRepository.findByProviderAndSocialId, userQueryRepository.findByEmail,
createNewUser.

In
`@ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/login/KakaoLoginService.kt`:
- Around line 30-36: KakaoLoginService currently maps a null Kakao email to an
empty string which breaks the UserEntity unique constraint; change the null
handling so that when kakaoProfile.kakao_account.email is null you generate a
unique placeholder (e.g., UUID-based string like "<uuid>@kakao.placeholder") and
pass that into createKakaoUserService.saveUserAndGenerateTokens instead of "",
ensure the placeholder format is deterministic/unique per user and acceptable to
your email validation rules, and update createNewUser /
userQueryRepository.findByEmail assumptions if they expect real emails (or
alternatively make email nullable in UserEntity and adjust uniqueness logic to
use provider+socialId in find/create flows).

In `@ssolv-infrastructure/build.gradle.kts`:
- Around line 48-52: Upgrade the hardcoded io.jsonwebtoken dependencies
(io.jsonwebtoken:jjwt-api, jjwt-impl, jjwt-jackson) from 0.12.6 to 0.13.0 in
both modules to mitigate the Bouncy Castle CVE; then centralize the version by
adding a single jjwt entry in the Version Catalog (gradle/libs.versions.toml) or
buildSrc and replace the literal "0.12.6" usages with the catalog/buildSrc
reference in both build.gradle.kts files so all modules consume the same
version.

In
`@ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/auth/client/AppleOAuthClient.kt`:
- Around line 30-54: getAllowedRedirectUris() currently only merges
hardcodedUris with appleProperties.redirectUris (a collection), missing the
single-value appleProperties.redirectUri, which can cause valid redirects to be
rejected in requestToken(); update getAllowedRedirectUris() to also include
appleProperties.redirectUri (if non-null/non-blank) so the returned Set contains
that single configured URI as well as redirectUris, referencing the
getAllowedRedirectUris(), appleProperties.redirectUri,
appleProperties.redirectUris, and requestToken() symbols when making the change.
- Around line 122-138: The parseIdToken function currently only Base64-decodes
the payload; update parseIdToken( idToken: String ) to validate the JWT
signature using Apple's JWKS (https://appleid.apple.com/auth/keys) and then
verify standard claims before converting to AppleResponse.IdTokenPayload: fetch
and cache JWKS, select the key by the token's header.kid, verify the token
signature, ensure iss == "https://appleid.apple.com", aud equals your configured
clientId, and exp is in the future; only after successful verification
deserialize the payload with objectMapper.readValue and propagate AuthException
on any verification failure (preserve existing exception handling behavior).
- Around line 59-83: The code in AppleOAuthClient currently creates a new
RestTemplate per request (variable restTemplate) without timeouts; inject a
RestTemplateBuilder into the AppleOAuthClient (constructor) and use it to build
a reusable RestTemplate bean with explicit connect and read timeouts, replace
the local new RestTemplate() usage with the injected/built RestTemplate, and
ensure AppleOAuthClient uses that instance when calling
restTemplate.exchange(...) to avoid indefinite blocking on Apple API calls.
🧹 Nitpick comments (4)
ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/auth/properties/AppleProperties.kt (1)

6-16: Spring Boot 권장 패턴 및 설정 검증을 고려해 주세요.

현재 @Component + @ConfigurationProperties 조합을 사용하고 있는데, Spring Boot 2.2+ 에서는 @ConfigurationPropertiesScan 또는 @EnableConfigurationProperties를 권장합니다. 또한 필수 설정값에 대한 검증이 없어 애플리케이션 시작 시 빈 값으로 실행될 수 있습니다.

privateKey는 민감 정보이므로 로깅에 노출되지 않도록 주의가 필요합니다.

♻️ 검증 추가 제안
+import jakarta.validation.constraints.NotBlank
+import org.springframework.validation.annotation.Validated

 `@Component`
+@Validated
 `@ConfigurationProperties`(prefix = "apple")
 data class AppleProperties(
-    var teamId: String = "",
-    var keyId: String = "",
-    var clientId: String = "",
+    `@field`:NotBlank var teamId: String = "",
+    `@field`:NotBlank var keyId: String = "",
+    `@field`:NotBlank var clientId: String = "",
     var redirectUri: String = "",
     var redirectUris: List<String> = emptyList(),
-    var privateKey: String = "",
+    `@field`:NotBlank var privateKey: String = "",
     var tokenUri: String = "https://appleid.apple.com/auth/token"
 )
ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/login/CreateKakaoUserService.kt (1)

74-89: 토큰 생성 및 프로필 업데이트 로직이 적절합니다.

프로필 이미지 변경 여부를 확인하고 조건부로 업데이트하는 로직이 명확합니다. user.id!!createNewUser에서 저장된 사용자를 반환하므로 안전하지만, 방어적으로 requireNotNull(user.id)를 사용하면 더 명확한 에러 메시지를 제공할 수 있습니다.

ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/auth/properties/KakaoProperties.kt (1)

13-14: adminKey 누락을 조기 검증하세요.
Line 13-14에서 adminKey 기본값이 빈 문자열이라 설정 누락 시 런타임에서만 실패할 가능성이 큽니다. unlink 기능을 쓴다면 @Validated + @NotBlank 등으로 fail-fast 검증을 두는 것이 안전합니다.

✅ 예시
+import org.springframework.validation.annotation.Validated
+import jakarta.validation.constraints.NotBlank
 
 `@Component`
 `@ConfigurationProperties`(prefix = "kakao")
+@Validated
 data class KakaoProperties(
     var clientId: String = "",
     var redirectUri: String = "",
     var redirectUris: List<String> = emptyList(),
     var tokenUri: String = "https://kauth.kakao.com/oauth/token",
     var userInfoUri: String = "https://kapi.kakao.com/v2/user/me",
-    var adminKey: String = ""
+    `@field`:NotBlank
+    var adminKey: String = ""
 )
ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/auth/client/KakaoOAuthClient.kt (1)

187-197: 예외를 삼키면 탈퇴 실패 원인 파악이 어려워질 수 있습니다.

현재 구현은 카카오 연결 끊기 실패 시 로그만 남기고 진행하는데, 이는 의도된 설계로 이해됩니다. 하지만 운영 환경에서 탈퇴 관련 이슈 발생 시 원인 파악이 어려울 수 있습니다.

성공/실패 여부를 반환하거나, 실패 시 메트릭을 수집하는 방안을 고려해 보세요.

♻️ 개선 제안
-    fun unlink(socialId: String) {
+    fun unlink(socialId: String): Boolean {
         val restTemplate = RestTemplate()
         val headers = HttpHeaders().apply {
             add("Content-Type", "application/x-www-form-urlencoded")
             add("Authorization", "KakaoAK ${kakaoProperties.adminKey}")
         }

         val params: MultiValueMap<String, String> = LinkedMultiValueMap<String, String>().apply {
             add("target_id_type", "user_id")
             add("target_id", socialId)
         }

         val request = HttpEntity(params, headers)

-        try {
+        return try {
             restTemplate.exchange(
                 "https://kapi.kakao.com/v1/user/unlink",
                 HttpMethod.POST,
                 request,
                 String::class.java
             )
+            true
         } catch (e: Exception) {
             log.error("카카오 연결 끊기 실패 - socialId: {}, error: {}", socialId, e.message)
-            // 탈퇴 과정이므로 에러가 발생해도 로컬 데이터 삭제는 진행할 수 있도록 예외를 던지지 않거나 로그만 남김
+            false
         }
     }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@ssolv-api-common/build.gradle.kts`:
- Around line 19-21: The build references the version-catalog accessor
libs.jjwt.api but no version catalog is configured; either add a
gradle/libs.versions.toml that defines JJWT entries (e.g., jjwt-api/impl/jackson
coordinates and version) and enable the catalog and TYPESAFE_PROJECT_ACCESSORS
in settings.gradle.kts (call enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
and configureVersionCatalogs if needed), or replace the libs.* usages in
build.gradle.kts with hard-coded dependency strings like
"io.jsonwebtoken:jjwt-api:0.12.3" / "io.jsonwebtoken:jjwt-impl:0.12.3" /
"io.jsonwebtoken:jjwt-jackson:0.12.3" so the build no longer relies on a missing
libs accessor (update references to libs.jjwt.api, libs.jjwt.impl,
libs.jjwt.jackson accordingly).

In `@ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/auth/UserEntity.kt`:
- Around line 39-43: The UserEntity.meetings mapping currently uses cascade =
[CascadeType.ALL] and orphanRemoval = true which causes MeetingEntity rows (and
thus dangling MeetingAttendeeEntity records) to be deleted when a host user is
removed; change the mapping to remove CascadeType.ALL and orphanRemoval on
meetings, and instead handle host-user deletion explicitly in the user deletion
flow: in the service that removes a UserEntity, check MeetingEntity.attendees
for each hosted meeting and either transfer hosting (assign hostUser to another
attendee), prevent deletion if attendees exist, or perform a controlled delete
that first removes MeetingAttendeeEntity rows then the MeetingEntity, ensuring
referential integrity (reference symbols: UserEntity.meetings, MeetingEntity,
MeetingAttendeeEntity, MeetingEntity.attendees).

In
`@ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/common/BaseTimeEntity.kt`:
- Around line 13-18: BaseTimeEntity exposes mutable timestamp fields which
allows external code to overwrite createdAt/updatedAt; change declarations to
make setters non-public (e.g., keep properties as var createdAt: LocalDateTime =
LocalDateTime.now() private set and var updatedAt: LocalDateTime? = null private
set) so only the entity itself (e.g., its updateTimestamp() method) can modify
them, leaving getters public for reads and preserving JPA mapping on the same
properties.

In
`@ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/mapper/UserMapper.kt`:
- Around line 33-36: The mapping sets entity.createdAt = domain.createdAt
directly in the apply block, which can violate BaseTimeEntity's non-null DB
constraint if domain.createdAt is null; update the mapper (the apply block in
UserMapper.kt where createdAt and updatedAt are assigned) to guard against null
by using a null-check or default (e.g., use domain.createdAt ?: Instant.now() or
only set when non-null) so createdAt is never assigned null, and do the same
consideration for updatedAt as appropriate.
🧹 Nitpick comments (2)
ssolv-api-core/build.gradle.kts (1)

15-17: ssolv-api-core에서 불필요한 jjwt 의존성 선언 제거 권장

ssolv-api-core는 jjwt 클래스를 직접 사용하지 않습니다. UpdateTokenService, CreateAppleUserService 등의 클래스들은 모두 내부 추상화 계층인 JwtTokenProvider를 통해서만 토큰 처리를 수행합니다.

ssolv-api-common에서 이미 api() 스코프로 jjwt를 선언하고 있으며, ssolv-api-core는 이 모듈에 의존하므로 jjwt를 transitively 사용할 수 있습니다. 따라서 lines 15-17의 jjwt 의존성 선언은 불필요합니다.

제거 권장 코드
    implementation(libs.jjwt.api)
    runtimeOnly(libs.jjwt.impl)
    runtimeOnly(libs.jjwt.jackson)
ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/login/CreateKakaoUserService.kt (1)

43-51: 동일 이메일의 타 프로바이더 계정 처리 정책 검토 권장

현재 UserEntityemail 컬럼에는 UNIQUE 제약이 없으므로 (provider, socialId만 UNIQUE), 기술적으로 동일 이메일의 신규 사용자 생성은 데이터베이스 제약으로 인한 실패 위험이 없습니다.

다만 보안 및 사용자 경험 관점에서 다른 프로바이더의 동일 이메일 계정이 있는 경우, 자동 병합 없이 사용자에게 명확한 안내를 제공하는 것이 권장됩니다.

🔧 선택적 개선 예시
     private fun findOrCreateUser(
         email: String,
         nickname: String,
         profileImage: String?,
         socialId: String
     ): User {
         val existingUser = userQueryRepository.findByProviderAndSocialId(AuthProvider.KAKAO, socialId)
+        if (existingUser == null) {
+            val emailOwner = userQueryRepository.findByEmail(email)
+            if (emailOwner != null && emailOwner.provider != AuthProvider.KAKAO) {
+                throw IllegalStateException("EMAIL_IN_USE_BY_OTHER_PROVIDER")
+            }
+        }
         return existingUser ?: createNewUser(email, nickname, profileImage, socialId)
     }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/common/WithdrawService.kt`:
- Around line 79-86: deleteLocalUser() is annotated with `@Transactional` but is
invoked via self-invocation from withdraw(), so Spring AOP proxying is skipped;
move the transactional deletion into a separate `@Service` bean (e.g.,
UserDeletionService) with a method deleteLocalUser(User) annotated with
`@Transactional`, inject that service into WithdrawService and replace the
internal call to deleteLocalUser(user) with
userDeletionService.deleteLocalUser(user) so the proxy applies and cascade
deletions run inside a real transaction; alternatively, if you prefer
programmatic transactions, use a TransactionTemplate in WithdrawService to
execute the deletion logic inside a transaction (or register a
TransactionSynchronizationManager callback to run unlink logic after commit).
🧹 Nitpick comments (2)
ssolv-global-utils/src/main/kotlin/org/depromeet/team3/common/exception/ErrorCode.kt (1)

67-76: 주석 범위와 실제 코드 불일치

주석에 "O011O020"이라고 되어 있지만 실제 에러 코드는 O019까지만 정의되어 있습니다. 주석을 "O011O019"로 수정하거나, O020을 예약해둔 것이라면 그 의도를 명시해주세요.

-    // Apple OAuth 관련 에러 (O011~O020)
+    // Apple OAuth 관련 에러 (O011~O019)
ssolv-api-core/src/main/kotlin/org/depromeet/team3/auth/application/common/WithdrawService.kt (1)

62-77: N+1 쿼리 문제 가능성

모임 목록을 조회한 후 각 모임별로 참석자를 개별 쿼리하면 모임 수만큼 추가 쿼리가 발생합니다. 모임 수가 많은 사용자의 탈퇴 시 성능 저하가 발생할 수 있습니다.

리포지토리에 다른 참석자가 있는 호스팅 모임을 한 번에 확인하는 메서드를 추가하는 것을 권장합니다.

♻️ 단일 쿼리로 최적화하는 예시
// MeetingRepository에 추가
fun hasHostedMeetingsWithOtherAttendees(userId: Long): Boolean

// WithdrawService에서 사용
private fun validateHostedMeetings(userId: Long) {
    if (meetingRepository.hasHostedMeetingsWithOtherAttendees(userId)) {
        log.warn("탈퇴 시도 실패 - userId: {}, 다른 참석자가 있는 모임 호스팅 중", userId)
        throw AuthException(ErrorCode.CANNOT_WITHDRAW_WITH_ACTIVE_MEETINGS)
    }
}

@parkmineum parkmineum merged commit ef90490 into dev Jan 28, 2026
3 checks passed
@parkmineum parkmineum added the ✨ FEATURE 기능 구현 관련 라벨 label Feb 3, 2026
@parkmineum parkmineum changed the title [FEATURE] Apple OAuth 기능 구현 [FEATURE] 애플 소셜 로그인 구현 Feb 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ FEATURE 기능 구현 관련 라벨

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] 애플 소셜 로그인 추가

1 participant