Skip to content

Conversation

@Yujin1219
Copy link
Member

@Yujin1219 Yujin1219 commented Dec 10, 2025

#️⃣ 연관된 이슈

📝 작업 내용

1. 자동 데이터 업데이트 (스케줄러)

  • 매주 월요일 새벽 3시 자동 실행
  • 앱 시작 시 DB가 비어있으면 자동 초기화
  • 항상 7일 후 출발 항공권 제공
  • 60개 이상 노선 × 노선당 최대 3개 = 약 180개 항공권

2. Amadeus API 연동

  • Amadeus API로 매주 최신 가격 갱신
  • DB 저장으로 사용자 요청 시 API 호출 없이 DB에서 조회

3. 자동 환율 변환

  • 10개 통화 지원 (EUR, USD, JPY, CNY, THB 등)
  • 모든 가격을 원화(KRW) 로 자동 변환
    • 예: EUR 98.34 → KRW 142,593원

4. 한국어 공항명 매핑

  • 전 세계 주요 공항 IATA 코드 → 해당 지역으로 한국어로 변환

5. 지역별 대표 이미지 매핑

  • 해당 공항의 국가/도시별 대표 이미지

📌 공유 사항

✅ 체크리스트

  • Reviewer에 팀원들을 선택 했나요?
  • Assignees에 본인을 선택 했나요?
  • Merge 하려는 브랜치가 올바르게 설정되어 있나요?
  • 컨벤션을 지키고 있나요?
  • 로컬에서 실행했을 때 에러가 발생하지 않나요?
  • 불필요한 주석이 제거되었나요?
  • 코드 스타일이 일관적인가요?

스크린샷 (선택)

💬 리뷰 요구사항 (선택)

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요? or 변경 사항 등

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 비행편 목록 조회 API 추가 (커서 기반 페이지네이션 지원)
    • Amadeus 항공사 API 통합으로 실시간 항공편 데이터 제공
    • 매주 항공편 데이터 자동 업데이트 스케줄 구현
    • CORS 설정으로 크로스 도메인 요청 지원

✏️ Tip: You can customize this high-level summary in your review settings.

@Yujin1219 Yujin1219 self-assigned this Dec 10, 2025
@Yujin1219 Yujin1219 added the ✨ feature 새로운 기능 개발 label Dec 10, 2025
@Yujin1219 Yujin1219 linked an issue Dec 10, 2025 that may be closed by this pull request
2 tasks
@Yujin1219 Yujin1219 merged commit 64a845b into develop Dec 10, 2025
2 checks passed
@Yujin1219 Yujin1219 deleted the feat/#18 branch December 10, 2025 11:34
@coderabbitai
Copy link

coderabbitai bot commented Dec 10, 2025

워크스루

Amadeus 항공사 API를 통합하여 항공편 데이터 조회 기능을 추가합니다. 항공편 엔티티, 리포지토리, 컨트롤러 및 서비스를 구현하고, API 응답을 변환하는 컨버터를 제공합니다. 주기적 스케줄링과 보안 설정도 함께 업데이트됩니다.

변경사항

