Skip to content

feat: 구글 가입 약관 동의 흐름을 signupToken 기반으로 분리#140

Merged
parkjuyeong0312 merged 6 commits into
devfrom
refactor/login-agreement
Jun 9, 2026
Merged

feat: 구글 가입 약관 동의 흐름을 signupToken 기반으로 분리#140
parkjuyeong0312 merged 6 commits into
devfrom
refactor/login-agreement

Conversation

@parkjuyeong0312

@parkjuyeong0312 parkjuyeong0312 commented Jun 8, 2026

Copy link
Copy Markdown
Member

변경 내용

  • POST /auth/google/login 응답을 GoogleLoginResponse로 타입화하고, 신규 사용자가 agreementsAccepted=true를 보내지 않으면 쿠키 없이 SIGNUP_REQUIRED + 10분짜리 signupToken을 반환하도록 분기 추가
  • POST /auth/google/signup 엔드포인트 신설 — signupToken + agreementsAccepted=true를 받아 신규 가입을 완료하고 인증 쿠키 발급
  • GoogleSignupTokenService: Redis Lua 스크립트로 user → token 매핑과 token → payload 저장을 원자적으로 처리, 이전 토큰은 즉시 폐기 (TTL 10분)
  • GoogleSignupTokenNotFound 에러 코드, SecurityConfig/auth/google/signup permitAll 추가, LoggingAspect의 민감 파라미터 셋에 signupToken 등록(로그 마스킹)
  • docs/ai/features.md "가입 약관 동의 기록" 항목 갱신 및 설계/계획 문서(docs/superpowers/specs, docs/superpowers/plans) 추가

변경 이유

  • Google OAuth code는 1회용이므로 신규 사용자가 약관 미동의 상태로 로그인하면 재요청 시 같은 code를 사용할 수 없어 가입 자체가 막혔다.
  • 로그인/가입 흐름을 분리해 약관 동의 UX 동안 code 재사용 문제를 해소하고, 가입 임시 토큰을 Redis에 짧게 보관해 보안 노출을 최소화했다.

테스트

  • ./gradlew build
  • /review-code-against-docs 스킬로 검증
  • 그 외 수동 검증: AuthServiceTest, AuthControllerTest, GoogleSignupTokenServiceTest, AuthIntegrationTest에서 신규 가입 분기 / 재발급 토큰 폐기 / Redis Lua 원자성 / SIGNUP_REQUIRED 응답을 단위·통합 테스트로 검증

체크리스트

  • PR 제목이 커밋 컨벤션 형식을 따른다.
  • 변경 사유를 PR 설명에 기록했다.
  • 테스트 방법과 결과를 기록했다.
  • 문서 변경이 필요한 경우 반영했다. (features.md, docs/superpowers/specs, docs/superpowers/plans)

하네스 변경 체크리스트

  • CLAUDE.md(AGENTS.md) 변경이 포함되어 있는가? — 해당 없음
  • 변경 사유가 PR 설명에 기록되어 있는가? — 해당 없음
  • 기존 규칙과 충돌하지 않는가? — 해당 없음
  • 팀원에게 변경 사항을 공유했는가? — 해당 없음

Summary by CodeRabbit

  • New Features

    • Google 로그인 흐름이 개선되어 신규 사용자는 약관 동의 후 가입을 완료하는 절차가 추가되었습니다. 약관을 즉시 동의하면 바로 가입/인증이 진행됩니다.
    • 가입 대기용 임시 signup 토큰(유효시간 10분)을 발급하며, 같은 계정의 재발급 시 이전 토큰은 무효화됩니다. 토큰 누락/만료 시 401 응답이 반환됩니다.
  • Documentation

    • Google 로그인·가입 및 임시 토큰 설계·운영 문서가 추가·갱신되었습니다.
  • Tests

    • 관련 통합·단위 테스트가 추가되어 가입 흐름과 토큰 동작을 검증합니다.

