Skip to content

[feat] #40 - 구글 캘린더 연동/해제#44

Open
Jy000n wants to merge 20 commits into
developfrom
feat/#40-google-calendar
Open

[feat] #40 - 구글 캘린더 연동/해제#44
Jy000n wants to merge 20 commits into
developfrom
feat/#40-google-calendar

Conversation

@Jy000n

@Jy000n Jy000n commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

관련 이슈 🛠

작업 내용 요약 ✏️

구글 OAuth 동의 완료 후 발급된 authorizationCode로 구글 토큰을 교환하여 캘린더를 연동하고, 연동 정보를 삭제 및 구글 토큰을 revoke하는 연동 해제 기능을 구현했습니다.

주요 변경 사항 🛠️

  • [CalendarController]: POST /api/v1/users/calendar(연동), DELETE /api/v1/users/calendar(해제) API 엔드포인트 추가
  • [CalendarService]: 연동/해제 비즈니스 로직 구현 (이미 연동된 경우 예외 처리, 가입 이메일과 구글 인증 이메일 일치 검증, revoke 처리)
  • [CalendarConnection]: calendar_connections 테이블 매핑 엔티티 추가 (토큰 평문 저장)
  • [CalendarConnectionRepository]: findByUserId, existsByUserId 조회 메서드 추가
  • [CalendarConnectRequest / CalendarConnectResponse / CalendarDisconnectResponse]: 연동/해제 API Request, Response DTO 추가
  • [CalendarConnectResponse]: connectedAt 응답 포맷을 yyyy-MM-dd HH:mm:ss로 통일 (@JsonFormat, @Schema 적용)
  • [GoogleOAuthClient]: 구글 토큰 교환(exchangeToken), 사용자 정보 조회(fetchUserInfo), 토큰 revoke(revokeToken) 로직 분리, 기존 spring.security.oauth2.client.registration.google 설정 재사용
  • [GoogleTokenResponse / GoogleUserInfoResponse]: 구글 API 응답 매핑 DTO 추가
  • [CalendarResponseFactory]: AuthResponseFactory 패턴에 맞춰 응답 조립 책임 분리
  • [CalendarControllerDocs]: Swagger 문서화를 위한 인터페이스 추가
  • [CalendarErrorCode]: CALENDAR_401_AUTH_FAILED, CALENDAR_401_EMAIL_MISMATCH, CALENDAR_409_ALREADY_CONNECTED, CALENDAR_404_NOT_CONNECTED 에러 코드 추가
  • [CalendarSuccessCode]: CALENDAR_CONNECTED, CALENDAR_DISCONNECTED 성공 코드 추가

트러블 슈팅 ⚽️

  • 유저가 로그인 계정과 다른 구글 계정으로 연동을 시도할 수 있어, 구글 userinfo API로 실제 인증된 이메일을 조회해 가입 이메일과 일치하는지 검증하는 로직 추가 (다른 계정으로는 연동 불가 정책)
  • 프론트-백엔드 연결 전이라 OAuth Playground를 임시 redirect_uri로 등록해 authorizationCode를 발급받아 테스트 진행함

테스트 결과 📄(진행 중)

  • 정상 케이스: 캘린더 연동 성공, calendarConnected: truecalendarEmail 반환 확인
  • authorizationCode 누락 시 400 에러 반환 확인
  • 유효하지 않은 authorizationCode인 경우 401(CALENDAR_401) 에러 반환 확인
  • 가입 이메일과 다른 구글 계정으로 연동 시도 시 401(이메일 불일치) 에러 반환 확인
  • 이미 연동된 상태에서 재연동 시도 시 409 에러 반환 확인
  • 토큰 없이 요청 시 401 에러 반환 확인
  • 정상 케이스: 캘린더 연동 해제 성공, calendarConnected: false 반환 확인
  • 연동되지 않은 상태에서 해제 시도 시 404 에러 반환 확인
  • 해제 후 DB에서 calendar_connections row 삭제 확인

스크린샷 📷

authorizationCode 누락 시 400 에러 반환 확인

유효하지 않은 authorizationCode인 경우 401(CALENDAR_401) 에러 반환 확인

