Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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

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
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,17 @@ public interface PlacePlatformReferenceRepository extends JpaRepository<PlacePla
* @return PlacePlatformReference 리스트
*/
List<PlacePlatformReference> findByPlace(Place place);

/**
* 플랫폼과 플랫폼 ID로 PlacePlatformReference 조회
* - AI 콜백에서 네이버 placeId로 중복 체크 시 사용
*
* @param placePlatform 플랫폼 (NAVER, GOOGLE, KAKAO)
* @param placePlatformId 플랫폼별 장소 ID
* @return Optional<PlacePlatformReference>
*/
Optional<PlacePlatformReference> findByPlacePlatformAndPlacePlatformId(
PlacePlatform placePlatform,
String placePlatformId
);
}
Loading