그룹 / 파일 변경 요약
핵심 설정
src/main/java/com/example/triptalk/TriptalkApplication.java, src/main/java/com/example/triptalk/global/config/AmadeusProperties.java, src/main/java/com/example/triptalk/global/config/SecurityConfig.java
스프링 스케줄링 지원 활성화, Amadeus API 설정 속성 클래스 추가, CORS 설정 및 인증 없는 /api/flights 엔드포인트 추가
API 응답 DTO
src/main/java/com/example/triptalk/domain/tripPlan/dto/AmadeusResponse.java, src/main/java/com/example/triptalk/domain/tripPlan/dto/FlightResponse.java
Amadeus API 응답 매핑 DTO, 항공편 조회 응답 DTO 및 페이지 결과 정의
엔티티 및 저장소
src/main/java/com/example/triptalk/domain/tripPlan/entity/Flight.java, src/main/java/com/example/triptalk/domain/tripPlan/repository/FlightRepository.java
항공편 JPA 엔티티 추가, 커서 기반 페이지네이션을 지원하는 쿼리 메서드 정의
변환기
src/main/java/com/example/triptalk/domain/tripPlan/converter/AmadeusConverter.java, src/main/java/com/example/triptalk/domain/tripPlan/converter/FlightConverter.java
Amadeus API 응답을 Flight 엔티티로 변환, Flight 엔티티를 응답 DTO로 변환
서비스 계층
src/main/java/com/example/triptalk/domain/tripPlan/service/AmadeusService.java, src/main/java/com/example/triptalk/domain/tripPlan/service/FlightService.java, src/main/java/com/example/triptalk/domain/tripPlan/service/FlightServiceImpl.java
Amadeus API 인증 및 항공편 검색 기능, 항공편 조회 서비스 인터페이스 및 구현
컨트롤러
src/main/java/com/example/triptalk/domain/tripPlan/controller/FlightController.java
/api/flights 엔드포인트 추가, 커서 기반 항공편 조회 GET 메서드
스케줄러
src/main/java/com/example/triptalk/domain/tripPlan/scheduler/FlightScheduler.java
주간 항공편 데이터 업데이트 및 초기 로드 스케줄 작업
유틸리티
src/main/java/com/example/triptalk/domain/tripPlan/util/AirportNameMapper.java, src/main/java/com/example/triptalk/domain/tripPlan/util/CountryImageMapper.java
IATA 공항 코드를 한국 공항명으로 매핑, 공항 코드를 이미지 URL로 매핑

