[feat] #40 - 구글 캘린더 연동/해제#44
Conversation
…to feat/#40-google-calendar
|
Warning Review limit reached
Next review available in: 32 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
Walkthrough구글 OAuth 기반 캘린더 연동·해제 기능이 추가되었습니다. OAuth 클라이언트, 서비스 로직, API 컨트롤러, 연결 저장 모델, DTO, 오류·성공 코드와 환경별 redirect URI 설정이 포함됩니다. Changes구글 캘린더 연동/해제
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CalendarController
participant CalendarService
participant GoogleOAuthClient
participant GoogleOAuth
participant CalendarConnectionRepository
CalendarController->>CalendarService: connect(userId, authorizationCode)
CalendarService->>GoogleOAuthClient: exchangeToken(authorizationCode)
GoogleOAuthClient->>GoogleOAuth: token request
GoogleOAuth-->>GoogleOAuthClient: GoogleTokenResponse
CalendarService->>GoogleOAuthClient: fetchUserInfo(accessToken)
GoogleOAuth-->>GoogleOAuthClient: GoogleUserInfoResponse
CalendarService->>CalendarConnectionRepository: save(CalendarConnection)
CalendarService-->>CalendarController: CalendarConnectResponse
CalendarController->>CalendarService: disconnect(userId)
CalendarService->>CalendarConnectionRepository: findByUserId(userId)
CalendarService->>GoogleOAuthClient: revokeToken(accessToken)
CalendarService->>CalendarConnectionRepository: delete(connection)
CalendarService-->>CalendarController: CalendarDisconnectResponse
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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: 2
🧹 Nitpick comments (3)
src/main/java/com/Timo/Timo/domain/calendar/client/GoogleOAuthClient.java (1)
82-93: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
revokeToken에서 토큰을 URL 인코딩 없이 쿼리 스트링에 연결하고 있습니다.Google 토큰은 일반적으로 URL-safe 문자로 구성되지만,
&나=등의 특수문자가 포함될 경우 URL이 깨질 수 있습니다.URLEncoder.encode를 적용하는 것이 안전합니다.🔒 제안: 토큰 URL 인코딩 적용
public void revokeToken(String token) { try { restClient.post() - .uri("https://oauth2.googleapis.com/revoke?token=" + token) + .uri("https://oauth2.googleapis.com/revoke?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8)) .retrieve() .toBodilessEntity();🤖 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 82 - 93, Update GoogleOAuthClient.revokeToken to URL-encode the token before appending it to the revoke endpoint query string, using UTF-8 encoding and preserving the existing exception handling behavior.src/main/java/com/Timo/Timo/domain/calendar/controller/CalendarController.java (1)
46-57: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winOAuth authorize URL에
state파라미터가 누락되어 있습니다.Google OAuth 권한 부여 요청에
state파라미터를 포함하면 CSRF 공격을 방지할 수 있습니다. 현재 서비스의 이메일 일치 검증(validateSameAccount)으로 완화되지만, OAuth 보안 모범 사례로state파라미터 추가를 권장합니다. As per path instructions, "인증/인가가 필요한 API에서 보안 처리가 누락되지 않았는지 확인해 주세요".🔒 제안: state 파라미터 추가
`@GetMapping`("/authorize") public void authorize(HttpServletResponse response) throws IOException { + String state = UUID.randomUUID().toString(); + // state를 세션 또는 캐시에 저장하여 callback에서 검증 + String url = GOOGLE_AUTH_URL + "?client_id=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8) + "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8) + "&response_type=code" + + "&state=" + URLEncoder.encode(state, StandardCharsets.UTF_8) + "&scope=" + URLEncoder.encode(CALENDAR_SCOPE, StandardCharsets.UTF_8) + "&access_type=offline" + "&prompt=consent"; response.sendRedirect(url); }참고:
state값은 세션, 쿠키, 또는 Redis 등에 저장한 후 callback 처리 시 검증해야 합니다. 프론트엔드에서 callback을 처리하므로, 쿠키에 state를 저장하고 프론트엔드에서 검증하는 방식도 고려할 수 있습니다.🤖 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/controller/CalendarController.java` around lines 46 - 57, Update the authorize flow in CalendarController.authorize to generate a cryptographically secure OAuth state value, persist it for the initiating session or request context, append it as a URL-encoded state parameter, and validate and consume the same value during the OAuth callback before accepting the authorization response.Source: Path instructions
src/main/java/com/Timo/Timo/domain/calendar/service/CalendarService.java (1)
79-84: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win토큰 revoke 시 access token 대신 refresh token을 사용하는 것을 권장합니다.
Google의 revoke 엔드포인트는 access token과 refresh token 모두를 받지만, refresh token을 revoke하면 파생된 모든 access token도 함께 무효화됩니다. 현재 access token만 revoke하면 refresh token이 유효한 상태로 남아 새 access token을 발급받을 수 있습니다. DB에서 연결 정보가 삭제된 후에도 refresh token이 orphaned 상태로 남는 보안 위험이 있습니다.
🔒 제안: refresh token으로 revoke 수행
public CalendarDisconnectResponse disconnect(Long userId) { CalendarConnection calendarConnection = calendarConnectionRepository.findByUserId(userId) .orElseThrow(() -> new CustomException(CalendarErrorCode.CALENDAR_404_NOT_CONNECTED)); - String accessToken = calendarConnection.getAccessToken(); + String refreshToken = calendarConnection.getRefreshToken(); deleteConnection(calendarConnection); try { - googleOAuthClient.revokeToken(accessToken); + googleOAuthClient.revokeToken(refreshToken); } catch (Exception e) { log.warn("Google token revoke failed after disconnect. userId={}", userId, e); }🤖 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 79 - 84, Update the token captured before deleteConnection in the CalendarService revoke flow to use the stored refresh token instead of the access token, then pass that refresh token to googleOAuthClient.revokeToken so all derived access tokens are invalidated.
🤖 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/service/CalendarService.java`:
- Line 43: CalendarService의 connect()와 disconnect()가 같은 클래스의 saveConnection() 및
deleteConnection()을 직접 호출해 `@Transactional` 프록시를 우회합니다. 트랜잭션 메서드인
saveConnection()과 deleteConnection()을 별도 Spring 서비스로 분리하고 CalendarService에 주입한
뒤, 두 흐름에서 해당 서비스의 메서드를 호출하도록 변경해 각 쓰기 작업이 프록시를 통해 실행되게 하세요.
In `@src/main/resources/application-local.yml`:
- Around line 33-34: Update the local profile configuration for
app.calendar.redirect-uri to provide a default FRONTEND_URL value, such as
http://localhost:5173, when the environment variable is unset. Keep the existing
calendar callback path unchanged so local startup succeeds without requiring
external configuration.
---
Nitpick comments:
In `@src/main/java/com/Timo/Timo/domain/calendar/client/GoogleOAuthClient.java`:
- Around line 82-93: Update GoogleOAuthClient.revokeToken to URL-encode the
token before appending it to the revoke endpoint query string, using UTF-8
encoding and preserving the existing exception handling behavior.
In
`@src/main/java/com/Timo/Timo/domain/calendar/controller/CalendarController.java`:
- Around line 46-57: Update the authorize flow in CalendarController.authorize
to generate a cryptographically secure OAuth state value, persist it for the
initiating session or request context, append it as a URL-encoded state
parameter, and validate and consume the same value during the OAuth callback
before accepting the authorization response.
In `@src/main/java/com/Timo/Timo/domain/calendar/service/CalendarService.java`:
- Around line 79-84: Update the token captured before deleteConnection in the
CalendarService revoke flow to use the stored refresh token instead of the
access token, then pass that refresh token to googleOAuthClient.revokeToken so
all derived access tokens are invalidated.
🪄 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: e15738cf-b4a3-4fcc-bb39-554a11e2e476
📒 Files selected for processing (6)
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/exception/CalendarErrorCode.javasrc/main/java/com/Timo/Timo/domain/calendar/service/CalendarService.javasrc/main/resources/application-local.ymlsrc/main/resources/application-prod.yml
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/Timo/Timo/domain/calendar/exception/CalendarErrorCode.java
| private static final String GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; | ||
| private static final String CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly email"; |
There was a problem hiding this comment.
이거 하드코딩해도 되는 값들 맞지요? 아니라면 환경변수처리해주세용
There was a problem hiding this comment.
GOOGLE_AUTH_URL은 구글의 고정 엔드포인트이고 CALENDAR_SCOPE도
요청할 권한 값으로 둘 다 고정된 정책값이라 환경변수로 처리하기보다는 상수로
그대로 유지할까 합니다..!!
| CALENDAR_401_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "구글 캘린더 인증에 실패했습니다."), | ||
| CALENDAR_401_EMAIL_MISMATCH(HttpStatus.UNAUTHORIZED, "CALENDAR_401", "가입 시 사용한 구글 계정으로만 연동할 수 있습니다."), | ||
| CALENDAR_404_NOT_CONNECTED(HttpStatus.NOT_FOUND, "CALENDAR_404", "연동된 캘린더가 없습니다."), | ||
| CALENDAR_409_ALREADY_CONNECTED(HttpStatus.CONFLICT, "CALENDAR_409", "이미 캘린더가 연동되어 있습니다."), | ||
| CALENDAR_500_REVOKE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "CALENDAR_500", "구글 토큰 해제에 실패했습니다."); | ||
| CALENDAR_500_REVOKE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "CALENDAR_500", "구글 토큰 해제에 실패했습니다."), | ||
| CALENDAR_503_TIMEOUT(HttpStatus.SERVICE_UNAVAILABLE, "CALENDAR_503", "구글 서버 응답이 지연되고 있습니다. 잠시 후 다시 시도해주세요."); |
There was a problem hiding this comment.
p3) 에러코드 네이밍 안에 숫자 넣지 말아주세요
| @Value("${app.calendar.redirect-uri}") | ||
| private String redirectUri; | ||
|
|
||
| @GetMapping("/authorize") |
There was a problem hiding this comment.
p1)
현재
/authorize는 인증이 필요한 API이므로 클라이언트가 Axios/fetch를 통해Authorization헤더를 붙여 호출할 것으로 보입니다.다만 Axios/fetch가 서버의 302 응답을 따라가더라도 이는 백그라운드 HTTP 요청일 뿐, 브라우저의 현재 화면이 Google OAuth 동의 화면으로 이동하지 않습니다. 반대로
window.location.assign("/api/v1/users/calendar/authorize")으로 직접 이동하면 Axios 인터셉터가 적용되지 않아 Bearer 토큰을 전달할 수 없습니다.따라서 현재 헤더 기반 인증 구조에서는 이 API가 직접 302 리다이렉트하기보다 Google 인증 URL을 응답으로 반환하고, 프론트에서 해당 URL로 이동시키는 방식이 적절해 보입니다.
const { data } = await axios.get("/api/v1/users/calendar/authorize"); window.location.assign(data.authorizationUrl);추가로 인증 URL을 생성할 때 사용자와 OAuth 응답을 연결할 수 있도록 일회성
state값을 함께 발급하고, 콜백 시 검증하는 것도 필요해 보입니다.
There was a problem hiding this comment.
움 정리해보자면, 현재 Bearer 토큰 기반 인증 구조에서 axios로 이 API를 호출하면 axios는 302 응답을 백그라운드로만 따라가서 실제 브라우저 화면은 이동하지 않고, 반대로 window.location으로 직접 이동시키면 Authorization 헤더가 실리지 않아 401이 나는 문제가 생기는 거군요..!! 말씀해주신대로 서버가 302로 직접 리다이렉트 하는 대신 구글 인증 URL을 JSON으로 응답하도록 변경하여 프론트쪽에서 해당 주소로 리다이렉트 시키는 방식으로 변경했습니다..!!
state는 아래 코드리뷰 방식에 따라서 추가하여 반영햇습니다..!!
|
|
||
| @GetMapping("/authorize") | ||
| public void authorize(HttpServletResponse response) throws IOException { | ||
| String url = GOOGLE_AUTH_URL |
There was a problem hiding this comment.
p2) OAuth 인증 URL에 state가 포함되어 있지 않아, Google에서 돌아온 authorization code가 현재 로그인한 사용자가 시작한 연동 흐름에서 발급된 것인지 검증할 수 없습니다.
현재 이메일 일치 검증이 일부 계정 연결 공격을 차단하지만, OAuth CSRF 방어를 위해 사용자별로 예측 불가능한 일회성 state를 발급하고 콜백에서 검증하는 것이 필요해 보입니다. 예를 들어 Redis에 state → userId를 짧은 TTL로 저장하고, 연동 완료 요청에서 검증 후 즉시 삭제할 수 있습니다.
There was a problem hiding this comment.
오호 그러네용 지금까지는 authorizationCode가 실제로 현재 로그인한 사용자가 시작한 연동 흐름에서 나온 것인지 검증할 방법이 없어 이메일 일치 검증에만 의존하고 있었습니다!
말씀 주신대로 Redis에 state(UUID) -> userId를 5분 TTL로 저장하고, 연동 완료 요청(connect) 시 state를 함께 받아 검증 후 즉시 삭제하도록 구현하였습니다..!!
| @Column(name = "access_token", nullable = false, length = 2048) | ||
| private String accessToken; | ||
|
|
||
| @Column(name = "refresh_token", length = 2048) | ||
| private String refreshToken; |
There was a problem hiding this comment.
p1) accessToken과 특히 장기간 유효할 수 있는 refreshToken이 DB에 평문으로 저장되고 있습니다. DB 조회 권한이나 백업 데이터가 유출될 경우 연결된 사용자의 Google Calendar에 접근할 수 있어 보안상 위험이 큽니다.
캘린더 API 호출을 위해 토큰을 복원해야 하므로 단방향 해싱보다는 애플리케이션 레벨의 양방향 암호화가 필요합니다. 최소한 AES-GCM 등으로 암호화하고, 암호화 키는 환경변수 또는 Secret Manager/KMS처럼 DB와 분리된 저장소에서 관리하는 방향을 권장합니다. 토큰이 로그나 예외 메시지에 포함되지 않는지도 함께 확인하면 좋겠습니다.
| String accessToken = calendarConnection.getAccessToken(); | ||
|
|
||
| deleteConnection(calendarConnection); | ||
|
|
||
| try { | ||
| googleOAuthClient.revokeToken(accessToken); | ||
| } catch (Exception e) { | ||
| log.warn("Google token revoke failed after disconnect. userId={}", userId, e); | ||
| } | ||
|
|
||
| return CalendarDisconnectResponse.builder() | ||
| .calendarConnected(false) | ||
| .build(); |
There was a problem hiding this comment.
p2) 연동 해제 시 장기간 유효한 refresh token은 사용하지 않고 access token만 revoke하고 있습니다. Access token이 이미 만료된 경우를 고려해 refresh token을 우선 revoke하고, 없을 때만 access token을 사용하는 것이 안전해 보입니다.
또한 현재는 DB 정보를 먼저 삭제한 뒤 revoke 실패를 무시하기 때문에, Google 요청 실패 시 토큰을 잃어버려 재시도할 수 없습니다. revoke 성공 후 삭제하거나, 해제 대기 상태/별도 작업으로 저장해 재시도할 수 있는 구조가 필요해 보입니다.
토큰을 쿼리 문자열에 직접 넣으면 프록시나 APM 로그에 노출될 수 있으므로 form-urlencoded body로 전달하는 것도 권장합니다.
There was a problem hiding this comment.
넵 3가지 모두 반영하였습니다..!!
- refresh token 우선 revoke
- revoke 실패 시 재시도 가능한 구조
- forrm-urlencoded body 전달
| private final GoogleOAuthClient googleOAuthClient; | ||
| private final CalendarConnectionCommandService commandService; | ||
|
|
||
| public CalendarConnectResponse connect(Long userId, CalendarConnectRequest request) { |
There was a problem hiding this comment.
p3) existsByUserId() 확인과 실제 저장이 원자적으로 처리되지 않아 동시 요청 두 개가 모두 사전 검사를 통과할 수 있습니다. DB의 user_id unique 제약조건 덕분에 중복 저장은 방지되지만, 나중에 저장하는 요청은 CALENDAR_ALREADY_CONNECTED가 아니라 unique 위반에 의한 500으로 응답할 가능성이 있습니다.
unique 제약조건은 유지하되 저장 시 발생하는 해당 제약조건 위반을 409로 변환하거나, 사용자별 연동 요청을 선점/멱등 처리하는 방식이 필요해 보입니다. existsByUserId()는 빠른 사전 검사로는 사용할 수 있지만 동시성 보장 수단으로는 충분하지 않습니다.
관련 이슈 🛠
작업 내용 요약 ✏️
구글 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패턴에 맞춰 응답 조립 책임 분리트러블 슈팅 ⚽️
userinfoAPI로 실제 인증된 이메일을 조회해 가입 이메일과 일치하는지 검증하는 로직 추가 (다른 계정으로는 연동 불가 정책)구글 캘린더 연동 시 fetchUserInfo 단계에서 401 에러 발생
POST /api/v1/users/calendar호출 시CALENDAR_401(구글 캘린더 인증에 실패했습니다) 응답이 반복적으로 발생CustomException으로 감싸 던지던 catch 블록에 로그를 추가하여 원인을 단계별로 추적GoogleTokenResponse에@JsonProperty가 없어, 구글이 스네이크 케이스(access_token,refresh_token,expires_in)로 응답하는 필드가 전부null로 파싱됨. 이로 인해fetchUserInfo()호출 시accessToken이null로 전달되어Authorization: Bearer null헤더가 생성되고, 구글이 401(UNAUTHENTICATED)로 거부하는 문제였음. 각 필드에@JsonProperty를 명시하여 해결calendar.readonly스코프만 요청하고, 사용자 정보 조회에 필요한email(userinfo) 스코프를 함께 요청하지 않았기 때문. 캘린더 접근 권한만 있는 토큰으로는userinfo엔드포인트 호출이 거부됨을 확인. 인가 요청 시calendar.readonly와email스코프를 함께 요청하도록 테스트 절차를 수정하여 해결invalid_grant로 거부된다는 점도 함께 확인. 테스트 시 매번 새 인가 코드를 발급받아야 함self-invocation으로 인해 @transactional이 실제로 적용되지 않던 문제
CalendarService.connect(),disconnect()에서 같은 클래스 내부의saveConnection(),deleteConnection()을this로 직접 호출하고 있었는데, 이 경우 Spring AOP 프록시를 우회하여@Transactional이 사실상 무시된다는 점을 코드 리뷰(CodeRabbit)를 통해 확인@Transactional은 프록시 기반으로 동작하는데, 외부에서 빈을 호출할 때만 프록시가 트랜잭션 시작/커밋 로직을 가로챌 수 있고, 같은 클래스 내부에서 자기 자신의 메서드를 호출하는 경우(self-invocation)에는 프록시를 거치지 않아 트랜잭션이 적용되지 않음saveConnection,deleteConnection)을 별도의CalendarConnectionCommandService로 분리하고,CalendarService가 이를 주입받아 호출하도록 수정 -> 다른 빈을 통해 호출되므로 프록시가 정상적으로 트랜잭션을 적용함revoke 실패를 조용히 무시하지 않고 log.warn으로 남긴 이유
disconnect()에서 DB 삭제를 먼저 확정한 뒤revokeToken을 best-effort로 실행하도록 바꾸면서, 이 호출이 실패했을 때catch {}로 완전히 무시할지 로그를 남길지 검토catch {}로 비워두면 코드는 단순해지지만, **"조용한 실패(silent failure)"**가 되어 다음과 같은 문제가 생길 수 있음을 확인error가 아닌warn레벨로 로그를 남기기로 결정error로 하지 않은 이유는 이 실패가 사용자 경험에는 즉각적인 영향을 주지 않는 부차적 실패이기 때문임 (DB 삭제는 이미 성공했고 우리 서비스는 더 이상 해당 토큰을 사용하지 않으므로 기능적으로는 정상 동작)log.warn(userId 포함)을 선택함테스트 결과 📄
calendarConnected: true및calendarEmail반환 확인CALENDAR_401) 에러 반환 확인calendarConnected: false반환 확인calendar_connectionsrow 삭제 확인스크린샷 📷
캘린더 연동 성공,








calendarConnected: true및calendarEmail반환 확인authorizationCode 누락 시 400 에러 반환 확인
유효하지 않은 authorizationCode인 경우 401(
CALENDAR_401) 에러 반환 확인가입 이메일과 다른 구글 계정으로 연동 시도 시 401(이메일 불일치) 에러 반환 확인
이미 연동된 상태에서 재연동 시도 시 409 에러 반환 확인
캘린더 연동 해제 성공,
calendarConnected: false반환 확인연동되지 않은 상태에서 해제 시도 시 404 에러 반환 확인
해제 후 DB에서
calendar_connectionsrow 삭제 확인리뷰 요구사항 📢
변경 사항
캘린더 연동을 시작하는
GET /api/v1/users/calendar/authorizeAPI를 새로 추가했습니다.기존
POST /api/v1/users/calendar(토큰 교환),DELETE /api/v1/users/calendar(연동 해제) API는 로직 변경 없이 그대로 유지됩니다.📎 참고 자료 (선택)
Summary by CodeRabbit
Summary by CodeRabbit