Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
@RequestMapping("/api/v1")
public class CalendarController {

private final CalendarService calendarService;
private final CalendarFacade calendarFacade;

@Value("${app.frontend.url}")
private String frontendUrl;
Expand All @@ -38,7 +38,7 @@ public void subscribeGoogleCalendar(@RequestParam(value = "code", required = fal
if (error != null) {
log.error("[subscribeGoogleCalendar] error: {}", error);
}
calendarService.linkGoogleCalender(code);
calendarFacade.linkGoogleCalendar(code);

String redirectUrl = UriComponentsBuilder.fromUriString(frontendUrl)
.path(frontendCallback)
Expand All @@ -49,7 +49,7 @@ public void subscribeGoogleCalendar(@RequestParam(value = "code", required = fal

@GetMapping("/calendar/list")
public ResponseEntity<ListResponse<CalendarResponse>> calendarList() {
ListResponse<CalendarResponse> response = calendarService.getCalendarList();
ListResponse<CalendarResponse> response = calendarFacade.getCalendarList();
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package endolphin.backend.domain.calendar;

import endolphin.backend.domain.calendar.dto.CalendarResponse;
import endolphin.backend.domain.calendar.entity.Calendar;
import endolphin.backend.domain.calendar.event.GoogleCalendarLinkEvent;
import endolphin.backend.domain.user.UserService;
import endolphin.backend.domain.user.entity.User;
import endolphin.backend.global.dto.ListResponse;
import endolphin.backend.global.google.GoogleCalendarService;
import endolphin.backend.global.google.GoogleOAuthService;
import endolphin.backend.global.google.dto.GoogleCalendarDto;
import endolphin.backend.global.google.dto.GoogleTokens;
import endolphin.backend.global.google.dto.GoogleUserInfo;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CalendarFacade {
Comment on lines +19 to +21
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add transaction management to the facade.

The facade orchestrates multiple service calls that modify data but lacks @Transactional annotation. If any step in linkGoogleCalendar fails (e.g., after token update but before calendar attachment), it could leave the system in an inconsistent state.

+import org.springframework.transaction.annotation.Transactional;
+
 @Service
+@Transactional
 @RequiredArgsConstructor
 public class CalendarFacade {
🤖 Prompt for AI Agents
In backend/src/main/java/endolphin/backend/domain/calendar/CalendarFacade.java
around lines 19 to 21, the CalendarFacade class is missing the @Transactional
annotation. Add @Transactional to the class declaration to ensure that all
service calls within methods like linkGoogleCalendar are executed within a
transaction, preventing partial updates and maintaining data consistency if any
step fails.

private final CalendarService calendarService;
private final UserService userService;

private final GoogleCalendarService googleCalendarService;
private final GoogleOAuthService googleOAuthService;

private final ApplicationEventPublisher eventPublisher;

public void linkGoogleCalendar(String code) {
if (code == null || code.isBlank()) {
return;
}
GoogleTokens tokens = googleOAuthService.getGoogleTokens(code, false);
GoogleUserInfo userInfo = googleOAuthService.getUserInfo(tokens.accessToken());


User user = userService.getUserByEmail(userInfo.email());
user = userService.updateTokens(user, tokens.accessToken(), tokens.refreshToken());

GoogleCalendarDto calendarDto = googleCalendarService.getPrimaryCalendar(user);
calendarService.attachGoogleCalendar(calendarDto, user);

eventPublisher.publishEvent(new GoogleCalendarLinkEvent(user));
}
Comment on lines +30 to +45
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

좋은 facade 패턴 구현이지만 에러 처리와 로깅 개선이 필요합니다.

linkGoogleCalendar 메서드가 OAuth 플로우를 잘 오케스트레이션하고 있어서 원래 버그를 해결할 것 같습니다. 하지만 몇 가지 개선사항이 있습니다.

다음과 같은 개선을 고려해보세요:

public void linkGoogleCalendar(String code) {
    if (code == null || code.isBlank()) {
+        log.warn("[linkGoogleCalendar] Invalid authorization code provided");
        return;
    }
+    
+    try {
+        log.info("[linkGoogleCalendar] Starting Google Calendar linking process");
        GoogleTokens tokens = googleOAuthService.getGoogleTokens(code, false);
        GoogleUserInfo userInfo = googleOAuthService.getUserInfo(tokens.accessToken());

-
        User user = userService.getUserByEmail(userInfo.email());
        user = userService.updateTokens(user, tokens.accessToken(), tokens.refreshToken());

        GoogleCalendarDto calendarDto = googleCalendarService.getPrimaryCalendar(user);
        calendarService.attachGoogleCalendar(calendarDto, user);

+        log.info("[linkGoogleCalendar] Successfully linked calendar for user: {}", user.getEmail());
        eventPublisher.publishEvent(new GoogleCalendarLinkEvent(user));
+    } catch (Exception e) {
+        log.error("[linkGoogleCalendar] Failed to link Google Calendar: {}", e.getMessage(), e);
+        throw e;
+    }
}

또한 이 메서드가 트랜잭션 내에서 실행되어야 하는지 고려해보세요:

+@Transactional
public void linkGoogleCalendar(String code) {
🤖 Prompt for AI Agents
In backend/src/main/java/endolphin/backend/domain/calendar/CalendarFacade.java
around lines 30 to 45, the linkGoogleCalendar method lacks error handling and
logging. Add try-catch blocks to handle potential exceptions from OAuth token
retrieval, user lookup, and calendar attachment. Log meaningful error messages
when exceptions occur to aid debugging. Also, consider annotating the method
with @Transactional if the operations should be atomic to ensure consistency in
case of failures.

🛠️ Refactor suggestion

Implement comprehensive error handling for Google API operations.

Based on the retrieved learnings and Google Calendar integration best practices, this method needs robust error handling for:

  1. GoogleJsonResponseException - Google API errors
  2. IOException - Network failures
  3. Invalid tokens or user info
  4. Calendar attachment failures

Consider wrapping the Google API calls in try-catch blocks and implementing proper rollback mechanisms.

 public void linkGoogleCalendar(String code) {
     if (code == null || code.isBlank()) {
         return;
     }
-    GoogleTokens tokens = googleOAuthService.getGoogleTokens(code, false);
-    GoogleUserInfo userInfo = googleOAuthService.getUserInfo(tokens.accessToken());
-
-
-    User user = userService.getUserByEmail(userInfo.email());
-    user = userService.updateTokens(user, tokens.accessToken(), tokens.refreshToken());
-
-    GoogleCalendarDto calendarDto = googleCalendarService.getPrimaryCalendar(user);
-    calendarService.attachGoogleCalendar(calendarDto, user);
-
-    eventPublisher.publishEvent(new GoogleCalendarLinkEvent(user));
+    try {
+        GoogleTokens tokens = googleOAuthService.getGoogleTokens(code, false);
+        GoogleUserInfo userInfo = googleOAuthService.getUserInfo(tokens.accessToken());
+        
+        User user = userService.getUserByEmail(userInfo.email());
+        user = userService.updateTokens(user, tokens.accessToken(), tokens.refreshToken());
+        
+        GoogleCalendarDto calendarDto = googleCalendarService.getPrimaryCalendar(user);
+        calendarService.attachGoogleCalendar(calendarDto, user);
+        
+        eventPublisher.publishEvent(new GoogleCalendarLinkEvent(user));
+    } catch (GoogleJsonResponseException e) {
+        log.error("Google API error during calendar linking: {}", e.getMessage());
+        throw new CalendarException(ErrorCode.GC_API_ERROR, e);
+    } catch (IOException e) {
+        log.error("Network error during calendar linking: {}", e.getMessage());
+        throw new CalendarException(ErrorCode.NETWORK_ERROR, e);
+    }
 }
📝 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
public void linkGoogleCalendar(String code) {
if (code == null || code.isBlank()) {
return;
}
GoogleTokens tokens = googleOAuthService.getGoogleTokens(code, false);
GoogleUserInfo userInfo = googleOAuthService.getUserInfo(tokens.accessToken());
User user = userService.getUserByEmail(userInfo.email());
user = userService.updateTokens(user, tokens.accessToken(), tokens.refreshToken());
GoogleCalendarDto calendarDto = googleCalendarService.getPrimaryCalendar(user);
calendarService.attachGoogleCalendar(calendarDto, user);
eventPublisher.publishEvent(new GoogleCalendarLinkEvent(user));
}
public void linkGoogleCalendar(String code) {
if (code == null || code.isBlank()) {
return;
}
try {
GoogleTokens tokens = googleOAuthService.getGoogleTokens(code, false);
GoogleUserInfo userInfo = googleOAuthService.getUserInfo(tokens.accessToken());
User user = userService.getUserByEmail(userInfo.email());
user = userService.updateTokens(user, tokens.accessToken(), tokens.refreshToken());
GoogleCalendarDto calendarDto = googleCalendarService.getPrimaryCalendar(user);
calendarService.attachGoogleCalendar(calendarDto, user);
eventPublisher.publishEvent(new GoogleCalendarLinkEvent(user));
} catch (GoogleJsonResponseException e) {
log.error("Google API error during calendar linking: {}", e.getMessage());
throw new CalendarException(ErrorCode.GC_API_ERROR, e);
} catch (IOException e) {
log.error("Network error during calendar linking: {}", e.getMessage());
throw new CalendarException(ErrorCode.NETWORK_ERROR, e);
}
}
🤖 Prompt for AI Agents
In backend/src/main/java/endolphin/backend/domain/calendar/CalendarFacade.java
around lines 30 to 45, the method linkGoogleCalendar lacks error handling for
Google API calls and related operations. Wrap the calls to
googleOAuthService.getGoogleTokens, googleOAuthService.getUserInfo, userService
methods, googleCalendarService.getPrimaryCalendar, and
calendarService.attachGoogleCalendar in try-catch blocks to handle
GoogleJsonResponseException, IOException, and other potential exceptions. Add
checks for invalid tokens or user info and handle failures gracefully. Implement
rollback or compensation logic if calendar attachment fails after tokens are
updated. Ensure exceptions are logged or propagated appropriately.


public ListResponse<CalendarResponse> getCalendarList() {
User user = userService.getCurrentUser();

List<Calendar> calendars = calendarService.findAllByUserId(user.getId());

List<CalendarResponse> response = calendars.stream()
.map(c -> new CalendarResponse(c.getName())).toList();
return new ListResponse<>(response);
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
package endolphin.backend.domain.calendar;

import endolphin.backend.domain.calendar.dto.CalendarResponse;
import endolphin.backend.domain.calendar.entity.Calendar;
import endolphin.backend.domain.calendar.event.GoogleCalendarLinkEvent;
import endolphin.backend.domain.user.UserService;
import endolphin.backend.domain.user.entity.User;
import endolphin.backend.global.dto.ListResponse;
import endolphin.backend.global.error.exception.CalendarException;
import endolphin.backend.global.error.exception.ErrorCode;
import endolphin.backend.global.google.GoogleOAuthService;
import endolphin.backend.global.google.dto.GoogleCalendarDto;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import endolphin.backend.global.google.dto.GoogleTokens;
import endolphin.backend.global.google.dto.GoogleUserInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -34,17 +25,14 @@
public class CalendarService {

private final CalendarRepository calendarRepository;
private final GoogleOAuthService googleOAuthService;
private final UserService userService;
private final ApplicationEventPublisher eventPublisher;

public Calendar attachGoogleCalendar(GoogleCalendarDto googleCalendarDto, User user) {
public void attachGoogleCalendar(GoogleCalendarDto googleCalendarDto, User user) {
Calendar calendar = Calendar.builder()
.user(user)
.calendarId(googleCalendarDto.id())
.name(googleCalendarDto.summary())
.description(googleCalendarDto.description()).build();
return calendarRepository.save(calendar);
calendarRepository.save(calendar);
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -106,30 +94,8 @@ public Map<Long, String> getCalendarIdByUsers(List<Long> userIds) {
));
}

public void linkGoogleCalender(String code) {
if (code == null || code.isBlank()) {
return;
}
GoogleTokens tokens = googleOAuthService.getGoogleTokens(code, false);
GoogleUserInfo userInfo = googleOAuthService.getUserInfo(tokens.accessToken());

User user = userService.getUserByEmail(userInfo.email());
if (user.getAccessToken() != null) {
return;
}
user.setAccessToken(tokens.accessToken());
user.setRefreshToken(tokens.refreshToken());

eventPublisher.publishEvent(new GoogleCalendarLinkEvent(user));
}

public ListResponse<CalendarResponse> getCalendarList() {
User user = userService.getCurrentUser();

List<Calendar> calendars = calendarRepository.findAllByUserId(user.getId());

List<CalendarResponse> response = calendars.stream()
.map(c -> new CalendarResponse(c.getName())).toList();
return new ListResponse<>(response);
@Transactional(readOnly = true)
public List<Calendar> findAllByUserId(Long userId) {
return calendarRepository.findAllByUserId(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ public ResponseEntity<ListResponse<PersonalEventResponse>> getPersonalEvents(
@PostMapping
public ResponseEntity<PersonalEventResponse> createPersonalEvent(
@Valid @RequestBody PersonalEventRequest request) {
PersonalEvent personalEvent = personalEventService.createWithRequest(request);
PersonalEventResponse response = PersonalEventResponse.fromEntity(personalEvent);
PersonalEventResponse response = personalEventService.createWithRequest(request);
URI location = URIUtil.buildResourceUri(response.id());
return ResponseEntity.created(location).body(response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,26 @@ public ListResponse<PersonalEventResponse> listPersonalEvents(
return new ListResponse<>(personalEventResponseList);
}

public PersonalEvent createWithRequest(PersonalEventRequest request) {
public PersonalEventResponse createWithRequest(PersonalEventRequest request) {
User user = userService.getCurrentUser();

List<Discussion> discussions = discussionParticipantService
.getDiscussionsByUserId(user.getId());

PersonalEvent personalEvent = createPersonalEvent(request, user, discussions);

if (request.syncWithGoogleCalendar()) {
String calendarId = calendarService.getCalendarIdByUser(user);
personalEvent.setGoogleEventId(IdGenerator.generateId(user.getId()));
personalEvent.setCalendarId(calendarId);
eventPublisher.publishEvent(new InsertPersonalEvent(List.of(personalEvent)));
}

return PersonalEventResponse.fromEntity(personalEvent);
}

private PersonalEvent createPersonalEvent(PersonalEventRequest request, User user,
List<Discussion> discussions) {
Validator.validateDateTimeRange(request.startDateTime(), request.endDateTime());

PersonalEvent personalEvent = PersonalEvent.builder()
Expand All @@ -79,29 +96,17 @@ public PersonalEvent createWithRequest(PersonalEventRequest request) {
.user(user)
.build();

PersonalEvent result = personalEventRepository.save(personalEvent);

if (request.syncWithGoogleCalendar()) {
String calendarId = calendarService.getCalendarIdByUser(user);
personalEvent.setGoogleEventId(IdGenerator.generateId(user.getId()));
personalEvent.setCalendarId(calendarId);
eventPublisher.publishEvent(new InsertPersonalEvent(List.of(personalEvent)));
}

List<Discussion> discussions = discussionParticipantService.getDiscussionsByUserId(
user.getId());
PersonalEvent savedPersonalEvent = personalEventRepository.save(personalEvent);

discussions.forEach(discussion -> {
personalEventPreprocessor.preprocessOne(result, discussion, user,
true);
personalEventPreprocessor.preprocessOne(savedPersonalEvent, discussion, user, true);
});

return result;
return savedPersonalEvent;
}

public void createPersonalEventsForParticipants(List<User> participants,
Discussion discussion,
SharedEventDto sharedEvent) {
Discussion discussion, SharedEventDto sharedEvent) {
List<Long> userIds = participants.stream().map(User::getId).toList();
Map<Long, String> calendarIdMap = calendarService.getCalendarIdByUsers(userIds);
List<PersonalEvent> events = participants.stream()
Expand Down Expand Up @@ -251,14 +256,14 @@ private Optional<SyncPersonalEvent> syncPersonalEventFromGoogleEvent(
if (personalEventOpt.isPresent()) {
PersonalEvent personalEvent = personalEventOpt.get();
PersonalEvent updatedPersonalEvent = updatePersonalEvent(
PersonalEventRequest.of(googleEvent, personalEvent.getIsAdjustable()),
personalEvent, user, discussions);
PersonalEventRequest.of(googleEvent, personalEvent.getIsAdjustable()),
personalEvent, user, discussions);

yield Optional.of(SyncPersonalEvent.from(updatedPersonalEvent, Status.UPDATED));
}

PersonalEvent personalEvent =
createWithRequest(PersonalEventRequest.of(googleEvent, false));
PersonalEvent personalEvent = createPersonalEvent(
PersonalEventRequest.of(googleEvent, false), user, discussions);

yield Optional.of(SyncPersonalEvent.from(personalEvent, Status.CREATED));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ public void updateAccessToken(User user, String accessToken) {
userRepository.save(user);
}

public User updateTokens(User user, String accessToken, String refreshToken) {
if (user.getAccessToken() != null) {
return user;
}
user.setAccessToken(accessToken);
user.setRefreshToken(refreshToken);

return userRepository.save(user);
}
Comment on lines +47 to +55
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

토큰 업데이트 로직 개선이 필요합니다.

새로운 updateTokens 메서드가 추가되었지만, guard clause 로직에 개선이 필요합니다. 현재는 accessToken만 확인하고 있어서 refreshToken이 누락된 경우를 처리하지 못할 수 있습니다.

다음과 같이 개선하는 것을 고려해보세요:

public User updateTokens(User user, String accessToken, String refreshToken) {
-    if (user.getAccessToken() != null) {
+    if (user.getAccessToken() != null && user.getRefreshToken() != null) {
        return user;
    }
    user.setAccessToken(accessToken);
    user.setRefreshToken(refreshToken);

    return userRepository.save(user);
}

또는 더 명확하게 하려면:

public User updateTokens(User user, String accessToken, String refreshToken) {
-    if (user.getAccessToken() != null) {
+    if (user.getAccessToken() != null && user.getRefreshToken() != null) {
        return user;
    }
+    if (accessToken != null) {
        user.setAccessToken(accessToken);
+    }
+    if (refreshToken != null) {
        user.setRefreshToken(refreshToken);
+    }

    return userRepository.save(user);
}
📝 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
public User updateTokens(User user, String accessToken, String refreshToken) {
if (user.getAccessToken() != null) {
return user;
}
user.setAccessToken(accessToken);
user.setRefreshToken(refreshToken);
return userRepository.save(user);
}
public User updateTokens(User user, String accessToken, String refreshToken) {
- if (user.getAccessToken() != null) {
+ if (user.getAccessToken() != null && user.getRefreshToken() != null) {
return user;
}
user.setAccessToken(accessToken);
user.setRefreshToken(refreshToken);
return userRepository.save(user);
}
🤖 Prompt for AI Agents
In backend/src/main/java/endolphin/backend/domain/user/UserService.java around
lines 47 to 55, the updateTokens method currently only checks if the user's
accessToken is non-null before returning early, which misses cases where the
refreshToken might be missing. To fix this, update the guard clause to check if
both accessToken and refreshToken are non-null and return the user early only if
both tokens are present. Otherwise, proceed to set the new tokens and save the
user.


public LoginUserDto upsertUser(GoogleUserInfo userInfo) {
Optional<User> userOpt = userRepository.findByEmail(userInfo.email());
Boolean isFirstLogin = userOpt.isEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,12 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class GoogleCalendarService {

Expand All @@ -55,13 +53,6 @@ public class GoogleCalendarService {
private final RetryExecutor retryExecutor;
private final ApplicationEventPublisher eventPublisher;

public void upsertGoogleCalendar(User user) {
GoogleCalendarDto googleCalendarDto = getPrimaryCalendar(user);
Calendar calendar = calendarService.attachGoogleCalendar(googleCalendarDto, user);
syncWithCalendar(calendar.getCalendarId(), user);
subscribeToCalendar(calendar, user);
}

public void subscribeGoogleCalendar(User user) {
Calendar calendar = calendarService.getCalendarByUserId(user.getId());
subscribeToCalendar(calendar, user);
Expand Down Expand Up @@ -146,8 +137,8 @@ public GoogleCalendarDto getPrimaryCalendar(User user) {

return retryExecutor.executeCalendarApiWithRetry(
() -> {
GoogleCalendarDto result = googleCalendarApi.getPrimaryCalendar(
eventsUrl, user.getAccessToken());
GoogleCalendarDto result =
googleCalendarApi.getPrimaryCalendar(eventsUrl, user.getAccessToken());
Comment on lines +140 to +141
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify transaction management after @transactional removal.

The formatting change is fine, but the removal of @Transactional from this service (mentioned in the summary) needs verification. This service still calls calendarService methods that perform database operations. Ensure that:

  1. Transactions are properly managed at the CalendarFacade level
  2. Database operations in methods like syncWithCalendar() are still atomic
  3. Rollback behavior is preserved for failed Google API calls

🏁 Script executed:

#!/bin/bash
# Verify that CalendarFacade or calling methods have proper transaction management
ast-grep --pattern $'@Transactional
class CalendarFacade {
  $$$
}'

# Check if calendarService calls are within transactional contexts
ast-grep --pattern $'calendarService.$_($$$)'

Length of output: 3564


🏁 Script executed:

#!/bin/bash
# Check for @Transactional annotation on CalendarService
rg '@Transactional' -n backend/src/main/java/endolphin/backend/domain/calendar/CalendarService.java || echo "No @Transactional in CalendarService"

# Check for @Service annotation on CalendarService
rg '@Service' -n backend/src/main/java/endolphin/backend/domain/calendar/CalendarService.java || echo "No @Service in CalendarService"

Length of output: 418


Reintroduce @Transactional on GoogleCalendarService to restore atomicity

The removal of @Transactional from this service means each calendarService call runs in its own transaction (CalendarService is annotated at class‐level), but multi‐step flows in GoogleCalendarService (e.g. syncWithCalendar) are no longer wrapped in a single transaction. If you need all DB updates and the Google API call to roll back together, re-add @Transactional at the class or method level.

Locations to update:

  • backend/src/main/java/endolphin/backend/global/google/GoogleCalendarService.java (class declaration or specific methods like syncWithCalendar)

Suggested diff:

@@ backend/src/main/java/endolphin/backend/global/google/GoogleCalendarService.java
- public class GoogleCalendarService {
+ @Transactional
+ public class GoogleCalendarService {
📝 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
GoogleCalendarDto result =
googleCalendarApi.getPrimaryCalendar(eventsUrl, user.getAccessToken());
@Transactional
public class GoogleCalendarService {
// … rest of class …
}
🤖 Prompt for AI Agents
In
backend/src/main/java/endolphin/backend/global/google/GoogleCalendarService.java
around lines 140-141, the @Transactional annotation was removed, causing
multi-step flows like syncWithCalendar to no longer run within a single
transaction. To fix this, re-add the @Transactional annotation either at the
class level or on specific methods such as syncWithCalendar to ensure all
database updates and Google API calls are executed atomically and can roll back
together on failure.

log.info("[getPrimaryCalendar] success: {}", result);
return result;
}, user, null
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

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

before commit으로 하신 이유가 있을까여?

Copy link
Collaborator

Choose a reason for hiding this comment

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

잘 몰라서 그냥 질문임당

Copy link
Contributor Author

Choose a reason for hiding this comment

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

기존에 비동기로 처리할 때는 AFTER_COMMIT이어도 다른 쓰레드로 실행되어 새로운 트랜잭션을 시작하게 됩니다. 하지만 동기로 처리할 때 AFTER_COMMIT인 상태라면 이전의 트랜잭션 내에서 실행하게 되는데, 이 때 commit이 실행된 이후이기 때문에 이후에 실행되는 SQL이 commit되지 않는 문제가 있다고 합니다. 그래서 BEFORE_COMMIT으로 하게 되었습니다.

그런데 더 찾아보니 현재 구조에서 BEFORE_COMMIT을 하면 문제가 생길 거 같아서 수정하고 다시 올리겠습니다

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package endolphin.backend.global.google.event.handler;

import endolphin.backend.domain.calendar.entity.Calendar;
import endolphin.backend.domain.calendar.event.GoogleCalendarLinkEvent;
import endolphin.backend.domain.personal_event.entity.PersonalEvent;
import endolphin.backend.domain.personal_event.event.DeletePersonalEvent;
Expand All @@ -11,6 +10,7 @@
import endolphin.backend.domain.personal_event.event.InsertPersonalEvent;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
Expand All @@ -30,15 +30,15 @@ public void insert(InsertPersonalEvent event) {
}

@Async
@TransactionalEventListener(classes = {GoogleCalendarLinkEvent.class})
public void link(GoogleCalendarLinkEvent event) {
@TransactionalEventListener(classes = {LoginEvent.class})
public void link(LoginEvent event) {
User user = event.user();
googleCalendarService.upsertGoogleCalendar(user);
googleCalendarService.subscribeGoogleCalendar(user);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

GoogleCalendarService의 메서드 변경 확인 필요

upsertGoogleCalendar에서 subscribeGoogleCalendar로 메서드가 변경되었습니다. 새로운 메서드의 구현과 기능을 확인해주세요.


🏁 Script executed:

#!/bin/bash
# Description: subscribeGoogleCalendar 메서드의 구현을 확인합니다.

# GoogleCalendarService에서 새로운 메서드 검색
ast-grep --pattern 'subscribeGoogleCalendar($_) {
  $$$
}'

# 이전 upsertGoogleCalendar 메서드가 제거되었는지 확인
rg -A 5 "upsertGoogleCalendar"

Length of output: 1572


Missing subscribeGoogleCalendar implementation in GoogleCalendarService

The subscribeGoogleCalendar(user) call was added on line 36, but there’s no corresponding method in GoogleCalendarService. Meanwhile, tests still refer to the old upsertGoogleCalendar, which has been removed from the main code.

Please add or locate the subscribeGoogleCalendar(User user) method in:

  • backend/src/main/java/endolphin/backend/global/google/GoogleCalendarService.java

And update the tests in:

  • backend/src/test/java/endolphin/backend/global/google/GoogleCalendarServiceTest.java

so that they no longer reference upsertGoogleCalendar and validate the new subscribeGoogleCalendar behavior.

🤖 Prompt for AI Agents
In
backend/src/main/java/endolphin/backend/global/google/GoogleCalendarService.java
around line 36, add the missing subscribeGoogleCalendar(User user) method to
implement the functionality called in GoogleEventHandler.java. Then, in
backend/src/test/java/endolphin/backend/global/google/GoogleCalendarServiceTest.java,
remove references to the old upsertGoogleCalendar method and replace them with
tests that validate the new subscribeGoogleCalendar method's behavior.

}

@Async
@TransactionalEventListener(classes = {LoginEvent.class})
public void link(LoginEvent event) {
@EventListener(classes = {GoogleCalendarLinkEvent.class})
public void link(GoogleCalendarLinkEvent event) {
User user = event.user();
googleCalendarService.subscribeGoogleCalendar(user);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ void createPersonalEvent() throws Exception {
"isAdjustable": false
}
""";
PersonalEvent personalEvent = mock(PersonalEvent.class);
given(personalEvent.getId()).willReturn(1L);
// PersonalEvent personalEvent = mock(PersonalEvent.class);
// given(personalEvent.getId()).willReturn(1L);
PersonalEventResponse personalEventResponse = new PersonalEventResponse(1L, "title",
LocalDateTime.of(2025, 2, 2, 10, 0),
LocalDateTime.of(2025, 2, 2, 12, 0),
false, "testCalendarId");
given(personalEventService.createWithRequest(any())).willReturn(personalEvent);
given(personalEventService.createWithRequest(any())).willReturn(personalEventResponse);

MvcResult result = mockMvc.perform(post("/api/v1/personal-event").
contentType(MediaType.APPLICATION_JSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ void testCreateWithRequest() {
given(personalEventRepository.save(any(PersonalEvent.class))).willReturn(savedEvent);

// when
PersonalEvent response = personalEventService.createWithRequest(request);
PersonalEventResponse response = personalEventService.createWithRequest(request);

// then
assertThat(response).isNotNull();
assertThat(response.getTitle()).isEqualTo(request.title());
assertThat(response.getStartTime()).isEqualTo(startTime);
assertThat(response.title()).isEqualTo(request.title());
assertThat(response.startDateTime()).isEqualTo(startTime);

verify(userService, times(1)).getCurrentUser();
verify(personalEventRepository, times(1)).save(any(PersonalEvent.class));
Expand Down
Loading
Loading