[feat] #40 - 구글 캘린더 연동/해제#44
Conversation
…to feat/#40-google-calendar
Walkthrough구글 OAuth 기반 캘린더 연동/해제 기능이 추가되었습니다. Changes구글 캘린더 연동/해제
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
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 wincatch 블록에서 원본 예외를 로깅하지 않아 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); }
fetchUserInfo와revokeToken의 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
⛔ Files ignored due to path filters (2)
src/main/java/com/Timo/Timo/domain/calendar/docs/CalendarControllerDocs.javais excluded by!**/docs/**src/main/java/com/Timo/Timo/global/auth/docs/AuthControllerDocs.javais excluded by!**/docs/**
📒 Files selected for processing (13)
src/main/java/com/Timo/Timo/domain/calendar/client/GoogleOAuthClient.javasrc/main/java/com/Timo/Timo/domain/calendar/controller/CalendarController.javasrc/main/java/com/Timo/Timo/domain/calendar/dto/request/CalendarConnectRequest.javasrc/main/java/com/Timo/Timo/domain/calendar/dto/response/CalendarConnectResponse.javasrc/main/java/com/Timo/Timo/domain/calendar/dto/response/CalendarDisconnectResponse.javasrc/main/java/com/Timo/Timo/domain/calendar/dto/response/GoogleTokenResponse.javasrc/main/java/com/Timo/Timo/domain/calendar/dto/response/GoogleUserInfoResponse.javasrc/main/java/com/Timo/Timo/domain/calendar/entity/CalendarConnection.javasrc/main/java/com/Timo/Timo/domain/calendar/exception/CalendarErrorCode.javasrc/main/java/com/Timo/Timo/domain/calendar/exception/CalendarSuccessCode.javasrc/main/java/com/Timo/Timo/domain/calendar/factory/CalendarResponseFactory.javasrc/main/java/com/Timo/Timo/domain/calendar/repository/CalendarConnectionRepository.javasrc/main/java/com/Timo/Timo/domain/calendar/service/CalendarService.java
| @Component | ||
| public class GoogleOAuthClient { | ||
|
|
||
| private final RestClient restClient = RestClient.create(); |
There was a problem hiding this comment.
🩺 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.
| 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`
설정이 적용되는지 함께 확인하세요.
| public record GoogleTokenResponse ( | ||
| String accessToken, | ||
| String refreshToken, | ||
| Integer expiresIn | ||
| ){} |
There was a problem hiding this comment.
🎯 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(...)`.
| CALENDAR_401_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "구글 캘린더 인증에 실패했습니다."), | ||
| CALENDAR_401_EMAIL_MISMATCH(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "가입 시 사용한 구글 계정으로만 연동할 수 있습니다."), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
중복된 에러 코드 "CALENDAR_401"로 클라이언트가 에러를 구분할 수 없습니다.
CALENDAR_401_AUTH_FAILED와 CALENDAR_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.
| 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.
| @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(); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
@Transactional 내에서 외부 HTTP 호출이 수행되어 트랜잭션이 장기화되고, 검증 실패 시 orphaned 토큰이 발생합니다.
exchangeToken과 fetchUserInfo가 @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
| @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(); | ||
| } |
There was a problem hiding this comment.
🗄️ 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
관련 이슈 🛠
작업 내용 요약 ✏️
구글 OAuth 동의 완료 후 발급된 authorizationCode로 구글 토큰을 교환하여 캘린더를 연동하고, 연동 정보를 삭제 및 구글 토큰을 revoke하는 연동 해제 기능을 구현했습니다.
주요 변경 사항 🛠️
POST /api/v1/users/calendar(연동),DELETE /api/v1/users/calendar(해제) API 엔드포인트 추가calendar_connections테이블 매핑 엔티티 추가 (토큰 평문 저장)findByUserId,existsByUserId조회 메서드 추가connectedAt응답 포맷을yyyy-MM-dd HH:mm:ss로 통일 (@JsonFormat,@Schema적용)exchangeToken), 사용자 정보 조회(fetchUserInfo), 토큰 revoke(revokeToken) 로직 분리, 기존spring.security.oauth2.client.registration.google설정 재사용AuthResponseFactory패턴에 맞춰 응답 조립 책임 분리CALENDAR_401_AUTH_FAILED,CALENDAR_401_EMAIL_MISMATCH,CALENDAR_409_ALREADY_CONNECTED,CALENDAR_404_NOT_CONNECTED에러 코드 추가CALENDAR_CONNECTED,CALENDAR_DISCONNECTED성공 코드 추가트러블 슈팅 ⚽️
userinfoAPI로 실제 인증된 이메일을 조회해 가입 이메일과 일치하는지 검증하는 로직 추가 (다른 계정으로는 연동 불가 정책)테스트 결과 📄(진행 중)
calendarConnected: true및calendarEmail반환 확인CALENDAR_401) 에러 반환 확인calendarConnected: false반환 확인calendar_connectionsrow 삭제 확인스크린샷 📷
authorizationCode 누락 시 400 에러 반환 확인
유효하지 않은 authorizationCode인 경우 401(
CALENDAR_401) 에러 반환 확인가입 이메일과 다른 구글 계정으로 연동 시도 시 401(이메일 불일치) 에러 반환 확인
이미 연동된 상태에서 재연동 시도 시 409 에러 반환 확인
토큰 없이 요청 시 401 에러 반환 확인
정상 케이스: 캘린더 연동 해제 성공,
calendarConnected: false반환 확인연동되지 않은 상태에서 해제 시도 시 404 에러 반환 확인
해제 후 DB에서
calendar_connectionsrow 삭제 확인리뷰 요구사항 📢
📎 참고 자료 (선택)