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
10 changes: 8 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_KEY }}
envs: DB_URL,DB_USERNAME,DB_PASSWORD,TOSS_CLIENT_KEY,TOSS_SECRET_KEY
envs: DB_URL,DB_USERNAME,DB_PASSWORD,TOSS_CLIENT_KEY,TOSS_SECRET_KEY,GOOGLE_CALENDAR_ID,GOOGLE_CREDENTIALS_JSON
script: |
cat > ~/app/.env << EOF
SPRING_PROFILES_ACTIVE=prod
Expand All @@ -60,11 +60,17 @@ jobs:
DB_PASSWORD=$DB_PASSWORD
TOSS_CLIENT_KEY=$TOSS_CLIENT_KEY
TOSS_SECRET_KEY=$TOSS_SECRET_KEY
GOOGLE_CALENDAR_ID=$GOOGLE_CALENDAR_ID
EOF
echo "$GOOGLE_CREDENTIALS_JSON" > ~/app/google-credentials.json
chmod 600 ~/app/google-credentials.json
echo "GOOGLE_CREDENTIALS_PATH=file:$(realpath ~/app/google-credentials.json)" >> ~/app/.env
Comment thread
coderabbitai[bot] marked this conversation as resolved.
sudo systemctl restart app
env:
DB_URL: ${{ secrets.DB_URL }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
TOSS_CLIENT_KEY: ${{ secrets.TOSS_CLIENT_KEY }}
TOSS_SECRET_KEY: ${{ secrets.TOSS_SECRET_KEY }}
TOSS_SECRET_KEY: ${{ secrets.TOSS_SECRET_KEY }}
GOOGLE_CALENDAR_ID: ${{ secrets.GOOGLE_CALENDAR_ID }}
GOOGLE_CREDENTIALS_JSON: ${{ secrets.GOOGLE_CREDENTIALS_JSON }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ out/

### Docs ###
docs/

### Google Credentials ###
src/main/resources/google-credentials.json
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ dependencies {
// Thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// Google Calendar API
implementation 'com.google.apis:google-api-services-calendar:v3-rev20220715-2.0.0'
implementation 'com.google.auth:google-auth-library-oauth2-http:1.23.0'
implementation 'com.google.http-client:google-http-client-jackson2:1.44.1'

// dotenv
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public class Reservation extends BaseEntity {

private Integer paidAmount;

private String googleEventId;

@Builder
public Reservation(Long customerId, String name, String phoneNum, LocalDate reserveDate,
String reserveTime, Integer estimatedDurationMin, String service,
Expand Down Expand Up @@ -85,6 +87,10 @@ public void cancelPayment() {
this.paymentStatus = PaymentStatus.CANCELLED;
}

public void updateGoogleEventId(String googleEventId) {
this.googleEventId = googleEventId;
}

public enum VisitStatus {
PENDING, CONFIRMED, VISITED, NO_SHOW
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.nailagent.backend.domain.reservation.repository.ReservationRepository;
import com.nailagent.backend.domain.shopinfo.entity.Shopinfo;
import com.nailagent.backend.domain.shopinfo.repository.ShopinfoRepository;
import com.nailagent.backend.global.calendar.GoogleCalendarService;
import com.nailagent.backend.global.exception.CustomException;
import com.nailagent.backend.global.exception.ErrorCode;

Expand All @@ -31,6 +32,7 @@ public class ReservationService {
private final ReservationRepository reservationRepository;
private final ShopinfoRepository shopinfoRepository;
private final CustomerRepository customerRepository;
private final GoogleCalendarService googleCalendarService;

public ScheduleResponse getSchedule(LocalDate date) {

Expand Down Expand Up @@ -147,6 +149,10 @@ public Long createReservation(ReservationRequest request) {
.build();

reservationRepository.save(reservation);

String eventId = googleCalendarService.createEvent(reservation);
reservation.updateGoogleEventId(eventId);

return reservation.getId();
}

Expand All @@ -155,12 +161,21 @@ public void updateReservation(Long reservationId, ReservationUpdateRequest reque
Reservation reservation = reservationRepository.findById(reservationId)
.orElseThrow(() -> new CustomException(ErrorCode.RESERVATION_NOT_FOUND));
reservation.update(request);

if (reservation.getGoogleEventId() != null) {
googleCalendarService.updateEvent(reservation.getGoogleEventId(), reservation);
}
}

@Transactional
public void deleteReservation(Long reservationId) {
Reservation reservation = reservationRepository.findById(reservationId)
.orElseThrow(() -> new CustomException(ErrorCode.RESERVATION_NOT_FOUND));

if (reservation.getGoogleEventId() != null) {
googleCalendarService.deleteEvent(reservation.getGoogleEventId());
}

reservationRepository.delete(reservation);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.nailagent.backend.global.calendar;

import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.calendar.Calendar;
import com.google.api.services.calendar.CalendarScopes;
import com.google.api.services.calendar.model.Event;
import com.google.api.services.calendar.model.EventDateTime;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.nailagent.backend.domain.reservation.entity.Reservation;
import com.nailagent.backend.global.exception.CustomException;
import com.nailagent.backend.global.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;

@Slf4j
@Service
public class GoogleCalendarService {

private Calendar calendar;
private final String calendarId;

public GoogleCalendarService(
@Value("${google.calendar.id}") String calendarId,
@Value("${google.calendar.credentials-path}") String credentialsPath,
ResourceLoader resourceLoader
) {
this.calendarId = calendarId;

try {
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();
} catch (Exception e) {
log.error("Google Calendar ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์‹คํŒจ - ์บ˜๋ฆฐ๋” ์—ฐ๋™์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค", e);
this.calendar = null;
}
}
Comment on lines +34 to +56
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major | โšก Quick win

์บ˜๋ฆฐ๋” ์ดˆ๊ธฐํ™” ์‹คํŒจ๊ฐ€ ์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ธฐ๋™ ์‹คํŒจ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒ์„ฑ์ž์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋‚˜๋ฉด ๋นˆ ์ƒ์„ฑ์ด ์ค‘๋‹จ๋˜์–ด ์˜ˆ์•ฝ ๊ธฐ๋Šฅ๊นŒ์ง€ ๊ฐ™์ด ์ฃฝ์Šต๋‹ˆ๋‹ค. ์บ˜๋ฆฐ๋” ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์‹คํŒจ๋Š” ๋กœ๊ทธ ํ›„ ๋น„ํ™œ์„ฑ ๋ชจ๋“œ๋กœ ์ „ํ™˜ํ•˜๊ณ , 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) {
if (calendar == null) throw new CustomException(ErrorCode.GOOGLE_CALENDAR_SYNC_FAILED);
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);
throw new CustomException(ErrorCode.GOOGLE_CALENDAR_SYNC_FAILED);
}
Comment on lines +58 to +68
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major | โšก Quick win

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.

}

public void updateEvent(String eventId, Reservation reservation) {
if (calendar == null) return;
try {
Event event = buildEvent(reservation);
calendar.events().update(calendarId, eventId, event).execute();
log.info("Google Calendar ์ด๋ฒคํŠธ ์ˆ˜์ •: {}", eventId);
} catch (IOException e) {
log.error("Google Calendar ์ด๋ฒคํŠธ ์ˆ˜์ • ์‹คํŒจ: eventId={}", eventId, e);
}
}

public void deleteEvent(String eventId) {
if (calendar == null) return;
try {
calendar.events().delete(calendarId, eventId).execute();
log.info("Google Calendar ์ด๋ฒคํŠธ ์‚ญ์ œ: {}", eventId);
} catch (IOException e) {
log.error("Google Calendar ์ด๋ฒคํŠธ ์‚ญ์ œ ์‹คํŒจ: eventId={}", eventId, e);
}
}

private Event buildEvent(Reservation reservation) {
// reserveTime ํ˜•์‹: "17:00-18:30"
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]);

LocalDateTime startDt = reservation.getReserveDate().atTime(startTime);
LocalDateTime endDt = reservation.getReserveDate().atTime(endTime);

String summary = String.format("[%s] %s", reservation.getService(), reservation.getName());
String description = String.format(
"๊ณ ๊ฐ๋ช…: %s\n์—ฐ๋ฝ์ฒ˜: %s\n์„œ๋น„์Šค: %s\n์˜คํ”„ ์ œ๊ฑฐ: %s\n๋‹ด๋‹น: %s\n์˜ˆ์•ฝ๊ธˆ: %s์›",
reservation.getName(),
reservation.getPhoneNum(),
reservation.getService(),
Boolean.TRUE.equals(reservation.getOffRemoval()) ? "O" : "X",
reservation.getDesigner(),
reservation.getDepositAmount() != null ? reservation.getDepositAmount() : 0
);

return new Event()
.setSummary(summary)
.setDescription(description)
.setStart(new EventDateTime()
.setDateTime(toGoogleDateTime(startDt))
.setTimeZone("Asia/Seoul"))
.setEnd(new EventDateTime()
.setDateTime(toGoogleDateTime(endDt))
.setTimeZone("Asia/Seoul"));
}

private com.google.api.client.util.DateTime toGoogleDateTime(LocalDateTime ldt) {
return new com.google.api.client.util.DateTime(
Date.from(ldt.atZone(ZoneId.of("Asia/Seoul")).toInstant())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ public enum ErrorCode {
PAYMENT_NOT_PAID(HttpStatus.BAD_REQUEST, "PAYMENT_NOT_PAID", "๊ฒฐ์ œ ์™„๋ฃŒ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ์–ด์„œ ํ™˜๋ถˆํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),

// Shopinfo
SHOPINFO_NOT_FOUND(HttpStatus.NOT_FOUND, "SHOPINFO_NOT_FOUND", "์ƒต ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
SHOPINFO_NOT_FOUND(HttpStatus.NOT_FOUND, "SHOPINFO_NOT_FOUND", "์ƒต ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),

// Google Calendar
GOOGLE_CALENDAR_SYNC_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "GOOGLE_CALENDAR_SYNC_FAILED", "๊ตฌ๊ธ€ ์บ˜๋ฆฐ๋” ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ toss:
client-key: ${TOSS_CLIENT_KEY}
secret-key: ${TOSS_SECRET_KEY}

google:
calendar:
id: ${GOOGLE_CALENDAR_ID}
credentials-path: ${GOOGLE_CREDENTIALS_PATH:classpath:google-credentials.json}

springdoc:
swagger-ui:
path: /swagger-ui.html
Expand Down
Loading