Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .github/workflows/PROJECT-SPRING-SYNOLOGY-PR-PREVIEW.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ env:
# HEALTH_CHECK_PATH: '/actuator/health'
# HEALTH_CHECK_LOG_PATTERN: 'Started .* in [0-9.]+ seconds'
# API_DOCS_PATH: '/swagger-ui.html' 또는 '/swagger-ui/index.html'
HEALTH_CHECK_PATH: '/actuator/health'
HEALTH_CHECK_PATH: '/docs/swagger'
HEALTH_CHECK_LOG_PATTERN: 'Started .* in [0-9.]+ seconds'
API_DOCS_PATH: '/docs/swagger'
Comment on lines +110 to 112
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

Swagger 엔드포인트를 Health Check 경로로 사용 시 신뢰성 저하 가능성

/actuator/health는 Spring Boot의 전용 헬스체크 엔드포인트로, 애플리케이션 상태(DB 연결, 외부 서비스 등)를 확인하여 {"status":"UP"} 형태의 JSON을 반환합니다. 반면 /docs/swagger는 Swagger UI HTML 페이지로:

  • Swagger UI 로딩이 health endpoint보다 느릴 수 있음
  • Swagger 페이지가 표시되더라도 실제 애플리케이션이 완전히 정상 동작하는지 보장하지 않음 (false positive 가능)

API_DOCS_PATH는 이미 배포 완료 코멘트에 표시 용도로 사용되고 있으므로, HEALTH_CHECK_PATH는 기존 /actuator/health로 유지하는 것이 더 신뢰성 있는 헬스체크가 됩니다.

🔧 제안: 기존 health endpoint 유지
-  HEALTH_CHECK_PATH: '/docs/swagger'
+  HEALTH_CHECK_PATH: '/actuator/health'
   HEALTH_CHECK_LOG_PATTERN: 'Started .* in [0-9.]+ seconds'
   API_DOCS_PATH: '/docs/swagger'
📝 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
HEALTH_CHECK_PATH: '/docs/swagger'
HEALTH_CHECK_LOG_PATTERN: 'Started .* in [0-9.]+ seconds'
API_DOCS_PATH: '/docs/swagger'
HEALTH_CHECK_PATH: '/actuator/health'
HEALTH_CHECK_LOG_PATTERN: 'Started .* in [0-9.]+ seconds'
API_DOCS_PATH: '/docs/swagger'
🤖 Prompt for AI Agents
In @.github/workflows/PROJECT-SPRING-SYNOLOGY-PR-PREVIEW.yaml around lines 110 -
112, HEALTH_CHECK_PATH is set to the Swagger UI path which can cause unreliable
health checks; change the value of HEALTH_CHECK_PATH back to the dedicated
Spring Boot endpoint '/actuator/health' while leaving API_DOCS_PATH as
'/docs/swagger' and keep HEALTH_CHECK_LOG_PATTERN unchanged so the workflow uses
the proper health endpoint for liveness checks (update the HEALTH_CHECK_PATH
entry in the workflow where HEALTH_CHECK_PATH is defined).


Expand Down
199 changes: 148 additions & 51 deletions MS-AI/src/main/java/kr/suhsaechan/mapsy/ai/dto/AiCallbackRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