가입 이메일과 다른 구글 계정으로 연동 시도 시 401(이메일 불일치) 에러 반환 확인

이미 연동된 상태에서 재연동 시도 시 409 에러 반환 확인

토큰 없이 요청 시 401 에러 반환 확인

정상 케이스: 캘린더 연동 해제 성공, calendarConnected: false 반환 확인

연동되지 않은 상태에서 해제 시도 시 404 에러 반환 확인

해제 후 DB에서 calendar_connections row 삭제 확인

리뷰 요구사항 📢

📎 참고 자료 (선택)

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

구글 OAuth 기반 캘린더 연동/해제 기능이 추가되었습니다. GoogleOAuthClient가 토큰 교환·사용자 정보 조회·토큰 리보크를 처리하고, CalendarService가 연결/해제 비즈니스 로직을 수행하며, CalendarController가 API 엔드포인트를 노출합니다. 관련 DTO, 엔티티, 리포지토리, 에러/성공 코드가 함께 추가되었습니다.

Changes

구글 캘린더 연동/해제

Layer / File(s) Summary
에러/성공 코드 및 데이터 계약
.../exception/CalendarErrorCode.java, .../exception/CalendarSuccessCode.java, .../dto/request/CalendarConnectRequest.java, .../dto/response/CalendarConnectResponse.java, .../dto/response/CalendarDisconnectResponse.java, .../dto/response/GoogleTokenResponse.java, .../dto/response/GoogleUserInfoResponse.java, .../entity/CalendarConnection.java, .../repository/CalendarConnectionRepository.java
캘린더 도메인 에러/성공 코드, 요청/응답 레코드, CalendarConnection 엔티티 및 리포지토리가 정의됨.
Google OAuth 클라이언트
.../client/GoogleOAuthClient.java
토큰 교환(exchangeToken), 사용자 정보 조회(fetchUserInfo), 토큰 리보크(revokeToken) 구현, 실패 시 CustomException 발생.
CalendarService 연동/해제 로직
.../service/CalendarService.java
사용자 존재/중복 연결 검사, OAuth 토큰 교환 및 이메일 검증, CalendarConnection 저장/삭제, 토큰 리보크 호출을 포함한 connect/disconnect 구현.
컨트롤러 및 응답 팩토리
.../controller/CalendarController.java, .../factory/CalendarResponseFactory.java
/api/v1/users/calendar POST/DELETE 엔드포인트와 BaseResponse 생성 팩토리 구현.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CalendarController
  participant CalendarService
  participant GoogleOAuthClient
  participant CalendarConnectionRepository

  CalendarController->>CalendarService: connect(userId, request)
  CalendarService->>GoogleOAuthClient: exchangeToken(authorizationCode)
  GoogleOAuthClient-->>CalendarService: GoogleTokenResponse
  CalendarService->>GoogleOAuthClient: fetchUserInfo(accessToken)
  GoogleOAuthClient-->>CalendarService: GoogleUserInfoResponse
  CalendarService->>CalendarConnectionRepository: save(CalendarConnection)
  CalendarConnectionRepository-->>CalendarService: saved entity
  CalendarService-->>CalendarController: CalendarConnectResponse

  CalendarController->>CalendarService: disconnect(userId)
  CalendarService->>CalendarConnectionRepository: findByUserId(userId)
  CalendarConnectionRepository-->>CalendarService: CalendarConnection
  CalendarService->>GoogleOAuthClient: revokeToken(accessToken)
  CalendarService->>CalendarConnectionRepository: delete(connection)
  CalendarService-->>CalendarController: CalendarDisconnectResponse
Loading

Suggested reviewers: aneykrap, laura-jung

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed 연동/해제 API, OAuth 토큰 교환, 사용자 이메일 검증, 저장·삭제 로직이 이슈 #40의 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed 요약된 변경은 모두 구글 캘린더 연동/해제 범위에 속해 보이며 별도 목적의 코드 변경은 보이지 않습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 구글 캘린더 연동/해제라는 핵심 변경을 정확히 담고 있어 PR 내용과 잘 맞습니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#40-google-calendar

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.

