1. OAuth2 액세스 토큰 여부 확인

Google Meet 링크를 생성하기 위해서는 먼저 구글 로그인 여부를 확인해야 한다. 이것을 확인하는 방법이 바로 OAuth2 액세스 토큰이 있는지 여부를 확인하는 것이다.

기본적으로 액세스 토큰은 1시간이면 만료되므로, 같이 발급되는 리프레쉬 토큰을 통해 만료 10분 전 자동으로 새 액세스 토큰을 발급받는 로직 또한 만들어놨다.

public MeetingResponse createMeeting(String userId, LocalDateTime startTime, Designer designer)  {

        String accessToken = googleTokenService.getValidAccessToken(userId);

        String meetingTitle = generateMeetingTitle(designer.getName(), startTime);

        try {
            ConferenceResponse response = googleMeetClient.createMeeting(
                    accessToken,
                    createConferenceRequest(meetingTitle, startTime, startTime.plusHours(1))
            );
            Meeting meeting = createMeeting2(startTime, meetingTitle, response.hangoutLink());
            meetingRepository.save(meeting);

            return new MeetingResponse(meetingTitle, response.hangoutLink(), meeting.getId());

        } catch (Exception e) {
            throw new RuntimeException("Google Meet 링크 생성에 실패했습니다." + e);
        }
    }

따라서 필자는 이렇게 getValidAccessToken 메서드로 토큰 유효성을 확인한 후에 미팅을 생성할 수 있게 했다.

public String getValidAccessToken(String userId) {
        GoogleJsonWebToken token = googleTokenRepository.findById(userId)
                .orElseThrow(() -> new EntityNotFoundException("Google 토큰을 찾을 수 없습니다."));

        if (token.getExpiresIn() == null || token.getExpiresIn().minusMinutes(10).isBefore(LocalDateTime.now())) {
            OAuth2TokenResponse newToken = tokenService.refreshAccessToken(token.getRefreshToken());

            // 새 토큰 저장
            GoogleJsonWebToken updatedToken = GoogleJsonWebToken.builder()
                    .userId(userId)
                    .accessToken(newToken.accessToken())
                    .refreshToken(token.getRefreshToken()) // refresh token은 유지
                    .expiresIn(LocalDateTime.now().plusHours(1))
                    .build();
            googleTokenRepository.save(updatedToken);

            log.info("리프레쉬 토큰 재발급");

            return "Bearer " + newToken.accessToken();
        }
        return "Bearer " + token.getAccessToken();
    }

getValidAccessToken은 이런 식으로 구성했다. 기본적으로 Id로 로그인한 유저의 토큰을 찾고, 만료 기간이 null이거나 10분 전이면 자동으로 새 토큰을 발급받은 후 리턴하고, 그렇지 않은 경우 기존 토큰을 반환한다.

2. GoogleMeetClient

필자는 이번에 FeignClient를 활용하여 API 호출을 구현했다. (FeignClient의 개념과 활용 관련해서도 포스팅 할 예정)

@FeignClient(
        name = "googleMeetClient",
        url = "<https://www.googleapis.com/calendar/v3>",
        configuration = GoogleMeetFeignConfig.class
)
public interface GoogleMeetClient {
    @PostMapping("/calendars/primary/events?conferenceDataVersion=1")
    ConferenceResponse createMeeting(
            @RequestHeader("Authorization") String accessToken,
            @RequestBody ConferenceRequest request);
}

여기서 "conferenceDataVersion=1" 이란 부분이 있는데, 여기를 1로 설정해줘야 API 호출 시 이를 인식해서 Google Meet 링크를 생성할 수 있다. 또 "primary"는 유저의 기본 캘린더에 이벤트를 추가하는 엔드포인트이다.

3. DTO

DTO를 두 개 설정해줘야 하는데, 하나는 Google과 API 호출 관련한 DTO이고, 다른 하나는 Meeting 엔티티 생성에 관여하는 DTO이다.

먼저 API 통신용 DTO이다. 현재는 record를 활용하여 리팩토링한 상태인데, 설명을 위해 잠깐 다시 합쳐보자면,

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

public class GoogleMeetDto {

    // ✅ 요청 DTO
    public static class Request {

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class ConferenceRequest {
            private String summary; // 미팅 제목
            private EventDateTime start; // 시작 시간
            private EventDateTime end; // 종료 시간
            private ConferenceData conferenceData; // Google Meet 회의 정보
            private int version = 1; // 요청 버전
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class EventDateTime {
            private String dateTime; // ISO 8601 형식 ("2024-02-16T10:00:00Z")
            private String timeZone; // 예: "Asia/Seoul"
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class ConferenceData {
            private CreateConferenceRequest createRequest;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class CreateConferenceRequest {
            private String requestId; // 요청을 구분하는 UUID
            private ConferenceSolutionKey conferenceSolutionKey;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class ConferenceSolutionKey {
            private String type = "hangoutsMeet"; // Google Meet 사용
        }
    }
}

// ✅ 응답 DTO
    public static class Response {

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class ConferenceResponse {
            private String id;
            private String hangoutLink; // Google Meet 링크
            private ConferenceData conferenceData;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class ConferenceData {
            private ConferenceSolution conferenceSolution;
            private CreateConferenceRequest createRequest;
            private EntryPoints[] entryPoints;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class ConferenceSolution {
            private ConferenceSolutionKey key;
            private String name;
            private String iconUri;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class ConferenceSolutionKey {
            private String type;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class CreateConferenceRequest {
            private String requestId;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class EntryPoints {
            private String entryPointType;
            private String uri;
            private String label;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class Key {
            private String type;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class Status {
            private String statusCode;
        }
    }
}