- LoggingAspect: SENSITIVE_PARAM_NAMES에 signupToken 추가
- GoogleSignupTokenService.create: 이전 토큰 무효화 + 신규 토큰 저장을 Lua 스크립트로 원자화하여 동일 providerId 동시 발급 race 제거
- GoogleSignupTokenService.consume: payload deserialize 실패 시 warn 로그 추가하여 운영 진단성 확보
- GoogleSignupTokenServiceTest: 손상된 JSON payload에 대한 consume 거부 테스트 추가
- AuthController.googleLogin: ResponseEntity<?> → ResponseEntity<GoogleLoginResponse>로 타입화하여 @apiresponse schema와 일치
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 665a33d0-ef88-48d6-90e6-15214396f7ff

📥 Commits

Reviewing files that changed from the base of the PR and between f2dae0d and 36f9f79.

📒 Files selected for processing (1)
  • docs/ai/features.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/ai/features.md

📝 Walkthrough

Walkthrough

Google OAuth 신규 사용자 약관 미동의 시 Redis 기반 임시 signup token(10분)을 발급하고, 프런트가 약관 동의 후 해당 토큰으로 POST /auth/google/signup을 호출해 가입을 완료하도록 흐름을 추가했습니다.

Changes

Google OAuth Signup Token 플로우

Layer / File(s) Summary
설계 및 구현 계획 문서화
docs/ai/features.md, docs/superpowers/plans/2026-06-08-google-signup-token.md, docs/superpowers/specs/2026-06-08-google-signup-token-design.md
기능 목표, API 계약, Redis 키·TTL·payload 구조, 테스트 전략 및 실행 계획을 문서화하고 기능 명세를 갱신
응답/요청 DTO 및 내부 결과 타입
src/main/java/.../GoogleLoginResponse.java, GoogleSignupRequest.java, src/main/java/.../GoogleLoginOutcome.java, GoogleSignupPayload.java, src/main/java/.../GoogleLoginRequest.java
Google 로그인 응답/가입 요청 DTO와 서비스 내부 결과 타입을 추가/수정하여 SIGNUP_REQUIRED 분기와 토큰 만료 정보를 포함
GoogleSignupTokenService 구현
src/main/java/com/howaboutus/backend/auth/service/GoogleSignupTokenService.java
Redis 기반 임시 signup token 생성(create)/소비(consume) 로직 구현: UUID 토큰, JSON 직렬화/역직렬화, Lua 스크립트로 providerId별 최신 토큰 관리, 10분 TTL
AuthService 로그인 및 가입 로직 변경
src/main/java/com/howaboutus/backend/auth/service/AuthService.java
googleLogin 반환타입을 GoogleLoginOutcome으로 변경해 신규 사용자 약관 미동의 시 signupRequired 분기 처리 추가, googleSignup(signupToken, agreementsAccepted) 메서드로 토큰 소비 후 사용자 생성·토큰 발급 수행
AuthController 응답 및 OpenAPI 갱신
src/main/java/com/howaboutus/backend/auth/controller/AuthController.java
googleLogin의 ResponseEntity 제네릭을 GoogleLoginResponse로 변경하고 outcome에 따라 본문(가입 필요) 또는 쿠키(인증)로 응답 처리, OpenAPI 설명 갱신
보안·에러·로깅 설정
src/main/java/com/howaboutus/backend/common/config/SecurityConfig.java, src/main/java/com/howaboutus/backend/common/error/ErrorCode.java, src/main/java/com/howaboutus/backend/common/logging/LoggingAspect.java
/auth/google/signup 경로 permitAll 추가, GOOGLE_SIGNUP_TOKEN_NOT_FOUND(401) 에러 코드 추가, signupToken 파라미터 로깅 마스킹
컨트롤러 단위 테스트
src/test/java/com/howaboutus/backend/auth/controller/AuthControllerTest.java
GoogleLoginOutcome 기반으로 컨트롤러 테스트 갱신: SIGNUP_REQUIRED 응답, signup 성공 시 쿠키 발급, 토큰 누락/만료 시 401 검증
서비스 단위 테스트
src/test/java/com/howaboutus/backend/auth/service/AuthServiceTest.java
AuthService 테스트를 outcome 중심으로 갱신, agreementsAccepted=false 시 signupRequired 반환 및 의존성 미호출 검증, googleSignup 로직 검증 추가
GoogleSignupTokenService 통합 테스트
src/test/java/com/howaboutus/backend/auth/service/GoogleSignupTokenServiceTest.java
토큰 생성·소비·TTL(약 10분)·재발급 무효화·손상 payload 처리 등 통합 테스트 추가
로그인→가입 통합 테스트
src/test/java/com/howaboutus/backend/auth/AuthIntegrationTest.java
신규 사용자 로그인 후 signup token 수령 → signup 완료 흐름과 동일 providerId 재발급 시 이전 토큰 무효화 검증