@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.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/main/java/com/Timo/Timo/domain/calendar/client/GoogleOAuthClient.java (1)

41-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

catch 블록에서 원본 예외를 로깅하지 않아 OAuth 실패 원인 파악이 어렵습니다.

세 catch 블록 모두 원본 예외를 swallow하고 커스텀 예외만 throw합니다. 운영 환경에서 OAuth 실패 시 원인(네트워크 오류, invalid grant, 타임아웃 등)을 로그로 확인할 수 없습니다.

📝 제안: 예외 로깅 추가
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
 `@Component`
 public class GoogleOAuthClient {

각 catch 블록에 로깅 추가 (exchangeToken 예시):

     } catch (Exception e) {
+      log.error("Google token exchange failed", e);
       throw new CustomException(CalendarErrorCode.CALENDAR_401_AUTH_FAILED);
     }

fetchUserInforevokeToken의 catch 블록에도 동일하게 적용하세요.

Also applies to: 53-55, 64-66

🤖 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/Timo/Timo/domain/calendar/client/GoogleOAuthClient.java`
around lines 41 - 43, The catch blocks in GoogleOAuthClient are swallowing the
original exception, so log the caught exception before rethrowing the
CustomException. Update each catch in exchangeToken, fetchUserInfo, and
revokeToken to record the exception details through the existing logger in
GoogleOAuthClient, then keep throwing the same CalendarErrorCode-based
CustomException so behavior stays unchanged while preserving OAuth failure
context for debugging.
src/main/java/com/Timo/Timo/domain/calendar/dto/response/CalendarConnectResponse.java (1)

13-14: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

@JsonFormat에 타임존 지정을 권장합니다.

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")에 타임존이 명시되어 있지 않아, 서버 환경에 따라 connectedAt 시간이 다르게 직렬화될 수 있습니다. 한국 시간 기준으로 명시하면 일관성이 보장됩니다.

♻️ 타임존 명시 제안
-  `@JsonFormat`(pattern = "yyyy-MM-dd HH:mm:ss")
+  `@JsonFormat`(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
   `@Schema`(example = "2026-07-06 17:51:50", type = "string")
   LocalDateTime connectedAt
🤖 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/Timo/Timo/domain/calendar/dto/response/CalendarConnectResponse.java`
around lines 13 - 14, `CalendarConnectResponse`의 `connectedAt`에 붙은
`@JsonFormat`이 타임존을 명시하지 않아 서버 환경에 따라 직렬화 값이 달라질 수 있습니다. `@JsonFormat(pattern =
"yyyy-MM-dd HH:mm:ss")`를 한국 시간 기준 타임존이 포함되도록 수정해 일관된 응답이 나오게 하세요. 이 변경은
`CalendarConnectResponse`의 `connectedAt` 직렬화 설정에만 적용하면 됩니다.
🤖 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.

Inline comments:
In `@src/main/java/com/Timo/Timo/domain/calendar/client/GoogleOAuthClient.java`:
- Line 16: `GoogleOAuthClient`의 `restClient`가 `RestClient.create()`로 기본 설정되어 있어
connect/read 타임아웃이 없어 요청 스레드가 블로킹될 수 있습니다. `GoogleOAuthClient` 생성 방식 또는
`restClient` 초기화를 수정해 `RestClient`에 연결 및 읽기 타임아웃을 명시적으로 설정하고, Google API 호출이 지연될
때도 요청이 무한 대기하지 않도록 하세요. 필요하면 이 클라이언트를 사용하는 토큰 교환/연동 해제 로직에서도 동일한 `restClient`
설정이 적용되는지 함께 확인하세요.

In
`@src/main/java/com/Timo/Timo/domain/calendar/dto/response/GoogleTokenResponse.java`:
- Around line 3-7: `GoogleTokenResponse` is missing snake_case JSON mapping, so
Google’s `access_token`, `refresh_token`, and `expires_in` fields won’t bind to
the camelCase record components and can end up null. Update the
`GoogleTokenResponse` record to use Jackson snake_case mapping by adding
`@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)` or equivalent
`@JsonProperty` annotations on the record components so the fields populate
correctly for the code paths used by `fetchUserInfo(...)` and
`plusSeconds(...)`.

