Feature/#17 google calendar#18
Conversation
📝 Walkthrough개요예약 시스템에 Google Calendar API 통합을 추가하여 예약 생성·수정·삭제 시 자동으로 캘린더 이벤트를 동기화하도록 확장되었습니다. 라이브러리 의존성, 애플리케이션 설정, 데이터 모델, 새로운 캘린더 서비스, 그리고 기존 예약 서비스의 통합이 포함되었습니다. 변경 사항Google Calendar 통합
코드 리뷰 난이도🎯 3 (중간) | ⏱️ ~25분 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
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/nailagent/backend/domain/reservation/service/ReservationService.java (1)
101-103:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift트랜잭션 내부의 외부 API 호출은 분리하는 편이 안정적입니다.
현재 예약 트랜잭션 안에서 Google API를 직접 호출하고 있어 DB 트랜잭션 점유 시간이 늘고 장애 전파 가능성이 커집니다. 커밋 이후 동기화(
@TransactionalEventListener(phase = AFTER_COMMIT)또는 outbox)로 분리하는 구조를 권장드립니다.Also applies to: 153-156, 161-169, 172-179
🤖 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/nailagent/backend/domain/reservation/service/ReservationService.java` around lines 101 - 103, The createReservation `@Transactional` method (and other transactional methods that currently call Google APIs) must not invoke external APIs inside the DB transaction; instead emit a domain event (e.g., ReservationCreatedEvent) or persist an outbox record within the transaction and move the Google API interaction into an async listener executed AFTER_COMMIT (use `@TransactionalEventListener`(phase = AFTER_COMMIT) or a separate outbox processor). Update createReservation to publish the event/outbox instead of calling the Google API directly, implement a separate listener/worker to perform the Google API calls and add proper retry/error handling and idempotency there so API failures do not hold or roll back the DB transaction.
🤖 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 @.github/workflows/cd.yml:
- Around line 65-66: The created google-credentials.json is left to the system
umask; immediately after writing it with echo "$GOOGLE_CREDENTIALS_JSON" >
~/app/google-credentials.json you must harden permissions by adding chmod 600
~/app/google-credentials.json (do this before appending the path to .env).
Update the workflow lines that reference GOOGLE_CREDENTIALS_JSON /
google-credentials.json to run chmod 600 right after file creation so the file
is only readable/writable by the owner.
In
`@src/main/java/com/nailagent/backend/global/calendar/GoogleCalendarService.java`:
- Around line 32-49: Constructor for GoogleCalendarService currently throws on
init which can stop app startup; wrap the calendar client initialization in a
try-catch inside the GoogleCalendarService constructor to catch Exception, log
the error with context, set a private boolean flag (e.g., enabled = false) and
leave the calendar client null or a safe no-op state so the bean still
constructs. Update all public methods that interact with the calendar
(create/update/delete methods in GoogleCalendarService) to first check the
enabled flag (or null calendar) and return safely (no-op) when disabled,
optionally logging a debug/info message; when initialization succeeds set
enabled = true. Ensure the constructor no longer propagates exceptions so the
application can continue running even if calendar setup fails.
- Around line 51-60: The reserveTime parsing errors are not isolated — only
IOException is caught in createEvent (and the similar blocks at 63-70 and 82-86)
so parsing failures (e.g., split index, LocalTime.parse) can bubble up and roll
back the transaction; change the code to validate the reserveTime format before
parsing and wrap the parsing logic inside buildEvent (or wherever reserveTime is
parsed) with a try-catch that catches parsing/runtime exceptions (e.g.,
DateTimeParseException, IndexOutOfBoundsException, RuntimeException) and handles
them the same as calendar failures: log a clear error including the problematic
reserveTime and exception, and return null (or the same failure signal) instead
of letting the exception propagate; apply the same pattern to the other calendar
methods that call buildEvent (the blocks referenced at lines 63-70 and 82-86,
e.g., updateEvent/deleteEvent) so all calendar-related parsing errors are
isolated.
---
Outside diff comments:
In
`@src/main/java/com/nailagent/backend/domain/reservation/service/ReservationService.java`:
- Around line 101-103: The createReservation `@Transactional` method (and other
transactional methods that currently call Google APIs) must not invoke external
APIs inside the DB transaction; instead emit a domain event (e.g.,
ReservationCreatedEvent) or persist an outbox record within the transaction and
move the Google API interaction into an async listener executed AFTER_COMMIT
(use `@TransactionalEventListener`(phase = AFTER_COMMIT) or a separate outbox
processor). Update createReservation to publish the event/outbox instead of
calling the Google API directly, implement a separate listener/worker to perform
the Google API calls and add proper retry/error handling and idempotency there
so API failures do not hold or roll back the DB transaction.
🪄 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: e975c83b-a259-48a0-b922-bef2d67f9825
📒 Files selected for processing (7)
.github/workflows/cd.yml.gitignorebuild.gradlesrc/main/java/com/nailagent/backend/domain/reservation/entity/Reservation.javasrc/main/java/com/nailagent/backend/domain/reservation/service/ReservationService.javasrc/main/java/com/nailagent/backend/global/calendar/GoogleCalendarService.javasrc/main/resources/application.yml
| public GoogleCalendarService( | ||
| @Value("${google.calendar.id}") String calendarId, | ||
| @Value("${google.calendar.credentials-path}") String credentialsPath, | ||
| ResourceLoader resourceLoader | ||
| ) throws Exception { | ||
| this.calendarId = calendarId; | ||
|
|
||
| InputStream credentialsStream = resourceLoader.getResource(credentialsPath).getInputStream(); | ||
| GoogleCredentials credentials = GoogleCredentials | ||
| .fromStream(credentialsStream) | ||
| .createScoped(List.of(CalendarScopes.CALENDAR)); | ||
|
|
||
| this.calendar = new Calendar.Builder( | ||
| GoogleNetHttpTransport.newTrustedTransport(), | ||
| GsonFactory.getDefaultInstance(), | ||
| new HttpCredentialsAdapter(credentials) | ||
| ).setApplicationName("NailAgent").build(); | ||
| } |
There was a problem hiding this comment.
캘린더 초기화 실패가 전체 애플리케이션 기동 실패로 이어질 수 있습니다.
생성자에서 예외가 나면 빈 생성이 중단되어 예약 기능까지 같이 죽습니다. 캘린더 클라이언트 초기화 실패는 로그 후 비활성 모드로 전환하고, create/update/delete에서 안전하게 no-op 처리하는 방식으로 격리하는 게 좋겠습니다.
🤖 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/nailagent/backend/global/calendar/GoogleCalendarService.java`
around lines 32 - 49, Constructor for GoogleCalendarService currently throws on
init which can stop app startup; wrap the calendar client initialization in a
try-catch inside the GoogleCalendarService constructor to catch Exception, log
the error with context, set a private boolean flag (e.g., enabled = false) and
leave the calendar client null or a safe no-op state so the bean still
constructs. Update all public methods that interact with the calendar
(create/update/delete methods in GoogleCalendarService) to first check the
enabled flag (or null calendar) and return safely (no-op) when disabled,
optionally logging a debug/info message; when initialization succeeds set
enabled = true. Ensure the constructor no longer propagates exceptions so the
application can continue running even if calendar setup fails.
| public String createEvent(Reservation reservation) { | ||
| try { | ||
| Event event = buildEvent(reservation); | ||
| Event created = calendar.events().insert(calendarId, event).execute(); | ||
| log.info("Google Calendar 이벤트 생성: {}", created.getId()); | ||
| return created.getId(); | ||
| } catch (IOException e) { | ||
| log.error("Google Calendar 이벤트 생성 실패", e); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
reserveTime 파싱 예외가 격리되지 않아 예약 처리 자체가 실패할 수 있습니다.
현재는 IOException만 잡고 있어서, 시간 포맷 불일치(split 인덱스/LocalTime.parse) 같은 런타임 예외가 터지면 트랜잭션이 롤백됩니다. 포맷 검증을 추가하고 런타임 파싱 예외도 캘린더 실패로 격리해 주세요.
🛠️ 제안 수정안 (요지)
public String createEvent(Reservation reservation) {
try {
Event event = buildEvent(reservation);
Event created = calendar.events().insert(calendarId, event).execute();
log.info("Google Calendar 이벤트 생성: {}", created.getId());
return created.getId();
- } catch (IOException e) {
+ } catch (IOException | RuntimeException e) {
log.error("Google Calendar 이벤트 생성 실패", e);
return null;
}
} private Event buildEvent(Reservation reservation) {
String[] timeRange = reservation.getReserveTime().split("-");
+ if (timeRange.length != 2) {
+ throw new IllegalArgumentException("Invalid reserveTime format: " + reservation.getReserveTime());
+ }
LocalTime startTime = LocalTime.parse(timeRange[0]);
LocalTime endTime = LocalTime.parse(timeRange[1]);Also applies to: 63-70, 82-86
🤖 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/nailagent/backend/global/calendar/GoogleCalendarService.java`
around lines 51 - 60, The reserveTime parsing errors are not isolated — only
IOException is caught in createEvent (and the similar blocks at 63-70 and 82-86)
so parsing failures (e.g., split index, LocalTime.parse) can bubble up and roll
back the transaction; change the code to validate the reserveTime format before
parsing and wrap the parsing logic inside buildEvent (or wherever reserveTime is
parsed) with a try-catch that catches parsing/runtime exceptions (e.g.,
DateTimeParseException, IndexOutOfBoundsException, RuntimeException) and handles
them the same as calendar failures: log a clear error including the problematic
reserveTime and exception, and return null (or the same failure signal) instead
of letting the exception propagate; apply the same pattern to the other calendar
methods that call buildEvent (the blocks referenced at lines 63-70 and 82-86,
e.g., updateEvent/deleteEvent) so all calendar-related parsing errors are
isolated.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/main/java/com/nailagent/backend/global/calendar/GoogleCalendarService.java (1)
58-68:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
createEvent의 예외 처리 방식이 PR 설명의 격리 전략과 다르게 동작해요.PR 설명에서는 "Calendar API failures are isolated with try-catch so reservations are still processed successfully"라고 명시되어 있는데요,
createEvent는calendar == null이거나IOException발생 시CustomException을 던지고 있어요. 이렇게 되면 캘린더 연동 실패 시 예약 생성 자체가 실패할 수 있어요.
updateEvent/deleteEvent처럼 격리하려면 예외를 던지지 않고null을 반환하는 방식으로 통일하거나, 의도적으로 예약 생성 시에만 엄격하게 처리하려는 것이라면 PR 설명을 업데이트해 주시면 좋겠어요.또한 이전 리뷰에서 언급된 것처럼
buildEvent에서 발생할 수 있는IllegalArgumentException이나DateTimeParseException도 격리되지 않고 있어요.🤖 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/nailagent/backend/global/calendar/GoogleCalendarService.java` around lines 58 - 68, The createEvent method currently throws CustomException on calendar==null and on IOException, which violates the intended isolation strategy; modify createEvent (and its use of buildEvent) to catch calendar==null, IOException, IllegalArgumentException, and DateTimeParseException, log an appropriate error message (including exception details) and return null instead of throwing; align behavior with updateEvent/deleteEvent by treating calendar failures as non-fatal and ensuring buildEvent parsing errors are also isolated and result in a null return.
🤖 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/nailagent/backend/domain/reservation/service/ReservationService.java`:
- Around line 154-160: The method currently assumes calendar sync succeeded by
always returning calendarSync=true after calling
googleCalendarService.createEvent(reservation); instead check the returned
eventId and set calendarSync = (eventId != null), only call
reservation.updateGoogleEventId(eventId) when eventId is non-null, and pass that
boolean into ReservationCreateResponse.builder().calendarSync(...) so the
response reflects whether createEvent actually returned an ID; refer to
createEvent, updateGoogleEventId, ReservationCreateResponse.builder() and
ReservationService to locate the changes.
---
Duplicate comments:
In
`@src/main/java/com/nailagent/backend/global/calendar/GoogleCalendarService.java`:
- Around line 58-68: The createEvent method currently throws CustomException on
calendar==null and on IOException, which violates the intended isolation
strategy; modify createEvent (and its use of buildEvent) to catch
calendar==null, IOException, IllegalArgumentException, and
DateTimeParseException, log an appropriate error message (including exception
details) and return null instead of throwing; align behavior with
updateEvent/deleteEvent by treating calendar failures as non-fatal and ensuring
buildEvent parsing errors are also isolated and result in a null return.
🪄 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: e1bd5372-da20-452e-8836-f8b784473580
📒 Files selected for processing (6)
.github/workflows/cd.ymlsrc/main/java/com/nailagent/backend/domain/reservation/controller/ReservationController.javasrc/main/java/com/nailagent/backend/domain/reservation/dto/Response/ReservationCreateResponse.javasrc/main/java/com/nailagent/backend/domain/reservation/service/ReservationService.javasrc/main/java/com/nailagent/backend/global/calendar/GoogleCalendarService.javasrc/main/java/com/nailagent/backend/global/exception/ErrorCode.java
| String eventId = googleCalendarService.createEvent(reservation); | ||
| reservation.updateGoogleEventId(eventId); | ||
|
|
||
| return ReservationCreateResponse.builder() | ||
| .id(reservation.getId()) | ||
| .calendarSync(true) | ||
| .build(); |
There was a problem hiding this comment.
calendarSync 값이 실제 동기화 결과를 반영하지 않아요.
PR 설명에 따르면 Calendar API 실패 시 try-catch로 예외를 처리한다고 되어 있는데요, 만약 createEvent()가 예외를 잡고 null을 반환하는 경우에도 calendarSync가 항상 true로 응답됩니다. 클라이언트에게 잘못된 정보를 전달할 수 있어요.
updateReservation과 deleteReservation에서 googleEventId != null 체크를 하는 것처럼, 여기서도 eventId 존재 여부에 따라 calendarSync 값을 설정하면 더 정확한 응답이 될 것 같아요! 😊
🔧 제안하는 수정 방향
String eventId = googleCalendarService.createEvent(reservation);
- reservation.updateGoogleEventId(eventId);
+ if (eventId != null) {
+ reservation.updateGoogleEventId(eventId);
+ }
return ReservationCreateResponse.builder()
.id(reservation.getId())
- .calendarSync(true)
+ .calendarSync(eventId != null)
.build();🤖 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/nailagent/backend/domain/reservation/service/ReservationService.java`
around lines 154 - 160, The method currently assumes calendar sync succeeded by
always returning calendarSync=true after calling
googleCalendarService.createEvent(reservation); instead check the returned
eventId and set calendarSync = (eventId != null), only call
reservation.updateGoogleEventId(eventId) when eventId is non-null, and pass that
boolean into ReservationCreateResponse.builder().calendarSync(...) so the
response reflects whether createEvent actually returned an ID; refer to
createEvent, updateGoogleEventId, ReservationCreateResponse.builder() and
ReservationService to locate the changes.
ReservationCreateResponse 삭제
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/main/java/com/nailagent/backend/domain/reservation/service/ReservationService.java (1)
153-154: ⚡ Quick win
eventIdnull 체크를 추가하면 다른 메서드들과 일관성이 높아져요.
updateReservation과deleteReservation에서는googleEventId != null체크 후 캘린더 API를 호출하는 패턴을 사용하고 있는데요, 여기서도 동일한 패턴을 적용하면 코드 일관성이 좋아지고,createEvent가 실패해서 null을 반환하는 경우 불필요한 메서드 호출을 방지할 수 있어요! 😊♻️ 제안하는 수정
String eventId = googleCalendarService.createEvent(reservation); - reservation.updateGoogleEventId(eventId); + if (eventId != null) { + reservation.updateGoogleEventId(eventId); + }🤖 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/nailagent/backend/domain/reservation/service/ReservationService.java` around lines 153 - 154, Add a null check for the Google Calendar event ID returned by googleCalendarService.createEvent(...) and only call reservation.updateGoogleEventId(eventId) when eventId is not null; specifically, after invoking googleCalendarService.createEvent(reservation) into the local variable eventId, guard the call to reservation.updateGoogleEventId(eventId) with a check (eventId != null) so you avoid unnecessary updates when createEvent fails or returns null and keep behavior consistent with updateReservation/deleteReservation.
🤖 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.
Nitpick comments:
In
`@src/main/java/com/nailagent/backend/domain/reservation/service/ReservationService.java`:
- Around line 153-154: Add a null check for the Google Calendar event ID
returned by googleCalendarService.createEvent(...) and only call
reservation.updateGoogleEventId(eventId) when eventId is not null; specifically,
after invoking googleCalendarService.createEvent(reservation) into the local
variable eventId, guard the call to reservation.updateGoogleEventId(eventId)
with a check (eventId != null) so you avoid unnecessary updates when createEvent
fails or returns null and keep behavior consistent with
updateReservation/deleteReservation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 541656df-5dc1-41b4-a757-314c923a65d1
📒 Files selected for processing (1)
src/main/java/com/nailagent/backend/domain/reservation/service/ReservationService.java
💡 개요
Issue Number: #17
🪐 주요 변경 사항
✅ 상세 내용
build.gradle에 Google Calendar API 의존성 추가GoogleCalendarService구현 (이벤트 생성/수정/삭제)Reservation엔티티에googleEventId필드 추가ReservationService에 예약 생성/수정/삭제 시 캘린더 연동 호출application.yml에 Google Calendar 설정 추가GOOGLE_CREDENTIALS_JSONSecret을 EC2에 파일로 배포🔔 참고 사항
google_event_id컬럼 추가 필요:ALTER TABLE reservation ADD COLUMN google_event_id VARCHAR(255);Google Calendar 통합 기능 추가
변경 목적:
예약 생성/수정/삭제 시 Google Calendar API와 동기화하여 예약 정보를 자동으로 구글 캘린더에 반영합니다. 캘린더 동기화 실패가 예약 작업을 방해하지 않도록 격리하여 처리합니다.
주요 변경 내용:
build.gradle에 추가GoogleCalendarService새 클래스 추가 (이벤트 생성/수정/삭제, 크레덴셜 관리)ReservationService에 캘린더 연동 로직 추가 (생성/수정/삭제 시 이벤트 동기화)Reservation에googleEventId필드 및updateGoogleEventId()메서드 추가application.yml에google.calendar설정 블록 추가 (ID, 크레덴셜 경로).env파일에GOOGLE_CALENDAR_ID,GOOGLE_CREDENTIALS_PATH환경변수 처리cd.yml워크플로우에 Google 크레덴셜 환경변수 및 JSON 파일(~/app/google-credentials.json) 배포 로직 추가ErrorCode에GOOGLE_CALENDAR_SYNC_FAILED추가.gitignore에docs/,google-credentials.json추가영향 범위:
reservation테이블에google_event_idVARCHAR(255) 컬럼 추가 필요calendar_sync필드 제거 및ReservationCreateResponse클래스 삭제