Sequence Diagram

sequenceDiagram
  participant Client
  participant AuthController
  participant AuthService
  participant GoogleSignupTokenService
  participant Redis
  Client->>AuthController: POST /auth/google/login<br/>code, agreementsAccepted=false
  AuthController->>AuthService: googleLogin(code, false)
  AuthService->>AuthService: Google OAuth 코드로 계정 조회
  AuthService->>AuthService: 신규 사용자 확인
  AuthService->>GoogleSignupTokenService: create(GoogleSignupPayload)
  GoogleSignupTokenService->>GoogleSignupTokenService: UUID 토큰 생성
  GoogleSignupTokenService->>Redis: Lua: 기존 토큰 삭제 후<br/>token → payload 저장<br/>providerId → token 저장<br/>TTL 10분 설정
  GoogleSignupTokenService-->>AuthService: signupToken 반환
  AuthService-->>AuthController: GoogleLoginOutcome.signupRequired()
  AuthController-->>Client: 200 OK<br/>{ status: SIGNUP_REQUIRED,<br/>signupToken,<br/>expiresInSeconds: 600 }<br/>쿠키 없음
  Client->>AuthController: POST /auth/google/signup<br/>signupToken, agreementsAccepted=true
  AuthController->>AuthService: googleSignup(signupToken, true)
  AuthService->>GoogleSignupTokenService: consume(signupToken)
  GoogleSignupTokenService->>Redis: GET & DELETE token 키
  GoogleSignupTokenService->>GoogleSignupTokenService: Payload JSON 역직렬화
  GoogleSignupTokenService->>Redis: 최신 토큰 확인<br/>providerId → token 조회
  alt 최신 토큰 일치
    GoogleSignupTokenService->>Redis: providerId 키 삭제
    GoogleSignupTokenService-->>AuthService: GoogleSignupPayload 반환
  else 토큰 불일치
    GoogleSignupTokenService-->>AuthService: CustomException<br/>GOOGLE_SIGNUP_TOKEN_NOT_FOUND
  end
  AuthService->>AuthService: Payload로 사용자 생성<br/>약관 버전/동의 시간 저장
  AuthService->>AuthService: issueTokens(user)
  AuthService-->>AuthController: LoginResult
  AuthController-->>Client: 200 OK<br/>Set-Cookie: access_token<br/>Set-Cookie: refresh_token
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

backend, test

Poem

토끼가 살짝 남긴 UUID 한 알,
Redis 밭에 심어 십 분을 재우네,
약관의 문을 열면 토큰은 꽃이 되어,
쿠키로 빵 굽듯 인증을 내어주네,
테스트로 웃음 짓는 작은 배포의 봄 🐰✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 커밋 컨벤션(feat:) 형식을 따르며, 주요 변경 사항인 '구글 가입 약관 동의 흐름을 signupToken 기반으로 분리'를 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 변경 내용, 변경 이유, 테스트, 체크리스트를 포함하여 템플릿의 주요 항목을 충실히 채우고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/howaboutus/backend/common/config/SecurityConfig.java (1)

46-60: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

웹소켓 permitAll 규칙을 일반 공개 경로와 분리해 주세요.

현재 "/ws/**"가 다른 공개 API와 같은 requestMatchers(...) 블록에 묶여 있습니다. 웹소켓 경로는 별도 matcher 체인으로 분리해 정책 의도를 명확히 해 주세요.