In
`@src/main/java/com/Timo/Timo/domain/calendar/exception/CalendarErrorCode.java`:
- Around line 12-13: `CalendarErrorCode` has two distinct enum entries,
`CALENDAR_401_AUTH_FAILED` and `CALENDAR_401_EMAIL_MISMATCH`, but they currently
share the same code value, so clients cannot distinguish the failure cases
reliably. Update the `CalendarErrorCode` enum so `CALENDAR_401_EMAIL_MISMATCH`
uses its own unique code string while keeping the existing HTTP status and
message, and make sure any references that consume `getCode()` or compare
calendar error codes are consistent with the new identifier.

In `@src/main/java/com/Timo/Timo/domain/calendar/service/CalendarService.java`:
- Around line 29-58: CalendarService.connect is doing Google OAuth HTTP calls
inside a transactional boundary, which keeps the DB transaction open too long
and can leave exchanged tokens orphaned on validation failure. Move
googleOAuthClient.exchangeToken and googleOAuthClient.fetchUserInfo outside the
`@Transactional` flow, then wrap only the User lookup and CalendarConnection save
in a transaction. If validateSameAccount fails after token exchange, make sure
the token is revoked or otherwise cleaned up before returning the error.
- Around line 66-77: CalendarService.disconnect currently calls
googleOAuthClient.revokeToken inside the `@Transactional` boundary, which can
leave DB state and token state inconsistent if delete(calendarConnection) fails
afterward. Move the revokeToken call outside the transaction or restructure
disconnect so the database delete completes first and revocation happens as a
best-effort follow-up. Keep the fix localized to CalendarService.disconnect and
preserve the CalendarConnectionRepository delete flow and
CalendarDisconnectResponse result handling.

---

Nitpick comments:
In `@src/main/java/com/Timo/Timo/domain/calendar/client/GoogleOAuthClient.java`:
- Around line 41-43: The catch blocks in GoogleOAuthClient are swallowing the
original exception, so log the caught exception before rethrowing the
CustomException. Update each catch in exchangeToken, fetchUserInfo, and
revokeToken to record the exception details through the existing logger in
GoogleOAuthClient, then keep throwing the same CalendarErrorCode-based
CustomException so behavior stays unchanged while preserving OAuth failure
context for debugging.

In
`@src/main/java/com/Timo/Timo/domain/calendar/dto/response/CalendarConnectResponse.java`:
- Around line 13-14: `CalendarConnectResponse`의 `connectedAt`에 붙은 `@JsonFormat`이
타임존을 명시하지 않아 서버 환경에 따라 직렬화 값이 달라질 수 있습니다. `@JsonFormat(pattern = "yyyy-MM-dd
HH:mm:ss")`를 한국 시간 기준 타임존이 포함되도록 수정해 일관된 응답이 나오게 하세요. 이 변경은
`CalendarConnectResponse`의 `connectedAt` 직렬화 설정에만 적용하면 됩니다.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9aee7a54-cd8f-44da-a58f-46ce41ebca58

📥 Commits

Reviewing files that changed from the base of the PR and between 77fee04 and eb7da00.