/**
* AI 서버로부터 Webhook Callback으로 받는 분석 결과 DTO
*
* @see <a href="https://github.com/MapSee-Lab/MapSy-BE/issues/16">GitHub Issue #16</a>
*/
@Getter
@Setter
Expand All @@ -22,105 +24,200 @@
@AllArgsConstructor
public class AiCallbackRequest {

@Schema(description = "Content UUID", example = "550e8400-e29b-41d4-a716-446655440000")
@NotNull(message = "contentId는 필수입니다.")
private UUID contentId;

@Schema(description = "처리 결과 상태", example = "SUCCESS", allowableValues = {"SUCCESS", "FAILED"})
@NotNull(message = "resultStatus는 필수입니다.")
private String resultStatus;

@Schema(description = "Content UUID (FAILED 상태일 때 필수, SUCCESS일 때는 contentInfo.contentId 사용 가능)")
private UUID contentId;
@Schema(description = "SNS 콘텐츠 정보 (SUCCESS 시 필수)")
@Valid
private SnsInfoCallback snsInfo;
Comment on lines +35 to +37
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

🧩 Analysis chain

🏁 Script executed:

rg -n "snsInfo" --type=java -A 3 -B 1 MS-AI/src/main/java

Repository: MapSee-Lab/MapSy-BE

Length of output: 521


🏁 Script executed:

# Look specifically for AiCallbackService to see how it validates snsInfo
fd "AiCallbackService" --type f -e java

Repository: MapSee-Lab/MapSy-BE

Length of output: 138


🏁 Script executed:

# Also search for validation logic related to SUCCESS status
rg -n "SUCCESS|resultStatus" --type=java -A 2 -B 2 MS-AI/src/main/java

Repository: MapSee-Lab/MapSy-BE

Length of output: 1018


🏁 Script executed:

cat -n MS-SNS/src/main/java/kr/suhsaechan/mapsy/sns/service/AiCallbackService.java

Repository: MapSee-Lab/MapSy-BE

Length of output: 26976


🏁 Script executed:

# Also check if there are any validation exception classes or error handlers
rg -n "snsInfo.*null|SnsInfoCallback|AiCallback" --type=java MS-SNS/src/main/java -A 2 -B 1

Repository: MapSee-Lab/MapSy-BE

Length of output: 9671


SUCCESS 상태일 때 snsInfo 필수 검증 부재

snsInfo가 "(SUCCESS 시 필수)"로 문서화되었으나, 현재 구현은 null 값을 허용합니다. AiCallbackService.updateContentWithSnsInfo()에서 null 체크는 있지만 단순히 경고를 로깅하고 처리를 계속하므로 API 계약을 위반합니다.

다음 중 하나를 권장합니다:

  • DTO에 @NotNull(message = "SUCCESS 상태에서는 snsInfo가 필수입니다.")를 추가하고, 조건부 검증이 필요하면 @ConditionalValidation 또는 커스텀 validator 구현
  • 또는 서비스 레이어에서 if ("SUCCESS".equals(request.getResultStatus()) && request.getSnsInfo() == null) 체크 후 명시적으로 CustomException 발생
🤖 Prompt for AI Agents
In `@MS-AI/src/main/java/kr/suhsaechan/mapsy/ai/dto/AiCallbackRequest.java` around
lines 35 - 37, The DTO AiCallbackRequest currently allows snsInfo to be null
despite the schema note; add validation so SUCCESS requests require it: either
annotate the snsInfo field in AiCallbackRequest with `@NotNull`(message = "SUCCESS
상태에서는 snsInfo가 필수입니다.") (or implement a conditional validator annotation) to
enforce at binding time, or modify AiCallbackService.updateContentWithSnsInfo()
to check if ("SUCCESS".equals(request.getResultStatus()) && request.getSnsInfo()
== null) and throw a suitable CustomException with that message; reference
AiCallbackRequest.snsInfo and AiCallbackService.updateContentWithSnsInfo() when
applying the change.


@Schema(description = "콘텐츠 정보")
@Schema(description = "추출된 장소 상세 목록")
@Valid
private ContentInfo contentInfo;
private List<PlaceDetailCallback> placeDetails;

@Schema(description = "추출된 장소 목록")
@Schema(description = "추출 처리 통계")
@Valid
private List<PlaceInfo> places;
private ExtractionStatistics statistics;

@Schema(description = "실패 사유 (FAILED 시)")
private String errorMessage;

/**
* 콘텐츠 정보
* SNS 콘텐츠 정보
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class ContentInfo {

@Schema(description = "Content UUID (백엔드에서 전송받은 UUID)", example = "123e4567-e89b-12d3-a456-426614174000")
@NotNull(message = "contentId는 필수입니다.")
private UUID contentId;
public static class SnsInfoCallback {

@Schema(description = "썸네일 URL", example = "https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg")
@NotNull(message = "thumbnailUrl은 필수입니다.")
private String thumbnailUrl;

@Schema(description = "SNS 플랫폼", example = "YOUTUBE", allowableValues = {"INSTAGRAM", "YOUTUBE", "YOUTUBE_SHORTS"})
@Schema(description = "SNS 플랫폼", example = "INSTAGRAM",
allowableValues = {"INSTAGRAM", "YOUTUBE", "YOUTUBE_SHORTS", "TIKTOK", "FACEBOOK", "TWITTER"})
@NotNull(message = "platform은 필수입니다.")
private String platform;

@Schema(description = "콘텐츠 제목", example = "일본 전국 라멘 투어 - 개당 1200원의 가성비 초밥")
// 필수 아님
private String title;
@Schema(description = "콘텐츠 타입", example = "reel")
@NotNull(message = "contentType은 필수입니다.")
private String contentType;

@Schema(description = "원본 SNS URL", example = "https://www.instagram.com/reel/ABC123/")
@NotNull(message = "url은 필수입니다.")
private String url;

@Schema(description = "작성자 ID", example = "username")
private String author;

@Schema(description = "게시물 본문", example = "여기 정말 맛있어! #맛집 #서울")
private String caption;

@Schema(description = "좋아요 수", example = "1234")
private Integer likesCount;

@Schema(description = "댓글 수", example = "56")
private Integer commentsCount;

@Schema(description = "콘텐츠 URL", example = "https://www.youtube.com/watch?v=VIDEO_ID")
private String contentUrl;
@Schema(description = "게시 날짜 (ISO 8601)", example = "2024-01-15T10:30:00Z")
private String postedAt;

@Schema(description = "업로더 아이디", example = "travel_lover_123")
private String platformUploader;
@Schema(description = "해시태그 리스트", example = "[\"맛집\", \"서울\"]")
private List<String> hashtags;

@Schema(description = "AI 콘텐츠 요약", example = "샷포로 3대 스시 맛집 '토리톤' 방문...")
private String summary;
@Schema(description = "대표 이미지/썸네일 URL", example = "https://...")
private String thumbnailUrl;

@Schema(description = "이미지 URL 리스트", example = "[\"https://...\"]")
private List<String> imageUrls;

@Schema(description = "작성자 프로필 이미지 URL", example = "https://...")
private String authorProfileImageUrl;
}

/**
* 장소 정보
* 장소 상세 정보
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class PlaceInfo {
public static class PlaceDetailCallback {

@Schema(description = "장소명", example = "명동 교자")
// 기본 정보
@Schema(description = "네이버 Place ID", example = "11679241")
@NotNull(message = "placeId는 필수입니다.")
private String placeId;

@Schema(description = "장소명", example = "늘푸른목장 잠실본점")
@NotNull(message = "name은 필수입니다.")
private String name;

@Schema(description = "주소", example = "서울특별시 중구 명동길 29")
@NotNull(message = "address는 필수입니다.")
private String address;
@Schema(description = "카테고리", example = "소고기구이")
private String category;

@Schema(description = "위도", example = "37.563476", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "latitude는 필수입니다.")
@Schema(description = "한줄 설명", example = "된장찌개와 냉면으로 완성하는 한상차림")
private String description;

// 위치 정보
@Schema(description = "위도", example = "37.5112")
private Double latitude;

@Schema(description = "경도", example = "126.983920", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "longitude는 필수입니다.")
@Schema(description = "경도", example = "127.0867")
private Double longitude;

@Schema(description = "국가 코드 (ISO 3166-1 alpha-2)", example = "KR")
private String country;
@Schema(description = "지번 주소", example = "서울 송파구 백제고분로9길 34 1F")
private String address;

@Schema(description = "도로명 주소", example = "서울 송파구 백제고분로9길 34 1F")
private String roadAddress;

@Schema(description = "카테고리", example = "한식 음식점")
private String category;
@Schema(description = "지하철 정보", example = "잠실새내역 4번 출구에서 412m")
private String subwayInfo;

@Schema(description = "전화번호", example = "02-776-5348")
private String phone;
@Schema(description = "찾아가는 길", example = "잠실새내역 4번 출구에서 맥도널드 골목 끼고...")
private String directionsText;

@Schema(description = "영업시간", example = "매일 10:30 - 21:30")
private String openingHours;
// 평점/리뷰
@Schema(description = "별점 (0.0~5.0)", example = "4.42")
private Double rating;

@Schema(description = "장소 설명", example = "칼국수와 만두로 유명한 맛집")
private String description;
@Schema(description = "방문자 리뷰 수", example = "1510")
private Integer visitorReviewCount;

@Schema(description = "AI 추출 원본 데이터", example = "명동 교자에서 칼국수 먹었어요 (caption, confidence: 0.95)")
private String rawData;
@Schema(description = "블로그 리뷰 수", example = "1173")
private Integer blogReviewCount;

@Schema(description = "언어 코드 (ISO 639-1)", example = "ko", allowableValues = {"ko", "en", "ja", "zh"})
private String language = "ko";
// 영업 정보
@Schema(description = "영업 상태", example = "영업 중")
private String businessStatus;

@Schema(description = "키워드 목록", example = "[\"#명동맛집\", \"#칼국수\", \"#만두\"]")
@Schema(description = "영업 시간 요약", example = "24:00에 영업 종료")
private String businessHours;

@Schema(description = "요일별 상세 영업시간", example = "[\"월 11:30 - 24:00\", \"화 11:30 - 24:00\"]")
private List<String> openHoursDetail;

@Schema(description = "휴무일 정보", example = "연중무휴")
private String holidayInfo;

// 연락처/링크
@Schema(description = "전화번호", example = "02-3431-4520")
private String phoneNumber;

@Schema(description = "홈페이지 URL", example = "http://example.com")
private String homepageUrl;

@Schema(description = "네이버 지도 URL", example = "https://map.naver.com/p/search/늘푸른목장/place/11679241")
private String naverMapUrl;

@Schema(description = "예약 가능 여부", example = "true")
private Boolean reservationAvailable;

// 부가 정보
@Schema(description = "편의시설 목록", example = "[\"단체 이용 가능\", \"주차\", \"발렛파킹\"]")
private List<String> amenities;

@Schema(description = "키워드/태그", example = "[\"소고기\", \"한우\", \"회식\"]")
private List<String> keywords;

@Schema(description = "TV 방송 출연 정보", example = "[\"줄서는식당 14회 (24.05.13)\"]")
private List<String> tvAppearances;

@Schema(description = "대표 메뉴", example = "[\"경주갈비살\", \"한우된장밥\"]")
private List<String> menuInfo;

@Schema(description = "대표 이미지 URL", example = "https://...")
private String imageUrl;

@Schema(description = "이미지 URL 목록", example = "[\"https://...\"]")
private List<String> imageUrls;
}

/**
* 추출 처리 통계
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class ExtractionStatistics {

@Schema(description = "LLM이 추출한 장소명 리스트", example = "[\"늘푸른목장\", \"강남역\"]")
private List<String> extractedPlaceNames;

@Schema(description = "LLM이 추출한 장소 수", example = "2")
private Integer totalExtracted;

@Schema(description = "네이버 지도에서 찾은 장소 수", example = "1")
private Integer totalFound;

@Schema(description = "검색 실패한 장소명", example = "[\"강남역\"]")
private List<String> failedSearches;
}
}
82 changes: 68 additions & 14 deletions MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/Place.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,50 +47,104 @@ public class Place extends SoftDeletableBaseEntity {
private String address;

@Column(length = 2, nullable = false)
private String country; //국가 코드 (ISO 3166-1 alpha-2: KR, US, JP, CN 등)
@Builder.Default
private String country = "KR"; // 국가 코드 (ISO 3166-1 alpha-2) - 기본값 KR

@Column(nullable = false, precision = 10, scale = 7)
@DecimalMin("-90.0")
@DecimalMax("90.0")
private BigDecimal latitude; //위도
private BigDecimal latitude; // 위도

@Column(nullable = false, precision = 10, scale = 7)
@DecimalMin("-180.0")
@DecimalMax("180.0")
private BigDecimal longitude; //경도
private BigDecimal longitude; // 경도

@Column(length = 100)
private String businessType; //업종
private String businessType; // 업종 (category 매핑)

@Column(length = 50)
private String phone;

@Column(length = 500)
private String openingHours; //영업시간
private String openingHours; // 영업시간 (레거시, businessHours로 대체)

@Column(columnDefinition = "TEXT")
private String description; //요약 설명
private String description; // 요약 설명

// Google Places API 추가 정보
// Google Places API 추가 정보 (레거시)
@Column(columnDefinition = "varchar(50)[]")
@JdbcTypeCode(SqlTypes.ARRAY)
private List<String> types; //장소 유형 배열 (restaurant, cafe, park 등)
private List<String> types; // 장소 유형 배열 (restaurant, cafe, park 등)

@Column(length = 30)
private String businessStatus; //영업 상태 (OPERATIONAL, CLOSED_TEMPORARILY, CLOSED_PERMANENTLY)
private String businessStatus; // 영업 상태 (영업 중, 영업 종료 등)

@Column(length = 500)
private String iconUrl; //Google 아이콘 URL
private String iconUrl; // Google 아이콘 URL (레거시)

@Column(precision = 2, scale = 1)
private BigDecimal rating; //평점 (0.0 ~ 5.0)
@Column(precision = 3, scale = 2)
@DecimalMin("0.0")
@DecimalMax("5.0")
private BigDecimal rating; // 평점 (0.0 ~ 5.0)

@Column
private Integer userRatingsTotal; //리뷰 수
private Integer userRatingsTotal; // 리뷰 수 (레거시, visitorReviewCount로 대체)

@Column(columnDefinition = "text[]")
@JdbcTypeCode(SqlTypes.ARRAY)
private List<String> photoUrls; //사진 URL 배열 (최대 10개)
private List<String> photoUrls; // 사진 URL 배열 (최대 10개)

// ========== 신규 필드 (AI 콜백 #16) ==========

@Column(length = 500)
private String roadAddress; // 도로명 주소

@Column
private Integer visitorReviewCount; // 방문자 리뷰 수

@Column
private Integer blogReviewCount; // 블로그 리뷰 수

@Column(length = 500)
private String businessHours; // 영업 시간 요약

@Column(columnDefinition = "varchar(100)[]")
@JdbcTypeCode(SqlTypes.ARRAY)
private List<String> openHoursDetail; // 요일별 상세 영업시간

@Column(length = 200)
private String holidayInfo; // 휴무일 정보

@Column(length = 500)
private String homepageUrl; // 홈페이지 URL

@Column(length = 500)
private String naverMapUrl; // 네이버 지도 URL

@Column
private Boolean reservationAvailable; // 예약 가능 여부

@Column(length = 200)
private String subwayInfo; // 지하철 정보

@Column(columnDefinition = "TEXT")
private String directionsText; // 찾아가는 길

@Column(columnDefinition = "varchar(100)[]")
@JdbcTypeCode(SqlTypes.ARRAY)
private List<String> amenities; // 편의시설 목록

@Column(columnDefinition = "varchar(200)[]")
@JdbcTypeCode(SqlTypes.ARRAY)
private List<String> tvAppearances; // TV 방송 출연 정보

@Column(columnDefinition = "varchar(200)[]")
@JdbcTypeCode(SqlTypes.ARRAY)
private List<String> menuInfo; // 대표 메뉴

@Column(length = 500)
private String imageUrl; // 대표 이미지 URL

/**
* 이 장소와 연결된 키워드 목록
Expand Down
Loading