변경 예시
             .authorizeHttpRequests(authorize -> authorize
                 .requestMatchers(
                     "/v3/api-docs/**",
                     "/swagger-ui.html",
                     "/swagger-ui/**",
                     "/springwolf/**",
                     "/actuator/health",
                     "/actuator/prometheus",
                     "/actuator/caches",
                     "/api/agreements/current",
                     "/auth/google/login",
                     "/auth/google/signup",
                     "/auth/refresh",
-                    "/auth/logout",
-                    "/ws/**")
+                    "/auth/logout")
                 .permitAll()
+                .requestMatchers("/ws/**").permitAll()
                 .anyRequest().authenticated())

As per coding guidelines: src/main/java/com/howaboutus/backend/common/**/*Security*.java에서는 WebSocket 엔드포인트를 다른 endpoint permission과 분리해 명시적으로 허용해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/howaboutus/backend/common/config/SecurityConfig.java`
around lines 46 - 60, The SecurityConfig currently bundles "/ws/**" into the
general requestMatchers(...) permitAll list; separate the WebSocket rule into
its own matcher so intent is explicit. Locate the requestMatchers(...) call in
SecurityConfig (e.g., inside the securityFilterChain or configure(HttpSecurity)
method), remove "/ws/**" from that array and add a separate matcher chain for
WebSocket endpoints such as .requestMatchers("/ws/**").permitAll() (placed
appropriately among other matcher rules). Ensure the rest of the existing
permitAll() list remains unchanged and that no other authorization rules are
inadvertently reordered.

Source: Coding guidelines

🧹 Nitpick comments (2)
docs/ai/features.md (1)

55-55: ⚡ Quick win

메인 기능 명세 파일에 구현 상세가 과도하게 들어가 있습니다.

이 줄은 흐름/조건/저장 동작까지 너무 상세해서, 메인 문서 요약 역할을 벗어납니다. docs/ai/ 하위 상세 문서로 분리하고 여기에는 “언제 어떤 문서를 참조할지”만 남겨 주세요.

권장 수정 예시
-| `[x]` | 가입 약관 동의 기록 | Google 인가 코드는 1회만 사용할 수 있으므로 ... 신규 사용자가 첫 로그인 요청부터 `agreementsAccepted=true`를 보내면 즉시 생성한다 | users, Redis |
+| `[x]` | 가입 약관 동의 기록 | Google 신규 가입 약관 동의 흐름은 signupToken 기반으로 분리되며, 상세 시퀀스/에러/TTL은 `docs/ai/auth-google-signup-token.md`를 참고한다 | users, Redis |

As per coding guidelines, "**/*.md: Split detailed explanations into separate documents under docs/ai/, keeping this main file brief with only 'when to read which document' references".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ai/features.md` at line 55, The row describing "`가입 약관 동의 기록`" contains
implementation-level details (flows, cookie behavior, signupToken, endpoints
`POST /auth/google/login` and `POST /auth/google/signup`, storage of `users` and
Redis) that should be moved into a new detailed spec under docs/ai/, leaving
this main feature line as a brief pointer; create a new document (e.g.,
"auth-google-signup.md") with the full flow and storage details, replace the
long cell in the main features table with a short reference like "See
auth-google-signup.md for flow and storage details", and ensure the new doc
documents the `SIGNUP_REQUIRED` response, `signupToken` TTL, agreementsAccepted
handling, and what gets stored in users/Redis.

Source: Coding guidelines

src/test/java/com/howaboutus/backend/auth/AuthIntegrationTest.java (1)

68-68: 💤 Low value

만료 시간 검증에 magic number 대신 상수를 사용하는 것을 고려하세요.

Line 68에서 expiresInSeconds를 600으로 hardcoding하고 있습니다. GoogleSignupTokenService.EXPIRES_IN_SECONDS 상수를 직접 참조하면 유지보수성이 향상됩니다.

♻️ 제안하는 개선
             .andExpect(status().isOk())
             .andExpect(jsonPath("$.status").value("SIGNUP_REQUIRED"))