⛔ Files ignored due to path filters (2)
  • src/main/java/com/Timo/Timo/domain/calendar/docs/CalendarControllerDocs.java is excluded by !**/docs/**
  • src/main/java/com/Timo/Timo/global/auth/docs/AuthControllerDocs.java is excluded by !**/docs/**
📒 Files selected for processing (13)
  • src/main/java/com/Timo/Timo/domain/calendar/client/GoogleOAuthClient.java
  • src/main/java/com/Timo/Timo/domain/calendar/controller/CalendarController.java
  • src/main/java/com/Timo/Timo/domain/calendar/dto/request/CalendarConnectRequest.java
  • src/main/java/com/Timo/Timo/domain/calendar/dto/response/CalendarConnectResponse.java
  • src/main/java/com/Timo/Timo/domain/calendar/dto/response/CalendarDisconnectResponse.java
  • src/main/java/com/Timo/Timo/domain/calendar/dto/response/GoogleTokenResponse.java
  • src/main/java/com/Timo/Timo/domain/calendar/dto/response/GoogleUserInfoResponse.java
  • src/main/java/com/Timo/Timo/domain/calendar/entity/CalendarConnection.java
  • src/main/java/com/Timo/Timo/domain/calendar/exception/CalendarErrorCode.java
  • src/main/java/com/Timo/Timo/domain/calendar/exception/CalendarSuccessCode.java
  • src/main/java/com/Timo/Timo/domain/calendar/factory/CalendarResponseFactory.java
  • src/main/java/com/Timo/Timo/domain/calendar/repository/CalendarConnectionRepository.java
  • src/main/java/com/Timo/Timo/domain/calendar/service/CalendarService.java

@Component
public class GoogleOAuthClient {

private final RestClient restClient = RestClient.create();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

RestClient에 타임아웃이 설정되지 않아 스레드 블로킹 위험이 있습니다.

RestClient.create()는 기본 connect/read 타임아웃이 없어, Google API 응답 지연 시 요청 스레드가 무한 대기합니다. 다수 사용자가 동시에 연동/해제 시도 시 Tomcat 스레드 풀 고갈로 전체 서비스 장애가 발생할 수 있습니다.

⏱️ 제안: 타임아웃 설정 추가
+import java.time.Duration;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+
 `@Component`
 public class GoogleOAuthClient {

-  private final RestClient restClient = RestClient.create();
+  private final RestClient restClient = RestClient.builder()
+      .requestFactory(buildRequestFactory())
+      .build();
+
+  private SimpleClientHttpRequestFactory buildRequestFactory() {
+    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+    factory.setConnectTimeout(Duration.ofSeconds(5));
+    factory.setReadTimeout(Duration.ofSeconds(10));
+    return factory;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final RestClient restClient = RestClient.create();
import java.time.Duration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
private final RestClient restClient = RestClient.builder()
.requestFactory(buildRequestFactory())
.build();
private SimpleClientHttpRequestFactory buildRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(5));
factory.setReadTimeout(Duration.ofSeconds(10));
return factory;
}
🤖 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/Timo/Timo/domain/calendar/client/GoogleOAuthClient.java` at
line 16, `GoogleOAuthClient`의 `restClient`가 `RestClient.create()`로 기본 설정되어 있어
connect/read 타임아웃이 없어 요청 스레드가 블로킹될 수 있습니다. `GoogleOAuthClient` 생성 방식 또는
`restClient` 초기화를 수정해 `RestClient`에 연결 및 읽기 타임아웃을 명시적으로 설정하고, Google API 호출이 지연될
때도 요청이 무한 대기하지 않도록 하세요. 필요하면 이 클라이언트를 사용하는 토큰 교환/연동 해제 로직에서도 동일한 `restClient`
설정이 적용되는지 함께 확인하세요.

Comment on lines +3 to +7
public record GoogleTokenResponse (
String accessToken,
String refreshToken,
Integer expiresIn
){}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 전역 Jackson naming strategy 설정 확인
rg -i "naming-strategy|SNAKE_CASE" -g '*.yml' -g '*.yaml' -g '*.properties' -n || echo "No naming strategy found"

# `@JsonNaming` 사용 패턴 확인
rg -l "`@JsonNaming`" --type java -g '!**/test/**' || echo "No `@JsonNaming` usage found"

# RestClient 빈 구성 확인
rg -n "RestClient" --type java -g '!**/test/**' -g '!**/build/**'

Repository: Team-Timo/Timo-Server

Length of output: 184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== GoogleTokenResponse ==\n'
sed -n '1,80p' src/main/java/com/Timo/Timo/domain/calendar/dto/response/GoogleTokenResponse.java

printf '\n== RestClient / ObjectMapper configuration ==\n'
rg -n "RestClient|ObjectMapper|Jackson2ObjectMapperBuilder|JsonNaming|JsonProperty|SnakeCaseStrategy|PropertyNamingStrategies" src/main/java -g '!**/test/**' -g '!**/build/**'

printf '\n== Google token usage ==\n'
rg -n "GoogleTokenResponse|access_token|refresh_token|expires_in|token" src/main/java -g '!**/test/**' -g '!**/build/**'

Repository: Team-Timo/Timo-Server

Length of output: 7536


src/main/java/com/Timo/Timo/domain/calendar/dto/response/GoogleTokenResponse.java: snake_case 매핑을 추가하세요. GoogleTokenResponse는 Google의 access_token, refresh_token, expires_in을 받는데 camelCase record라 매칭되지 않습니다. 이대로면 토큰과 만료 시간이 null로 들어와 뒤이어 fetchUserInfo(...)plusSeconds(...)가 실패합니다. @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 또는 @JsonProperty를 붙여주세요.

🤖 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/Timo/Timo/domain/calendar/dto/response/GoogleTokenResponse.java`
around lines 3 - 7, `GoogleTokenResponse` is missing snake_case JSON mapping, so
Google’s `access_token`, `refresh_token`, and `expires_in` fields won’t bind to
the camelCase record components and can end up null. Update the
`GoogleTokenResponse` record to use Jackson snake_case mapping by adding
`@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)` or equivalent
`@JsonProperty` annotations on the record components so the fields populate
correctly for the code paths used by `fetchUserInfo(...)` and
`plusSeconds(...)`.

Comment on lines +12 to +13
CALENDAR_401_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "구글 캘린더 인증에 실패했습니다."),
CALENDAR_401_EMAIL_MISMATCH(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "가입 시 사용한 구글 계정으로만 연동할 수 있습니다."),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

중복된 에러 코드 "CALENDAR_401"로 클라이언트가 에러를 구분할 수 없습니다.

CALENDAR_401_AUTH_FAILEDCALENDAR_401_EMAIL_MISMATCH가 동일한 code 값을 공유합니다. 클라이언트가 code만으로 두 케이스를 구분할 수 없어 message 파싱에 의존해야 하므로, 이메일 불일치 케이스에 대한 별도 사용자 안내가 어렵습니다.

🔧 제안: 에러 코드 분리
   CALENDAR_401_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "구글 캘린더 인증에 실패했습니다."),
-  CALENDAR_401_EMAIL_MISMATCH(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "가입 시 사용한 구글 계정으로만 연동할 수 있습니다."),
+  CALENDAR_401_EMAIL_MISMATCH(HttpStatus.UNAUTHORIZED, "CALENDAR_401_EMAIL", "가입 시 사용한 구글 계정으로만 연동할 수 있습니다."),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CALENDAR_401_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "구글 캘린더 인증에 실패했습니다."),
CALENDAR_401_EMAIL_MISMATCH(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "가입 시 사용한 구글 계정으로만 연동할 수 있습니다."),
CALENDAR_401_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "구글 캘린더 인증에 실패했습니다."),
CALENDAR_401_EMAIL_MISMATCH(HttpStatus.UNAUTHORIZED, "CALENDAR_401_EMAIL", "가입 시 사용한 구글 계정으로만 연동할 수 있습니다."),
🤖 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/Timo/Timo/domain/calendar/exception/CalendarErrorCode.java`
around lines 12 - 13, `CalendarErrorCode` has two distinct enum entries,
`CALENDAR_401_AUTH_FAILED` and `CALENDAR_401_EMAIL_MISMATCH`, but they currently
share the same code value, so clients cannot distinguish the failure cases
reliably. Update the `CalendarErrorCode` enum so `CALENDAR_401_EMAIL_MISMATCH`
uses its own unique code string while keeping the existing HTTP status and
message, and make sure any references that consume `getCode()` or compare
calendar error codes are consistent with the new identifier.

Comment on lines +29 to +58
@Transactional
public CalendarConnectResponse connect(Long userId, CalendarConnectRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));

if (calendarConnectionRepository.existsByUserId(userId)) {
throw new CustomException(CalendarErrorCode.CALENDAR_409_ALREADY_CONNECTED);
}

GoogleTokenResponse tokenResponse = googleOAuthClient.exchangeToken(request.authorizationCode());
GoogleUserInfoResponse userInfo = googleOAuthClient.fetchUserInfo(tokenResponse.accessToken());

validateSameAccount(user, userInfo);

CalendarConnection calendarConnection = CalendarConnection.builder()
.user(user)
.calendarEmail(userInfo.email())
.accessToken(tokenResponse.accessToken())
.refreshToken(tokenResponse.refreshToken())
.tokenExpiresAt(LocalDateTime.now().plusSeconds(tokenResponse.expiresIn()))
.build();

calendarConnectionRepository.save(calendarConnection);

return CalendarConnectResponse.builder()
.calendarConnected(true)
.calendarEmail(calendarConnection.getCalendarEmail())
.connectedAt(calendarConnection.getConnectedAt())
.build();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

@Transactional 내에서 외부 HTTP 호출이 수행되어 트랜잭션이 장기화되고, 검증 실패 시 orphaned 토큰이 발생합니다.

exchangeTokenfetchUserInfo@Transactional 내에서 호출되어, 외부 HTTP 응답 대기 중에도 DB 커넥션이 점유됩니다. 부하 시 커넥션 풀 고갈 위험이 있습니다. 또한 validateSameAccount 실패 시 이미 교환된 Google 토큰이 revoke되지 않아 orphaned 토큰이 남습니다.

외부 호출을 트랜잭션 외부로 이동하고, DB 조작만 트랜잭션으로 감싸는 구조를 권장합니다. As per path instructions, "트랜잭션 경계가 적절한지 확인해 주세요" 및 "외부 시스템 호출, 데이터 접근, 도메인 규칙이 과하게 섞이지 않았는지 확인해 주세요".

🤖 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/Timo/Timo/domain/calendar/service/CalendarService.java`
around lines 29 - 58, CalendarService.connect is doing Google OAuth HTTP calls
inside a transactional boundary, which keeps the DB transaction open too long
and can leave exchanged tokens orphaned on validation failure. Move
googleOAuthClient.exchangeToken and googleOAuthClient.fetchUserInfo outside the
`@Transactional` flow, then wrap only the User lookup and CalendarConnection save
in a transaction. If validateSameAccount fails after token exchange, make sure
the token is revoked or otherwise cleaned up before returning the error.

Source: Path instructions

Comment on lines +66 to +77
@Transactional
public CalendarDisconnectResponse disconnect(Long userId) {
CalendarConnection calendarConnection = calendarConnectionRepository.findByUserId(userId)
.orElseThrow(() -> new CustomException(CalendarErrorCode.CALENDAR_404_NOT_CONNECTED));

googleOAuthClient.revokeToken(calendarConnection.getAccessToken());
calendarConnectionRepository.delete(calendarConnection);

return CalendarDisconnectResponse.builder()
.calendarConnected(false)
.build();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

트랜잭션 내에서 non-rollbackable revokeToken을 호출하여 partial failure 위험이 있습니다.

revokeToken은 HTTP 호출로 롤백이 불가능하지만 @Transactional 내에서 실행됩니다. revoke 성공 후 delete 실패 시 트랜잭션은 롤백되지만 토큰은 이미 revoke된 상태가 되어, DB에는 연동 정보가 남아있으나 실제 토큰은 무효화된 불일치 상태가 됩니다.

revoke를 트랜잭션 외부로 이동하거나 delete 완료 후 best-effort로 revoke를 수행하는 구조를 권장합니다. As per path instructions, "트랜잭션 경계가 적절한지 확인해 주세요" 및 "예외 처리와 실패 케이스 처리가 충분한지 확인해 주세요".

🤖 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/Timo/Timo/domain/calendar/service/CalendarService.java`
around lines 66 - 77, CalendarService.disconnect currently calls
googleOAuthClient.revokeToken inside the `@Transactional` boundary, which can
leave DB state and token state inconsistent if delete(calendarConnection) fails
afterward. Move the revokeToken call outside the transaction or restructure
disconnect so the database delete completes first and revocation happens as a
best-effort follow-up. Keep the fix localized to CalendarService.disconnect and
preserve the CalendarConnectionRepository delete flow and
CalendarDisconnectResponse result handling.

Source: Path instructions

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.

[feat] 구글 캘린더 연동/해제

1 participant