제안 검토자

  • ys1217-gg
  • davidcho0701

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '[#18] Feat: 항공권 조회 API구현'은 주요 변경 사항인 항공권 조회 API 구현을 명확하고 간결하게 설명하고 있습니다.
Description check ✅ Passed PR 설명은 관련 이슈(#18), 작업 내용(5가지 주요 기능)을 포함하고 있으나 공유 사항과 체크리스트 항목들이 미완성 상태입니다.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#18

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (12)
src/main/java/com/example/triptalk/domain/tripPlan/converter/AmadeusConverter.java (2)

20-24: 사용되지 않는 파라미터 제거 권장

tempId 파라미터가 사용되지 않으며 Javadoc에서도 "사용되지 않음"으로 명시되어 있습니다. 불필요한 파라미터는 제거하는 것이 좋습니다.

     public static Flight toFlight(
             AmadeusResponse.FlightOffer flightOffer,
-            Boolean isOutbound,
-            Long tempId
+            Boolean isOutbound
     ) {

71-88: 하드코딩된 환율은 시간이 지나면 정확도가 떨어집니다

환율이 코드에 하드코딩되어 있어 실제 환율과 차이가 발생할 수 있습니다. PR 설명에 따르면 주간 업데이트가 진행되므로, 환율 API를 활용하거나 설정 파일에서 환율을 관리하는 방안을 고려해 주세요.

환율 API 통합 또는 설정 기반 환율 관리 구현을 도와드릴까요?

src/main/java/com/example/triptalk/domain/tripPlan/service/FlightServiceImpl.java (1)

23-23: PAGE_SIZE 설정 외부화 권장

페이지 크기를 application.yml에서 설정할 수 있도록 @Value 어노테이션 사용을 고려해 보세요. 운영 중 튜닝이 용이해집니다.

src/main/java/com/example/triptalk/domain/tripPlan/entity/Flight.java (2)

9-15: 엔티티에 @Setter 사용 검토

JPA 엔티티에 @Setter를 사용하면 의도치 않은 필드 변경이 가능해집니다. 현재 코드에서는 @Builder로 생성하고 업데이트가 없으므로, @Setter 제거를 권장합니다.

 @Builder
 @Getter
-@Setter
 @NoArgsConstructor
 @AllArgsConstructor
 @Entity
 public class Flight extends BaseEntity {

14-40: 커서 기반 조회를 위한 인덱스 추가 권장

커서 기반 페이지네이션에서 id 기준 조회가 빈번할 것으로 예상됩니다. Primary Key는 자동으로 인덱싱되지만, 복합 조건 조회가 추가될 경우 인덱스 전략을 검토해 주세요.

src/main/java/com/example/triptalk/domain/tripPlan/service/AmadeusService.java (2)

30-34: 토큰 갱신 시 이중 검사 잠금(Double-Checked Locking) 적용 권장

현재 구현에서는 여러 스레드가 동시에 토큰 만료를 감지하면 중복 토큰 발급 요청이 발생할 수 있습니다.

+    private final Object tokenLock = new Object();
+
     private String getAccessToken() {
         // 토큰이 유효하면 재사용
         if (accessToken != null && System.currentTimeMillis() < tokenExpiryTime) {
             return accessToken;
         }
+        
+        synchronized (tokenLock) {
+            // Double-check after acquiring lock
+            if (accessToken != null && System.currentTimeMillis() < tokenExpiryTime) {
+                return accessToken;
+            }
+            // ... token fetch logic ...
+        }

97-102: URL 파라미터 인코딩 누락

쿼리 파라미터를 직접 문자열로 연결하면 특수 문자 인코딩이 누락될 수 있습니다. UriComponentsBuilder 사용을 권장합니다.

-            String dateStr = departureDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
-            String fullUrl = String.format(
-                    "%s?originLocationCode=%s&destinationLocationCode=%s&departureDate=%s&adults=%d&max=%d",
-                    url, originLocationCode, destinationLocationCode, dateStr, adults, max
-            );
+            String fullUrl = UriComponentsBuilder.fromHttpUrl(url)
+                    .queryParam("originLocationCode", originLocationCode)
+                    .queryParam("destinationLocationCode", destinationLocationCode)
+                    .queryParam("departureDate", departureDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
+                    .queryParam("adults", adults)
+                    .queryParam("max", max)
+                    .toUriString();
src/main/java/com/example/triptalk/domain/tripPlan/dto/AmadeusResponse.java (5)

5-5: 와일드카드 임포트는 필요 시 개별 임포트로 정리하는 것을 검토해 보세요.

import lombok.*; 형태는 일부 정적 분석/컨벤션 설정에서 경고가 날 수 있습니다. 사용하는 애노테이션만 명시적으로 임포트하면 가독성과 도구 호환성이 조금 더 좋아집니다.

-import lombok.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;

26-35: FlightOffersResponse에서 data/meta/dictionaries 분리가 실용적인 선택입니다.

현재 사용 중인 data 만 구체 타입으로 두고, meta·dictionariesObject 로 둔 것은 필요 이상으로 타입을 조이지 않으면서도 응답 전체를 보존하려는 용도로는 적절해 보입니다. 다만 이 필드들을 비즈니스 로직에서 적극 활용하게 되면, 별도 DTO 타입으로 정의해 두면 컴파일 타임 검증을 더 받을 수 있습니다.


37-58: itineraries/segments를 배열로 둔 설계는 괜찮지만, 컬렉션 활용을 고려해 볼 수 있습니다.

현재 JSON 구조와 1:1 매핑이라는 관점에서는 배열(Itinerary[], Segment[]) 사용이 충분히 타당합니다. 다만 이후 스트림/컬렉션 연산을 많이 하게 된다면 DTO에서 바로 List<Itinerary>, List<Segment> 를 사용하거나, 배열을 List 로 감싸는 헬퍼 메서드를 추가하는 것도 선택지로 두시면 좋겠습니다.


74-92: 출발/도착 시간 필드를 String으로 유지하는 선택은 단순 매핑에는 적절합니다.

Amadeus 원본 응답을 그대로 보존하는 DTO 용도에는 at 을 String으로 두는 것이 간단하고 이해하기 좋습니다. 다만 시간대·차이 계산 같은 비즈니스 로직이 많아지면, 도메인 레벨로 변환할 때 OffsetDateTime/ZonedDateTime 등 JDK 날짜 타입으로 변환해 두면 런타임 오류를 줄이는 데 도움이 됩니다.


103-112: Price를 String으로 보존하는 설계는 원본 유지에는 좋고, 계산은 별도 도메인 모델에서 처리하는 방식이 어울립니다.

환율 변환·합산 등 금액 연산이 잦다면, 이 DTO를 바로 쓰기보다는 이후 변환 레이어에서 currency/total/baseBigDecimal 기반의 값 객체로 매핑해 두는 편이 부동소수점/파싱 오류를 줄이기에 더 안전합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e31d17 and 3e22527.

📒 Files selected for processing (16)
  • src/main/java/com/example/triptalk/TriptalkApplication.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/controller/FlightController.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/converter/AmadeusConverter.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/converter/FlightConverter.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/dto/AmadeusResponse.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/dto/FlightResponse.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/entity/Flight.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/repository/FlightRepository.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/scheduler/FlightScheduler.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/service/AmadeusService.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/service/FlightService.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/service/FlightServiceImpl.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/util/AirportNameMapper.java (1 hunks)
  • src/main/java/com/example/triptalk/domain/tripPlan/util/CountryImageMapper.java (1 hunks)
  • src/main/java/com/example/triptalk/global/config/AmadeusProperties.java (1 hunks)
  • src/main/java/com/example/triptalk/global/config/SecurityConfig.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/com/example/triptalk/domain/tripPlan/service/FlightService.java (1)
src/main/java/com/example/triptalk/domain/tripPlan/dto/FlightResponse.java (1)
  • FlightResponse (12-71)
src/main/java/com/example/triptalk/domain/tripPlan/service/AmadeusService.java (1)
src/main/java/com/example/triptalk/domain/tripPlan/dto/AmadeusResponse.java (1)
  • AmadeusResponse (9-113)
src/main/java/com/example/triptalk/domain/tripPlan/scheduler/FlightScheduler.java (1)
src/main/java/com/example/triptalk/domain/tripPlan/converter/AmadeusConverter.java (1)
  • AmadeusConverter (11-133)
src/main/java/com/example/triptalk/domain/tripPlan/converter/AmadeusConverter.java (2)
src/main/java/com/example/triptalk/domain/tripPlan/dto/AmadeusResponse.java (1)
  • AmadeusResponse (9-113)
src/main/java/com/example/triptalk/domain/tripPlan/util/CountryImageMapper.java (1)
  • CountryImageMapper (12-239)
src/main/java/com/example/triptalk/domain/tripPlan/controller/FlightController.java (1)
src/main/java/com/example/triptalk/domain/tripPlan/dto/FlightResponse.java (1)
  • FlightResponse (12-71)
src/main/java/com/example/triptalk/domain/tripPlan/converter/FlightConverter.java (2)
src/main/java/com/example/triptalk/domain/tripPlan/dto/FlightResponse.java (1)
  • FlightResponse (12-71)
src/main/java/com/example/triptalk/domain/tripPlan/util/AirportNameMapper.java (1)
  • AirportNameMapper (6-138)
🔇 Additional comments (17)
src/main/java/com/example/triptalk/TriptalkApplication.java (1)

6-9: LGTM!

스케줄링 지원을 활성화하는 변경사항이 올바르게 구현되었습니다. FlightScheduler의 스케줄링 기능이 정상적으로 동작할 수 있습니다.

src/main/java/com/example/triptalk/domain/tripPlan/service/FlightService.java (1)

1-13: LGTM!

인터페이스 설계가 명확하고 Javadoc 문서화가 잘 되어 있습니다. 커서 기반 무한 스크롤 방식의 페이징 처리가 적절합니다.

src/main/java/com/example/triptalk/global/config/SecurityConfig.java (2)

44-45: LGTM!

항공권 검색 API를 비회원에게도 공개하는 설정이 적절합니다.


47-53: LGTM!

Swagger UI 접근 경로가 포괄적으로 잘 구성되어 있습니다.

src/main/java/com/example/triptalk/global/config/AmadeusProperties.java (1)

1-17: 테스트 환경 URL 사용을 확인하세요.

기본값으로 Amadeus 테스트 API 엔드포인트(https://test.api.amadeus.com)를 사용하고 있습니다. 개발 환경에서는 적절하지만, 프로덕션 배포 시 실제 API 엔드포인트로 변경이 필요합니다.

application.properties 또는 application-prod.properties에서 다음과 같이 오버라이드할 수 있는지 확인하세요:

amadeus.base-url=https://api.amadeus.com
src/main/java/com/example/triptalk/domain/tripPlan/controller/FlightController.java (1)

1-62: LGTM!

컨트롤러 구현이 우수합니다. Swagger 문서화가 매우 상세하고, 서비스 레이어로의 위임이 올바르게 처리되어 있습니다. 커서 기반 페이징 방식도 적절합니다.

src/main/java/com/example/triptalk/domain/tripPlan/util/CountryImageMapper.java (1)

221-238: LGTM!

공항 코드를 이미지 URL로 매핑하는 로직이 잘 구현되어 있습니다. null/empty 체크와 기본 이미지 폴백 처리가 적절합니다.

src/main/java/com/example/triptalk/domain/tripPlan/util/AirportNameMapper.java (1)

1-139: LGTM!

공항 코드를 한국어 명칭으로 매핑하는 유틸리티가 잘 구현되어 있습니다. 주요 공항들을 포괄적으로 커버하고 있으며, 매핑되지 않은 코드에 대한 폴백 처리도 적절합니다.

src/main/java/com/example/triptalk/domain/tripPlan/repository/FlightRepository.java (1)

1-26: LGTM!

커서 기반 페이징 쿼리가 올바르게 구현되어 있습니다. Slice를 사용하여 불필요한 count 쿼리를 피하고, NULL 처리로 첫 페이지 조회가 가능합니다. 성능을 고려한 좋은 설계입니다.

src/main/java/com/example/triptalk/domain/tripPlan/converter/FlightConverter.java (2)

15-27: LGTM!

Flight 엔티티를 DTO로 변환하는 로직이 올바르게 구현되었습니다. AirportNameMapper를 통한 IATA 코드 → 한국어명 변환도 적절합니다.


38-40: The project's build.gradle explicitly configures languageVersion = JavaLanguageVersion.of(21), confirming that Java 21 is the target version. The use of List.getLast() is fully compatible and appropriate for this project. No changes needed.

Likely an incorrect or invalid review comment.

src/main/java/com/example/triptalk/domain/tripPlan/dto/FlightResponse.java (2)

14-47: LGTM!

FlightDTO가 잘 구성되었습니다. Swagger Schema 어노테이션을 통한 API 문서화와 Lombok을 활용한 보일러플레이트 코드 감소가 적절합니다.


49-70: LGTM!

커서 기반 페이지네이션을 위한 FlightListResultDTO가 적절하게 설계되었습니다. nextCursorId, hasNext, isFirst 필드를 통해 무한 스크롤 구현에 필요한 정보를 제공합니다.

src/main/java/com/example/triptalk/domain/tripPlan/scheduler/FlightScheduler.java (1)

168-179: 중첩 트랜잭션 동작 확인 필요

initialLoadFlights()@Transactional로 선언되었고 내부에서 updateFlights()를 호출합니다. 동일 클래스 내 메서드 호출은 프록시를 거치지 않아 updateFlights()@Transactional이 적용되지 않을 수 있습니다.

updateFlights()를 별도 서비스로 분리하거나, initialLoadFlights()에서 @Transactional을 제거하는 방안을 검토해 주세요.

src/main/java/com/example/triptalk/domain/tripPlan/service/FlightServiceImpl.java (1)

15-34: LGTM!

서비스 구현이 깔끔합니다. @Transactional(readOnly = true)를 통한 읽기 전용 트랜잭션 설정과 커서 기반 페이지네이션 로직이 적절합니다.

src/main/java/com/example/triptalk/domain/tripPlan/dto/AmadeusResponse.java (2)

1-9: Amadeus API 응답을 한 DTO로 묶은 구조가 명확합니다.

외부 API 전용 DTO를 AmadeusResponse 내부의 중첩 static 클래스로 정리해서, 도메인 모델과 책임이 잘 분리되어 있고 이후 컨버터/서비스에서 사용하기에도 깔끔해 보입니다.


11-24: AccessToken DTO가 토큰 응답 형식을 잘 반영하고 있습니다.

JSON의 snake_case 필드를 자바 필드와 명시적으로 매핑해 두어서, 토큰 응답 스펙을 추적·유지보수하기에 충분히 읽기 좋은 형태입니다.

Comment on lines +25 to +28
// 첫 번째 여정 정보 추출 (출발편 또는 귀환편)
AmadeusResponse.Itinerary itinerary = flightOffer.getItineraries()[isOutbound ? 0 : 1];
AmadeusResponse.Segment firstSegment = itinerary.getSegments()[0];
AmadeusResponse.Segment lastSegment = itinerary.getSegments()[itinerary.getSegments().length - 1];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

배열 접근 시 예외 발생 가능성

itineraries 배열이 비어있거나 isOutbound가 false일 때 itinerary가 1개만 있으면 ArrayIndexOutOfBoundsException이 발생합니다. 또한 segments 배열이 비어있을 경우에도 예외가 발생할 수 있습니다.

+        if (flightOffer.getItineraries() == null || flightOffer.getItineraries().length == 0) {
+            throw new IllegalArgumentException("Flight offer has no itineraries");
+        }
+        int itineraryIndex = isOutbound ? 0 : Math.min(1, flightOffer.getItineraries().length - 1);
         // 첫 번째 여정 정보 추출 (출발편 또는 귀환편)
-        AmadeusResponse.Itinerary itinerary = flightOffer.getItineraries()[isOutbound ? 0 : 1];
+        AmadeusResponse.Itinerary itinerary = flightOffer.getItineraries()[itineraryIndex];
+        if (itinerary.getSegments() == null || itinerary.getSegments().length == 0) {
+            throw new IllegalArgumentException("Itinerary has no segments");
+        }
         AmadeusResponse.Segment firstSegment = itinerary.getSegments()[0];
📝 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
// 첫 번째 여정 정보 추출 (출발편 또는 귀환편)
AmadeusResponse.Itinerary itinerary = flightOffer.getItineraries()[isOutbound ? 0 : 1];
AmadeusResponse.Segment firstSegment = itinerary.getSegments()[0];
AmadeusResponse.Segment lastSegment = itinerary.getSegments()[itinerary.getSegments().length - 1];
if (flightOffer.getItineraries() == null || flightOffer.getItineraries().length == 0) {
throw new IllegalArgumentException("Flight offer has no itineraries");
}
int itineraryIndex = isOutbound ? 0 : Math.min(1, flightOffer.getItineraries().length - 1);
// 첫 번째 여정 정보 추출 (출발편 또는 귀환편)
AmadeusResponse.Itinerary itinerary = flightOffer.getItineraries()[itineraryIndex];
if (itinerary.getSegments() == null || itinerary.getSegments().length == 0) {
throw new IllegalArgumentException("Itinerary has no segments");
}
AmadeusResponse.Segment firstSegment = itinerary.getSegments()[0];
AmadeusResponse.Segment lastSegment = itinerary.getSegments()[itinerary.getSegments().length - 1];

Comment on lines +115 to +118
try {
// 기존 데이터 모두 삭제
flightRepository.deleteAll();
log.info("기존 항공권 데이터 삭제 완료");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

데이터 일관성 위험: 삭제 후 저장 실패 시 데이터 손실

deleteAll() 후 API 호출이나 saveAll()이 실패하면 항공권 데이터가 모두 삭제된 상태로 남을 수 있습니다. 새 데이터를 먼저 임시 저장하거나, 실패 시 롤백 전략을 고려해 주세요.

대안으로 soft delete 또는 버전 기반 교체 패턴을 사용할 수 있습니다:

     @Transactional
     public void updateFlights() {
         log.info("=== 항공권 데이터 업데이트 시작 ===");

         try {
-            // 기존 데이터 모두 삭제
-            flightRepository.deleteAll();
-            log.info("기존 항공권 데이터 삭제 완료");
-
             // 7일 후 출발 날짜
             LocalDate departureDate = LocalDate.now().plusDays(7);

             List<Flight> allFlights = new ArrayList<>();
             // ... fetch logic ...
             
             // 일괄 저장
-            flightRepository.saveAll(allFlights);
+            if (!allFlights.isEmpty()) {
+                flightRepository.deleteAll();
+                flightRepository.saveAll(allFlights);
+            }
             log.info("=== 항공권 데이터 업데이트 완료: 총 {}건 ===", allFlights.size());

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +146 to +152
// API Rate Limit 방지를 위해 대기
Thread.sleep(1000);

} catch (Exception e) {
log.error("노선 조회 실패: {} → {}, 에러: {}",
route.originName, route.destinationName, e.getMessage());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

InterruptedException 처리 개선 필요

Thread.sleep()이 발생시키는 InterruptedException이 일반 Exception으로 처리되어 스레드의 인터럽트 상태가 손실됩니다.

                     // API Rate Limit 방지를 위해 대기
-                    Thread.sleep(1000);
+                    Thread.sleep(1000);
+                } catch (InterruptedException ie) {
+                    Thread.currentThread().interrupt();
+                    log.warn("항공권 업데이트 중 인터럽트 발생");
+                    break;
                 } catch (Exception e) {
                     log.error("노선 조회 실패: {} → {}, 에러: {}",
                             route.originName, route.destinationName, e.getMessage());
                 }
🤖 Prompt for AI Agents
In
src/main/java/com/example/triptalk/domain/tripPlan/scheduler/FlightScheduler.java
around lines 146 to 152, the current catch block treats InterruptedException
from Thread.sleep as a generic Exception and thus loses the thread's interrupt
status; change the code to catch InterruptedException separately, restore the
interrupt status with Thread.currentThread().interrupt(), log an appropriate
message, and either return/propagate if the task should stop; keep the generic
Exception catch for other errors and log them as before.

Comment on lines +21 to +26
private final AmadeusProperties amadeusProperties;
private final RestTemplate restTemplate = new RestTemplate();

private String accessToken;
private long tokenExpiryTime;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

스레드 안전성 문제 및 RestTemplate 주입 권장

  1. accessTokentokenExpiryTime이 동기화 없이 여러 스레드에서 접근될 수 있어 경쟁 조건이 발생할 수 있습니다.
  2. RestTemplate을 직접 인스턴스화하면 Spring의 설정(타임아웃, 인터셉터 등)을 적용할 수 없습니다.
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class AmadeusService {

     private final AmadeusProperties amadeusProperties;
-    private final RestTemplate restTemplate = new RestTemplate();
+    private final RestTemplate restTemplate;

-    private String accessToken;
-    private long tokenExpiryTime;
+    private volatile String accessToken;
+    private volatile long tokenExpiryTime;

RestTemplate Bean을 설정 클래스에서 정의해 주세요:

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
                .setConnectTimeout(Duration.ofSeconds(10))
                .setReadTimeout(Duration.ofSeconds(30))
                .build();
    }
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/triptalk/domain/tripPlan/service/AmadeusService.java
around lines 21 to 26, the class currently instantiates RestTemplate directly
and uses plain fields for accessToken and tokenExpiryTime which are unsafe for
concurrent access; change to have RestTemplate injected via constructor (define
a RestTemplate @Bean in a config class as suggested) and make token state
thread-safe by replacing accessToken and tokenExpiryTime with thread-safe
holders (e.g., AtomicReference<String> for token and AtomicLong for expiry) or
guard access/refresh with a synchronized block or ReentrantLock so only one
thread refreshes the token while others read stable values; ensure all
reads/writes use the chosen concurrency primitives and update code that
refreshes or reads the token accordingly.

Comment on lines +42 to +48
// 울산 공항 → 울산 이미지 (대왕암공원)
String ulsanImage = "https://images.unsplash.com/photo-1716902923395-1d9539c2266f?q=80&w=1674&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
AIRPORT_IMAGES.put("USN", ulsanImage); // 울산공항

// 청주 공항 → 청주 이미지 (속리산)
String cheongjuImage = "https://images.unsplash.com/photo-1716902923395-1d9539c2266f?q=80&w=1674&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
AIRPORT_IMAGES.put("CJJ", cheongjuImage); // 청주국제공항
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

이미지 URL 중복을 수정하세요.

울산공항(USN)과 청주공항(CJJ)에 동일한 이미지 URL이 사용되고 있습니다. 각 지역을 대표하는 서로 다른 이미지를 사용해야 합니다.

Line 47의 청주 이미지를 다른 URL로 변경하세요:

     // 청주 공항 → 청주 이미지 (속리산)
-    String cheongjuImage = "https://images.unsplash.com/photo-1716902923395-1d9539c2266f?q=80&w=1674&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
+    String cheongjuImage = "https://images.unsplash.com/photo-[청주-대표-이미지-URL]";
     AIRPORT_IMAGES.put("CJJ", cheongjuImage); // 청주국제공항

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/com/example/triptalk/domain/tripPlan/util/CountryImageMapper.java
around lines 42 to 48, the image URL for 청주공항 (CJJ) is a duplicate of 울산공항
(USN); replace the String assigned to cheongjuImage (line 47) with a different,
region-appropriate image URL (e.g., an Unsplash link representing 속리산/청주) and
keep the AIRPORT_IMAGES.put("CJJ", cheongjuImage) call unchanged so CJJ maps to
the new distinct URL.

Comment on lines +60 to +72
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

CORS 설정의 보안 취약점을 수정하세요.

모든 origin 패턴을 허용(setAllowedOriginPatterns("*"))하면서 동시에 credentials를 활성화(setAllowCredentials(true))하는 것은 보안 위험이 있습니다. 이 조합은 임의의 도메인이 인증 정보를 포함한 요청을 보낼 수 있게 합니다.

다음 중 하나의 방법으로 수정하세요:

  • 권장: 프로덕션에서 허용할 실제 origin 목록을 명시적으로 지정
  • 또는 credentials를 false로 설정

다음 diff를 적용하여 프로덕션 준비를 위한 구성으로 변경하세요:

     @Bean
     public CorsConfigurationSource corsConfigurationSource() {
         CorsConfiguration configuration = new CorsConfiguration();
-        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
+        configuration.setAllowedOrigins(Arrays.asList(
+                "http://localhost:3000",  // 로컬 개발
+                "https://your-domain.com"  // 프로덕션 도메인
+        ));
         configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
         configuration.setAllowedHeaders(Arrays.asList("*"));
         configuration.setAllowCredentials(true);
         configuration.setMaxAge(3600L);
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/global/config/SecurityConfig.java around
lines 60 to 72, the CORS config currently allows all origins with credentials
enabled which is insecure; change it to use an explicit list of allowed origins
for production (e.g., load from application properties or an environment
variable and call configuration.setAllowedOrigins(Arrays.asList(...))) and keep
setAllowCredentials(true) only when explicit origins are used; alternatively, if
you cannot supply explicit origins now, set
configuration.setAllowCredentials(false) to remove credential support; ensure
the UrlBasedCorsConfigurationSource registration remains the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 새로운 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Feat: 항공권 조회 API 구현

2 participants