-            .andExpect(jsonPath("$.expiresInSeconds").value(600))
+            .andExpect(jsonPath("$.expiresInSeconds").value(GoogleSignupTokenService.EXPIRES_IN_SECONDS))
             .andExpect(cookie().doesNotExist("access_token"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/howaboutus/backend/auth/AuthIntegrationTest.java` at line
68, Replace the hardcoded magic number in the test assertion with the constant
from the token service: change the expiresInSeconds expectation in
AuthIntegrationTest to use GoogleSignupTokenService.EXPIRES_IN_SECONDS (or a
static import of it) instead of 600 so the test stays in sync with the
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main/java/com/howaboutus/backend/common/config/SecurityConfig.java`:
- Around line 46-60: The SecurityConfig currently bundles "/ws/**" into the
general requestMatchers(...) permitAll list; separate the WebSocket rule into
its own matcher so intent is explicit. Locate the requestMatchers(...) call in
SecurityConfig (e.g., inside the securityFilterChain or configure(HttpSecurity)
method), remove "/ws/**" from that array and add a separate matcher chain for
WebSocket endpoints such as .requestMatchers("/ws/**").permitAll() (placed
appropriately among other matcher rules). Ensure the rest of the existing
permitAll() list remains unchanged and that no other authorization rules are
inadvertently reordered.

---

Nitpick comments:
In `@docs/ai/features.md`:
- Line 55: The row describing "`가입 약관 동의 기록`" contains implementation-level
details (flows, cookie behavior, signupToken, endpoints `POST
/auth/google/login` and `POST /auth/google/signup`, storage of `users` and
Redis) that should be moved into a new detailed spec under docs/ai/, leaving
this main feature line as a brief pointer; create a new document (e.g.,
"auth-google-signup.md") with the full flow and storage details, replace the
long cell in the main features table with a short reference like "See
auth-google-signup.md for flow and storage details", and ensure the new doc
documents the `SIGNUP_REQUIRED` response, `signupToken` TTL, agreementsAccepted
handling, and what gets stored in users/Redis.

In `@src/test/java/com/howaboutus/backend/auth/AuthIntegrationTest.java`:
- Line 68: Replace the hardcoded magic number in the test assertion with the
constant from the token service: change the expiresInSeconds expectation in
AuthIntegrationTest to use GoogleSignupTokenService.EXPIRES_IN_SECONDS (or a
static import of it) instead of 600 so the test stays in sync with the
implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 01321fb0-48b4-400c-899c-97ccf1ed2191

📥 Commits

Reviewing files that changed from the base of the PR and between 8db5a9b and f2dae0d.

📒 Files selected for processing (18)
  • docs/ai/features.md
  • docs/superpowers/plans/2026-06-08-google-signup-token.md
  • docs/superpowers/specs/2026-06-08-google-signup-token-design.md
  • src/main/java/com/howaboutus/backend/auth/controller/AuthController.java
  • src/main/java/com/howaboutus/backend/auth/controller/dto/GoogleLoginRequest.java
  • src/main/java/com/howaboutus/backend/auth/controller/dto/GoogleLoginResponse.java
  • src/main/java/com/howaboutus/backend/auth/controller/dto/GoogleSignupRequest.java
  • src/main/java/com/howaboutus/backend/auth/service/AuthService.java
  • src/main/java/com/howaboutus/backend/auth/service/GoogleSignupTokenService.java
  • src/main/java/com/howaboutus/backend/auth/service/dto/GoogleLoginOutcome.java
  • src/main/java/com/howaboutus/backend/auth/service/dto/GoogleSignupPayload.java
  • src/main/java/com/howaboutus/backend/common/config/SecurityConfig.java
  • src/main/java/com/howaboutus/backend/common/error/ErrorCode.java
  • src/main/java/com/howaboutus/backend/common/logging/LoggingAspect.java
  • src/test/java/com/howaboutus/backend/auth/AuthIntegrationTest.java
  • src/test/java/com/howaboutus/backend/auth/controller/AuthControllerTest.java
  • src/test/java/com/howaboutus/backend/auth/service/AuthServiceTest.java
  • src/test/java/com/howaboutus/backend/auth/service/GoogleSignupTokenServiceTest.java

@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

@parkjuyeong0312 parkjuyeong0312 merged commit 20c6c8f into dev Jun 9, 2026
4 checks passed
@parkjuyeong0312 parkjuyeong0312 deleted the refactor/login-agreement branch June 9, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant